用單元測試Junit徹底能夠知足平常開發自測,爲何還要學習TestNG,都影響了個人開發進度!html
最近技術部老大忽然宣佈:全體開發人員必須熟練掌握自動化測試框架TestNG,就有了上邊同事們的抱怨,是的,開始我也在抱怨,由於並不知道它是個什麼東東,但從開始接觸到慢慢編寫測試用例,應用到項目後,我發現它真的超實用。java
咱們來一塊兒看看它比Junit好在哪?node
TestNG[後面都簡稱爲TG]是一款爲了大量測試(好比測試時多接口數據依賴)須要,所誕生的一款測試框架,從簡單的單元測試再到集成測試甚至是框架級別的測試,均可以覆蓋到,所以是一款很是強大的測試框架!mysql
常規的TG的測試案例有三個步驟linux
運行整個項目的test-ng測試案例【若是是多模塊項目,則是進入到對應的模塊目錄運行命令或者配置】git
xmlgithub
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <skip>false</skip> <testFailureIgnore>false</testFailureIgnore> <suiteXmlFiles> <file>${project.basedir}/src/test/OrderTest.xml</file> </suiteXmlFiles> </configuration> </plugin>
MVN命令web
此處的命令會優先於pom.xml
中的配置,相對於java命令更優,由於mvn處理打包的一環,方便監控正則表達式
mvn clean package/install -DskipTests mvn clean package/install -Dmaven.test.skip=true #或者直接執行 mvn test
註解 | 做用 |
---|---|
@BeforeSuite | 被註解的方法,將在整個測試套件以前運行 |
@AfterSuite | 被註解的方法,將在整個測試套件以後運行 |
@BeforeTest | 被註解的方法,將在測試套件內全部用例執行以前運行 |
@AfterTest | 被註解的方法,將在測試套件內全部用例執行以後運行 |
@BeforeGroups | 被註解的方法,將在指定組內任意用例執行以前運行 |
@AfterGroups | 被註解的方法,將在指定組內任意用例執行以後運行 |
@BeforeClass | 被註解的方法,將在此方法對應類中的任意其餘的,被標註爲@Test 的方法執行前運行 |
@AfterClass | 被註解的方法,將在此方法對應類中的任意其餘的,被標註爲@Test 的方法執行後運行 |
@BeforeMethod | 被註解的方法,將在此方法對應類中的任意其餘的,被標註爲@Test的方法執行前運行 |
@AfterMethod | 被註解的方法,將在此方法對應類中的任意其餘的,被標註爲@Test的方法執行後運行 |
@DataProvider | 被註解的方法,強制返回一個 二維數組Object 做爲另一個@Test方法的數據工廠 |
@Factory | 被註解的方法,做爲對象工廠,強制返回一個對象數組 Object[ ] |
@Listeners | 定義一個測試類的監聽器 |
@Parameters | 定義一組參數,在方法運行期間向方法傳遞參數的值,參數的值在testng.xml中定義 |
@Test | 標記方法爲測試方法,若是標記的是類,則此類中全部的public方法都爲測試方法 |
備註:相關對應的屬性配置值,點進去對應類中查詢便可,不在一一贅述spring
由xml文件表示,包含一個或者多個測試案例,使用<suite>標籤包裹
通常來說,一個xml的<suite>
對應一個java類,除非特殊狀況,在java中須要特別指定<suite>
不然xml對應java類的全部@Test註解屬性suiteName默認都是xml中定義的<suite name='xxx'>
由<test>標籤表示,包含一個或者多個TestNG的類
這些測試類須要在提交新代碼前運行,保證基本功能不會被破壞
這些測試應該覆蓋軟件的全部功能,而且天天至少運行一次,即便有些狀況下你不想運行它
check-in test是Functional tests的子集
public class Test1 { @Test(groups = { "functest", "checkintest" }) public void testMethod1() { } @Test(groups = {"functest", "checkintest"} ) public void testMethod2() { } @Test(groups = { "functest" }) public void testMethod3() { } }
<test name="Test1"> <groups> <run> <include name="functest"/> <!-- <include name="checkintest"/> --> </run> </groups> <classes> <class name="example1.Test1"/> </classes> </test>
運行testng.xml文件結果:
全部@Test註解中,在xml中定義的一、二、3方法都會被運行,若是換成check-intest,則只運行方法一、2
總結:
1.xml文件定義測試案例的運行策略
2.所謂的測試案例類別,只是概念上的定義
--- 登記類的測試案例,若是不在xml編排中沒有涉及,它不必定運行
--- 功能性測試類,是咱們在xml編排中,必定會運行的測試案例
@Test public class Test1 { @Test(groups = { "windows.checkintest" }) public void testWindowsOnly() { } @Test(groups = {"linux.checkintest"} ) public void testLinuxOnly() { } @Test(groups = { "windows.functest" ) public void testWindowsToo() { } }
<test name="Test1"> <groups> <run> <include name="windows.*"/> </run> </groups> <classes> <class name="example1.Test1"/> </classes> </test>
你們發揮一下腦洞,應該能夠猜到運行結果【不要被windows的命名所引誘】
【官方原話,不建議使用此類寫法】
若是您開始重構Java代碼(標記中使用的正則表達式可能與您的方法再也不匹配)這會使您的測試框架極可能崩潰
package org.vk.test.springtest_testng; import org.testng.annotations.Test; public class Test1 { @Test(groups = {"functest", "checkintest"}) public void testMethod1() { System.out.println(1); } @Test(groups = {"functest", "checkintest"}) public void testMethod2() { System.out.println(2); } @Test(groups = {"functest"}) public void testMethod3() { System.out.println(3); } }
<suite name="Suite" parallel="classes" thread-count="1"> <test name="Test1"> <groups> <run> <include name="functest"/> </run> </groups> <classes> <class name="org.vk.test.springtest_testng.Test1"> <methods> <include name="testMethod*"></include> <exclude name="testMethod3"></exclude> </methods> </class> </classes> </test> </suite>
運行結果:testMethod3不會執行
總結:suit配置,從上而下,實際上是對編排規則的一個層層過濾
對應group配置,顯然全部方法都會執行,可是到了class配置時,對其再次配置,過濾了方法3
java類,包含至少一個TG的註解,由<class>表示
java方法,含有@Test註解,默認狀況下,test方法的返回值都會忽略,除非聲明須要返回
<suite allow-return-values="true"> <!--或者--> <test allow-return-values="true">
測試方法組,不只能夠定義方法屬於哪一個group,還能夠設置group包含哪些子group,TG會自動調用
能夠在testng.xml的<test>or<suite>
中定義
若是在<suite>中指定組「a」,在<test>中指定組「b」,則「a」和「b」都將包括在內
@Test(groups = {"checkintest", "broken"} ) public void testMethod2() { }
<test name="Simple example"> <groups> <run> <include name="checkintest"/> <exclude name="broken"/> </run> </groups> <classes> <class name="example1.Test1"/> </classes> </test>
運行結果:什麼都沒有
總結:xml配置,決定最終結果,不管出現什麼反思惟的配置,理論上什麼結果就是最終結果
【官方提示】
達到禁用效果,也能夠經過使用@Test和@Before/After註釋上的「enabled」屬性單獨禁用測試。
@Test(groups = { "checkin-test" }) public class All { @Test(groups = { "func-test" ) public void method1() { ... } public void method2() { ... } }
結果:method1屬於checkin-test和func-test兩個組,method2僅屬於checkin-test組
group裏面含有子group,稱爲 MetaGroups
,我本身稱爲元組
functest和checkintest在名詞解釋的test小節中有提到,此處咱們將其funtest再細化分windows、linux組
新增all組,包含兩個大組
<suite name="Suite" parallel="classes" thread-count="1"> <test name="Test1"> <groups> <define name="functest"> <include name="windows"/> <include name="linux"/> </define> <define name="all"> <include name="functest"/> <include name="checkintest"/> </define> <run> <include name="all"/> </run> </groups> <classes> <class name="example1.Test1"/> </classes> </test> </suite>
結果:全部方法都會運行
總結:能夠對多個測試進行組合編排,造成group表示一個大的功能
testng.xml的每一個section部分,均可以在ant、命令行對應的文檔中找到【另外2種調用TG的方式】
我的理解:通常來說,直接在idea、eclipse中運行@Test類也能夠,但爲何咱們須要testng.xml?
緣由:若是須要對java類、方法的測試案例進行編排,不使用xml進行編排,僅僅提供xml文件之外的方式,很難作到高度靈活的業務邏輯測試!
測試案例的參數,測試方法的入參配置,執行方法時用到
測試案例的參數
@Parameters({ "first-name" }) @Test public void testSingleString(String firstName) { System.out.println("Invoked testString " + firstName); assert "Cedric".equals(firstName); }
<suite name="My suite"> <parameter name="first-name" value="Cedric"/> <test name="Simple example"> <-- ... --> </suite>
- XML參數映射到Java參數的順序與註釋中的順序相同,若是數量不匹配,TestNG將發出一個錯誤。
- 參數的做用域:
在testng.xml中,能夠在<suite>標記下或<test>下聲明它們。
若是兩個參數具備相同的名稱,則在<test>中定義的參數具備優先權。若是您須要指定一個適用於全部測試的參數,並僅對某些測試重寫其值,則這很方便。
@Parameters("hello") @Test(groups = {"functest"}) public void testMethod4(@Optional("hello") String hello) { System.out.println(hello); }
【官方說明】
@Parameters註解,一樣適用於@Before/After
and@Factory
此類的註解!結果:輸出hello字符串
總結:以上的方式,適合簡單參數配置,不適合作複雜對象注入
這種方式是爲了彌補第一種方式而衍生的,若是參數構建比較複雜,複雜對象沒法在xml或者利用@Optional註解
構建時,就須要這種方式了
//This method will provide data to any test method that declares that its Data Provider //is named "test1" @DataProvider(name = "test1",parallel = true) public Object[][] createData1() { return new Object[][] { { "Cedric", new Integer(36) }, { "Anne", new Integer(37)}, }; } //This test method declares that its data should be supplied by the Data Provider //named "test1" @Test(dataProvider = "test1") public void verifyData1(String n1, Integer n2) { System.out.println(n1 + " " + n2); }
輸出結果:
Cedric 36 Anne 37
備註:併發線程數設置能夠在xml的<suite data-provider-thread-count="20">
中調整,默認配置10個線程
若是要在不一樣的線程池中運行一些特定的數據提供程序,則須要從不一樣的XML文件運行它們。
public class StaticProvider { @DataProvider(name = "create") public static Object[][] createData() { return new Object[][] { new Object[] { new Integer(42) } }; } } public class MyTest { @Test(dataProvider = "create", dataProviderClass = StaticProvider.class) public void test(Integer n) { // ... } }
@DataProvider(name = "test1") public MyCustomData[] createData() { return new MyCustomData[]{ new MyCustomData() }; } @DataProvider(name = "test1") public Iterator<MyCustomData> createData() { return Arrays.asList(new MyCustomData()).iterator(); } @DataProvider(name = "test1") public Iterator<Stream> createData() { return Arrays.asList(Stream.of("a", "b", "c")).iterator(); }
Object[] []
數組第一個維度的大小是調用測試方法的次數數組第二個維度的大小包含必須與測試方法的參數類型兼容的對象數組
Iterator<Object[ ]>
和第一種返回類型不一樣,這種方式容許你對返回值作懶初始化惟一的限制是在迭代器的狀況下,它的參數類型不能被顯式地參數化
若是有不少參數集要傳遞給方法,而且不想預先建立全部參數集,那麼這一點特別有用
下面這幾個例子都有一個特性: can't be explicitly parametrized
@DataProvider(name = "test1")
不容許顯示的初始化,有使用經驗的人能夠私聊!目前我這邊直接把代碼粘上去,是會報錯的。
工廠還能夠與數據提供程序一塊兒使用,能夠經過將@Factory註釋放在常規方法或構造函數上來利用此功能
使用@Factory可動態地建立測試,通常用來建立一個測試類的多個實例,每一個實例中的全部測試用例都會被執行,@Factory構造實例的方法必須返回Object[]。
下一個小節的dependencyes,就有對其應用,此處不作過多的說明了就,官網示例和其相差不大。
某些狀況,咱們須要對執行順序作編排,TG提供了2種方式:
xml
<test name="My suite"> <groups> <dependencies> <group name="c" depends-on="a b" /> <group name="z" depends-on="c" /> </dependencies> </groups> </test>
在@Test
註解上設置依賴的屬性: dependsOnMethods
或者 dependsOnGroups
依賴的全部方法都必須已運行併成功才能運行。
若是依賴項中至少發生一個錯誤,則不會在報表中調用並標記爲跳過
默認狀況下alwaysRun=false
@Test(groups = { "init" }) public void serverStartedOk() {} @Test(groups = { "init" }) public void initEnvironment() {} @Test(dependsOnGroups = { "init.*" }) public void method1() {}
若是依賴的方法失敗,而且對它有硬依賴關係,則依賴它的方法不會標記爲失敗,而是標記爲跳過。跳過的方法將在最終報告中以一樣的方式報告(在HTML中,顏色既不是紅色也不是綠色),這一點很重要,由於跳過的方法不必定是失敗的。
高級用法參照【 http://beust.com/weblog/2004/... 】
即便有些方法失敗了,你也會一直在追求你所依賴的方法。
當您只想確保您的測試方法以特定的順序運行,但它們的成功並不真正依賴於其餘方法的成功時,這很是有用。
經過在@Test註釋中添加alwaysRun=true
得到軟依賴性。
測試案例按照實例進行分組運行
一般的dependsOnGroups依賴註解,只能實現如下的模式:
a(1) a(2) b(2) b(2)
可是也有一種狀況, 假設咱們要實現多組用戶一組操做行爲:登陸、登出
signIn("us") signOut("us") signIn("uk") signOut("uk")
此時咱們須要使用@Factory註解,配合group-by-instance來實現
Test1.java
public class Test1 { private String countryName; public Test1(String countryName) { this.countryName = countryName; } @Test public void signIn() { System.out.println(countryName + " signIn"); } @Test(dependsOnMethods = "signIn") public void signOut() { System.out.println(countryName + " signOut"); } }
Test.xml
<suite name="Suite"> <test name="Test1" > <classes> <class name="org.vk.test.springtest_testng.Test1"></class> </classes> </test> </suite>
TestFactory.java
public class TestFactory { @Factory(dataProvider = "init") public Object[] test(int nums) { Object[] object = new Object[nums]; List<String> ctrys = Arrays.asList("US", "UK", "HK"); for (int i = 0; i < nums; i++) { Test1 t = new Test1(ctrys.get(i)); object[i] = t; } return object; } @DataProvider//可缺省名稱,默認以方法名爲準 public Object[][] init() { return new Object[][]{new Object[]{3}}; } }
TestFactory.xml
<suite name="Suite2" group-by-instances="true"> <test name="TestFactory" > <classes> <class name="org.vk.test.springtest_testng.TestFactory"/> </classes> </test> </suite>
verbose="2" 標識的就是記錄的日誌級別,共有0-10的級別,其中0表示無,10表示最詳細
默認就是true,<test>
標籤下的class按順序執行
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="Preserve order test runs"> <test name="Regression 1" preserve-order="true"> <classes> <class name="com.pack.preserve.ClassOne"/> <class name="com.pack.preserve.ClassTwo"/> <class name="com.pack.preserve.ClassThree"/> </classes> </test> </suite>
若是您運行多個套件文件(例如「java org.testng.testng testng1.xml testng2.xml」),而且但願這些套件在單獨的線程中運行,那麼這很是有用。可使用如下命令行標誌指定線程池的大小:
java org.testng.TestNG -suitethreadpoolsize 3 testng1.xml testng2.xml testng3.xml
<suite name="My suite" parallel="methods" thread-count="5"></suite> <!--TestNG將在單獨的線程中運行全部的測試方法。依賴方法也將在單獨的線程中運行,但它們將遵循您指定的順序--> <suite name="My suite" parallel="tests" thread-count="5"></suite> <!--TestNG將在同一線程中的同一個<test>標記中運行全部方法,但每一個<test>標記將在單獨的線程中。這容許您將全部非線程安全的類分組到同一個<test>中,並保證它們都將在同一個線程中運行,同時利用TestNG使用盡量多的線程來運行測試。--> <suite name="My suite" parallel="classes" thread-count="5"></suite> <!--TestNG將在同一線程中運行同一類中的全部方法,但每一個類將在單獨的線程中運行。--> <suite name="My suite" parallel="instances" thread-count="5"></suite> <!--TestNG將在同一線程中運行同一實例中的全部方法,但兩個不一樣實例上的兩個方法將在不一樣線程中運行。-->
從三個不一樣的線程調用函數testServer十次。10秒的超時保證沒有一個線程會永遠阻塞這個線程。
@Test(threadPoolSize = 3, invocationCount = 10, timeOut = 10000)//timeOut無論是否多線程都有效 public void testServer() { ... ... }
每次在套件中測試失敗時,TestNG都會在輸出目錄中建立一個名爲TestNG-failed.xml的文件。
這個XML文件包含了只從新運行失敗的方法所必需的信息,容許您快速地從新生成失敗,而沒必要運行整個測試。
所以,典型會話以下所示:
java -classpath testng.jar;%CLASSPATH% org.testng.TestNG -d test-outputs testng.xml java -classpath testng.jar;%CLASSPATH% org.testng.TestNG -d test-outputs test-outputs\testng-failed.xml
testng-failed.xml將包含全部必需的依賴方法,這樣您就能夠保證在沒有任何跳過失敗的狀況下運行失敗的方法。
其餘方式:經過測試報告
target\surefire-reports\index.html
或者第三方測試報告插件也能夠獲取
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UCKF794W-1581562336476)(C:UsersdellAppDataRoamingTyporatypora-user-images1579156460298.png)]
若是測試案例出現錯誤,想要啓用TG的重試步驟以下:
import org.testng.IRetryAnalyzer; import org.testng.ITestResult; public class MyRetry implements IRetryAnalyzer { private int retryCount = 0; private static final int maxRetryCount = 3; @Override public boolean retry(ITestResult result) { if (retryCount < maxRetryCount) { retryCount++; return true; } return false; } }
import org.testng.Assert; import org.testng.annotations.Test; public class TestclassSample { @Test(retryAnalyzer = MyRetry.class) public void test2() { Assert.fail(); } }
java org.testng.TestNG testng1.xml [testng2.xml testng3.xml ...
選項 | 參數類型 | 說明 | ||
---|---|---|---|---|
-configfailurepolicy | skip |
continue |
||
-d | 一個目錄 | 生成測試報告的地址 | ||
-dataproviderthreadcount | 並行運行測試案例的默認線程數 | 並行測試時,設置默認的最大線程數,前提是使用【-parallel】選項纔會生效 | ||
-excludegroups | 逗號分割的組列表 | 排除須要運行的組列表 | ||
-groups | 逗號分割的組列表 | 須要運行的組列表 (示例. "windows,linux,regression" ). |
||
-listener | classpath目錄下能找到的java類 | 容許本身定義測試監聽器,但必需要實現org.testng.ITestListener |
||
-usedefaultlisteners | true |
false |
||
-methods | 逗號分割的全路徑類方法 | 指定特定的方法運行,com.OBJ1.test,com.Obj2.test |
||
-methodselectors | 逗號分割的方法優先級列表 | 指定方法選擇器,com.Selector1:3,com.Selector2:2 |
||
-parallel | methods\ | tests\ | classes | 設置默認測試的並行線程數。若是未設置,默認機制是單線程測試。這能夠在套件定義中重寫。能夠是方法、測試案例、類 |
-reporter | 自定義報表監聽器 | 與 -listener 選項功能類似,只是它容許在報告中額外設置JavaBeans的屬性 Example: -reporter com.MyReporter:methodFilter=*insert*,enableFiltering=true 能夠出現一次或者屢次,若是有必要的話 |
||
-sourcedir | 逗號分割的目錄 | JavaDoc註釋的測試源所在的目錄。只有在使用JavaDoc類型註釋時,此選項纔是必需的. "src/test" or "src/test/org/testng/eclipse-plugin;src/test/org/testng/testng" |
||
-suitename | 默認套件suit名稱 | 若是suit.xml或者源碼配置了相關名稱,則忽略此配置 | ||
-testclass | classpath目錄下,逗號分割的java類列表 | "org.foo.Test1,org.foo.test2" |
||
-testjar | jar包名稱 | 指定包含測試類的jar文件。若是在該jar文件的根目錄下找到testng.xml文件,則將使用該文件,不然,在該jar文件中找到的全部測試類都將被視爲測試類。 | ||
-testname | 測試案例的默認名稱 | 指定在命令行上定義的測試的名稱。若是suite.xml文件或源代碼指定了不一樣的測試名稱,則忽略此選項。若是用雙引號「like this」將測試名稱括起來,則有可能建立一個包含空格的測試名稱。 | ||
-testnames | 逗號分割的測試名稱 | 只有測試案例的 <test> 匹配上此處的配置纔會運行 | ||
-testrunfactory | 逗號分隔的classpath下能夠找到的java類 | 容許本身定義要運行的類. 類必需要實現org.testng.ITestRunnerFactory |
||
-threadcount | 數字 | 設置併發運行測試案例的最大線程數.只有使用-parallel選項纔會生效 若是suit中有定義,則該配置會被忽略/覆蓋。 | ||
-xmlpathinjar | jar包下xml的路徑 | 包含測試jar中有效XML文件的路徑(例如「resources/testng.XML」)。默認值是「testng.xml」,這意味着在jar文件的根目錄下有一個名爲「testng.xml」的文件。除非指定了「-testjar」,不然將忽略此選項。 |
https://testng.org/doc/ant.html
https://testng.org/doc/eclips...
https://testng.org/doc/idea.html
本例建立一個TestNG對象並運行測試類Run2。
它還添加了一個TestListener。您可使用適配器類org.testng.TestListenerAdapter,也能夠本身實現org.testng.ITestListener。此接口包含各類回調方法,可用於跟蹤測試什麼時候開始、成功、失敗等。
TestListenerAdapter tla = new TestListenerAdapter(); TestNG testng = new TestNG(); testng.setTestClasses(new Class[] { Run2.class }); testng.addListener(tla); testng.run();
再好比若是想實現相似於xml這樣的功能:
<suite name="TmpSuite" > <test name="TmpTest" > <classes> <class name="test.failures.Child" /> <classes> </test> </suite>
那麼你能夠這樣編程:
// 1.編排 XmlSuite suite = new XmlSuite(); suite.setName("TmpSuite"); XmlTest test = new XmlTest(suite); test.setName("TmpTest"); List<XmlClass> classes = new ArrayList<XmlClass>(); classes.add(new XmlClass("test.failures.Child")); test.setXmlClasses(classes) ; // 2.運行 List<XmlSuite> suites = new ArrayList<XmlSuite>(); suites.add(suite); TestNG tng = new TestNG(); tng.setXmlSuites(suites); tng.run();
【 https://jitpack.io/com/github... 】
若是testng.xml中的<include>和<exclude>標記不足以知足您的須要,您可使用BeanShell表達式來決定某個測試方法是否應包含在測試運行中。
您能夠在<test>標記下指定此表達式:
<test name="BeanShell test"> <method-selectors> <method-selector> <script language="beanshell"><![CDATA[ groups.containsKey("test1") ]]></script> </method-selector> </method-selectors> <!-- ... -->
當在testng.xml中找到script
標記時,testng將忽略當前<test>標記中組和方法的後續<include>和<exclude>,您的BeanShell表達式將是決定是否包含測試方法的惟一方法。
另外有幾個地方還須要注意:
它必須返回布爾值。除此約束外,容許任何有效的BeanShell代碼(例如,您可能但願在工做日期間返回true,而在週末期間返回false,這將容許您根據日期以不一樣的方式運行測試)。
java.lang.reflect.Method --- method
: the current test method.
org.testng.ITestNGMethod --- testngMethod: the description of the current test method.java.util.Map groups---
: a map of the groups the current test method belongs to.
TestNG容許您在運行時修改全部註釋的內容,若是但願在運行時重寫特定註解,須要使用到註解轉換器.
實踐步驟:
1.實現 IAnnotationTransformer 接口
public class MyTransformer implements IAnnotationTransformer { public void transform(ITest annotation, Class testClass, Constructor testConstructor, Method testMethod) { if ("invoke".equals(testMethod.getName())) { annotation.setInvocationCount(5);////執行5次 } } }
2.運行cmd命令或者編程式運行。
TestNG tng = new TestNG()
【官方原話】
IAnnotationTransformer只容許您修改@Test註釋。若是須要修改另外一個TestNG註釋(@Factory或@DataProvider),請使用IAnnotationTransformer2接口
一旦TestNG計算出調用測試方法的順序,這些方法就被分紅兩組:
1.按順序運行【包含依賴關係】
2.不按特定順序運行
爲了對屬於第二類的方法有更多的控制,TestNG定義瞭如下接口:
public interface IMethodInterceptor { List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext context); }
入參:
傳入參數的方法列表是能夠按任何順序運行的全部方法。
返回:
能夠對入參方法列表進行編程,不改、縮減、擴大methods均可以
執行:
java -classpath "testng-jdk15.jar:test/build" org.testng.TestNG -listener test.methodinterceptors.NullMethodInterceptor -testclass test.methodinterceptors.FooTest
示例:
這裏有一個方法攔截器,它將對方法從新排序,以便始終首先運行屬於fast
組的測試方法:
public List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext context) { List<IMethodInstance> result = new ArrayList<IMethodInstance>(); for (IMethodInstance m : methods) { Test test = m.getMethod().getConstructorOrMethod().getAnnotation(Test.class); Set<String> groups = new HashSet<String>(); for (String group : test.groups()) { groups.add(group); } if (groups.contains("fast")) { result.add(0, m); } else { result.add(m); } } return result; }
詳細示例【 https://www.jianshu.com/p/2f9... 】
有幾個接口容許您修改TestNG的行爲。這些接口被普遍地稱爲「TestNG監聽器」
IAnnotationTransformer (doc, javadoc)對註釋進行轉換,須要實現該接口,並重寫transform 方法 IAnnotationTransformer2 (doc, javadoc)也是對註釋進行轉換,在上面的接口不知足的狀況下,使用較少 IHookable (doc, javadoc) 執行測試方法前進行受權檢查,根據受權結果執行測試 IInvokedMethodListener (doc, javadoc) 調用方法前、後啓用該監聽器,經常使用於日誌的採集 IMethodInterceptor (doc, javadoc) 調用方法前、後啓用該監聽器,經常使用於日誌的採集 IReporter (doc, javadoc) 運行全部套件時都將調用此方法,後續可用於自定義測試報告 ISuiteListener (doc, javadoc) 測試套件執行前或執行後嵌入相關邏輯 ITestListener (doc, javadoc) 經常使用TestListenerAdapter來替代
1.命令行
2.ant命令
3.xml配置
<suite> <listeners> <listener class-name="com.example.MyListener" /> <listener class-name="com.example.MyMethodInterceptor" /> </listeners> </suite>
或者
@Listeners({ com.example.MyListener.class, com.example.MyMethodInterceptor.class }) public class MyTest { // ... }
4.使用ServiceLoader
注意,@Listeners註釋將應用於整個套件文件,就像您在testng.xml文件中指定它同樣。
若是要限制其做用域(例如,僅在當前類上運行),偵聽器中的代碼能夠首先檢查即將運行的測試方法,而後決定
執行什麼操做!
1.自定義一個新註解
@Retention(RetentionPolicy.RUNTIME) @Target ({ElementType.TYPE}) public @interface DisableListener {}
2.監聽檢查
public void beforeInvocation(IInvokedMethod iInvokedMethod, ITestResult iTestResult) { ConstructorOrMethod consOrMethod =iInvokedMethod.getTestMethod().getConstructorOrMethod(); DisableListener disable = consOrMethod.getMethod().getDeclaringClass().getAnnotation(DisableListener.class); if (disable != null) { return; } // 恢復正常操做 }
3.註釋不調用監聽器的測試類
@DisableListener @Listeners({ com.example.MyListener.class, com.example.MyMethodInterceptor.class }) public class MyTest { // ... }
原生方法(由TestNG自己執行)
擴展方法(由依賴的注入框架執行,如:Guice)。
註解 | ITestContext | XmlTest | Method | Object[] | ITestResult |
---|---|---|---|---|---|
@BeforeSuite | Yes | No | No | No | No |
@BeforeTest | Yes | Yes | No | No | No |
@BeforeGroups | Yes | Yes | No | No | No |
@BeforeClass | Yes | Yes | No | No | No |
@BeforeMethod | Yes | Yes | Yes | Yes | Yes |
@Test | Yes | No | No | No | No |
@DataProvider | Yes | No | Yes | No | No |
@AfterMethod | Yes | Yes | Yes | Yes | Yes |
@AfterClass | Yes | Yes | No | No | No |
@AfterGroups | Yes | Yes | No | No | No |
@AfterTest | Yes | Yes | No | No | No |
@AfterSuite | Yes | No | No | No | No |
package org.vk.test.springtest_testng; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import org.vk.demo.EatCompentConfig; import java.lang.reflect.Method; public class Test2 { @DataProvider(name = "provider") public Object[][] provide() throws Exception { return new Object[][] { { EatCompentConfig.class.getMethod("hasNext") } }; } @Test(dataProvider = "provider") public void withoutInjection(@NoInjection Method m) { Assert.assertEquals(m.getName(), "hasNext"); } @Test(dataProvider = "provider") public void withInjection(Method m) { Assert.assertEquals(m.getName(), "withInjection"); } }
該註解是爲了關閉依賴注入,爲何?
對於案例中,withoutInjection方法的入參和依賴注入中的默認對象有重疊,且默認狀況下,使用的就是依賴
所對應的對象也就是說Method自己若是在不加@NoInjection的狀況下,那麼它是表明withoutInjection方法自己的,可是咱們代碼的意思,確是但願,傳入一個入參Method而不是依賴注入默認的Method,因此咱們須要該註解@NoInjection來關閉依賴注入,從而Assert斷言成功!
關於測試報告,技術選型不少種,我選用的是比較簡單、好看的external-report插件,配合自定義的監聽器實現
備註:targetsurefire-reportsindex.html,這裏是最原始的測試報告,其餘插件的報告位置能夠本身定義
pom.xml
<!--testng依賴--> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>7.1.0</version> </dependency> <!--測試報告的依賴--> <dependency> <groupId>com.relevantcodes</groupId> <artifactId>extentreports</artifactId> <version>2.41.1</version> </dependency> <dependency> <groupId>com.vimalselvam</groupId> <artifactId>testng-extentsreport</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>com.aventstack</groupId> <artifactId>extentreports</artifactId> <version>3.0.6</version> </dependency>
xml配置
<suite name="test2"> <listeners> <listener class-name="org.vk.test.listeners.report.ExtentTestNGIReporterListener"/> </listeners> <test name="Test2" > <classes> <class name="org.vk.test.demos.Test2"> </class> </classes> </test> </suite>
自定義測試報告的監聽器【照搬便可,不必本身實現】
package org.vk.test.listeners.report; import com.aventstack.extentreports.ExtentReports; import com.aventstack.extentreports.ExtentTest; import com.aventstack.extentreports.ResourceCDN; import com.aventstack.extentreports.Status; import com.aventstack.extentreports.model.TestAttribute; import com.aventstack.extentreports.reporter.ExtentHtmlReporter; import com.aventstack.extentreports.reporter.configuration.ChartLocation; import com.aventstack.extentreports.reporter.configuration.Theme; import org.testng.*; import org.testng.xml.XmlSuite; import java.io.File; import java.util.*; /** * TestNg生成好看的測試UI報告 * * @author liuleiba@ecej.com * @version 1.0 */ public class ExtentTestNGIReporterListener implements IReporter { //美化後的測試報告生成的路徑以及文件名 private static final String OUTPUT_FOLDER = "target/test-report/"; private static final String FILE_NAME = "index.html"; private ExtentReports extent; @Override public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) { init(); boolean createSuiteNode = false; if(suites.size()>1){ createSuiteNode=true; } for (ISuite suite : suites) { Map<String, ISuiteResult> result = suite.getResults(); //若是suite裏面沒有任何用例,直接跳過,不在報告裏生成 if(result.size()==0){ continue; } //統計suite下的成功、失敗、跳過的總用例數 int suiteFailSize=0; int suitePassSize=0; int suiteSkipSize=0; ExtentTest suiteTest=null; //存在多個suite的狀況下,在報告中將同一個一個suite的測試結果歸爲一類,建立一級節點。 if(createSuiteNode){ suiteTest = extent.createTest(suite.getName()).assignCategory(suite.getName()); } boolean createSuiteResultNode = false; if(result.size()>1){ createSuiteResultNode=true; } for (ISuiteResult r : result.values()) { ExtentTest resultNode; ITestContext context = r.getTestContext(); if(createSuiteResultNode){ //沒有建立suite的狀況下,將在SuiteResult的建立爲一級節點,不然建立爲suite的一個子節點。 if( null == suiteTest){ resultNode = extent.createTest(r.getTestContext().getName()); }else{ resultNode = suiteTest.createNode(r.getTestContext().getName()); } }else{ resultNode = suiteTest; } if(resultNode != null){ resultNode.getModel().setName(suite.getName()+" : "+r.getTestContext().getName()); if(resultNode.getModel().hasCategory()){ resultNode.assignCategory(r.getTestContext().getName()); }else{ resultNode.assignCategory(suite.getName(),r.getTestContext().getName()); } resultNode.getModel().setStartTime(r.getTestContext().getStartDate()); resultNode.getModel().setEndTime(r.getTestContext().getEndDate()); //統計SuiteResult下的數據 int passSize = r.getTestContext().getPassedTests().size(); int failSize = r.getTestContext().getFailedTests().size(); int skipSize = r.getTestContext().getSkippedTests().size(); suitePassSize += passSize; suiteFailSize += failSize; suiteSkipSize += skipSize; if(failSize>0){ resultNode.getModel().setStatus(Status.FAIL); } resultNode.getModel().setDescription(String.format("Pass: %s ; Fail: %s ; Skip: %s ;",passSize,failSize,skipSize)); } buildTestNodes(resultNode,context.getFailedTests(), Status.FAIL); buildTestNodes(resultNode,context.getSkippedTests(), Status.SKIP); buildTestNodes(resultNode,context.getPassedTests(), Status.PASS); } if(suiteTest!= null){ suiteTest.getModel().setDescription(String.format("Pass: %s ; Fail: %s ; Skip: %s ;",suitePassSize,suiteFailSize,suiteSkipSize)); if(suiteFailSize>0){ suiteTest.getModel().setStatus(Status.FAIL); } } } for (String s : Reporter.getOutput()) { extent.setTestRunnerOutput(s); } extent.flush(); } private void init() { //文件夾不存在的話進行建立 File reportDir= new File(OUTPUT_FOLDER); if(!reportDir.exists()&& !reportDir .isDirectory()){ reportDir.mkdir(); } ExtentHtmlReporter htmlReporter = new ExtentHtmlReporter(OUTPUT_FOLDER + FILE_NAME); // 設置靜態文件的DNS //怎麼樣解決cdn.rawgit.com訪問不了的狀況 htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS); htmlReporter.config().setDocumentTitle("PC端自動化測試報告"); htmlReporter.config().setReportName("PC端自動化測試報告"); htmlReporter.config().setChartVisibilityOnOpen(true); htmlReporter.config().setTestViewChartLocation(ChartLocation.TOP); htmlReporter.config().setTheme(Theme.STANDARD); htmlReporter.config().setEncoding("gbk"); htmlReporter.config().setCSS(".node.level-1 ul{ display:none;} .node.level-1.active ul{display:block;}"); extent = new ExtentReports(); extent.attachReporter(htmlReporter); extent.setReportUsesManualConfiguration(true); } private void buildTestNodes(ExtentTest extenttest, IResultMap tests, Status status) { //存在父節點時,獲取父節點的標籤 String[] categories=new String[0]; if(extenttest != null ){ List<TestAttribute> categoryList = extenttest.getModel().getCategoryContext().getAll(); categories = new String[categoryList.size()]; for(int index=0;index<categoryList.size();index++){ categories[index] = categoryList.get(index).getName(); } } ExtentTest test; if (tests.size() > 0) { //調整用例排序,按時間排序 Set<ITestResult> treeSet = new TreeSet<ITestResult>(new Comparator<ITestResult>() { @Override public int compare(ITestResult o1, ITestResult o2) { return o1.getStartMillis()<o2.getStartMillis()?-1:1; } }); treeSet.addAll(tests.getAllResults()); for (ITestResult result : treeSet) { Object[] parameters = result.getParameters(); String name=""; //若是有參數,則使用參數的toString組合代替報告中的name for(Object param:parameters){ name+=param.toString(); } if(name.length()>0){ if(name.length()>50){ name= name.substring(0,49)+"..."; } }else{ name = result.getMethod().getMethodName(); } if(extenttest==null){ test = extent.createTest(name); }else{ //做爲子節點進行建立時,設置同父節點的標籤一致,便於報告檢索。 test = extenttest.createNode(name).assignCategory(categories); } //test.getModel().setDescription(description.toString()); //test = extent.createTest(result.getMethod().getMethodName()); for (String group : result.getMethod().getGroups()) test.assignCategory(group); List<String> outputList = Reporter.getOutput(result); for(String output:outputList){ //將用例的log輸出報告中 test.debug(output); } if (result.getThrowable() != null) { test.log(status, result.getThrowable()); } else { test.log(status, "Test " + status.toString().toLowerCase() + "ed"); } test.getModel().setStartTime(getTime(result.getStartMillis())); test.getModel().setEndTime(getTime(result.getEndMillis())); } } } private Date getTime(long millis) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(millis); return calendar.getTime(); } }
結果輸出頁面:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-OkxMK1By-1581562336478)(C:UsersdellAppDataRoamingTyporatypora-user-images1579085189960.png)]
對於單元測試,咱們但願您按照如下幾種規則去設計、處理、編碼:
測試包的根目錄:必須在src/test/java
下[源碼構建時會跳過此目錄,單元測試框架默認是掃描此目錄]
測試包中java
類的包路徑:與實際要測試的類,保持一致[ 參考編寫流程中的截圖]
測試包的java
類名:遵循OrderQueryService.java -> OrderQueryServiceTest.java
規則
測試包的xml
路徑:在實際要測試的類的包下,新建xml
包便可,存放各個測試類型testng.xml
測試包的監聽器:在實際要測試的類的包下,根據監聽器做用範圍,新建listerners
包便可,
mvn test
或者運行對整個類的測試案例時,均可以自動運行完全部測試Assert
斷言,不容許使用System.out.print
,使用日誌log
佔位符輸出測試信息repository
層的測試,不容許使用mock進行測試,而且保證要有數據回滾機制,不形成髒數據[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-LbjdW9PA-1581562336480)(C:Users86151AppDataRoamingTyporatypora-user-images1580782404369.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8hS2NtoM-1581562336481)(C:Users86151AppDataRoamingTyporatypora-user-images1580782525627.png)]
<!--testng依賴--> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>7.1.0</version> </dependency> <!--測試報告的依賴--> <dependency> <groupId>com.relevantcodes</groupId> <artifactId>extentreports</artifactId> <version>2.41.1</version> </dependency> <dependency> <groupId>com.vimalselvam</groupId> <artifactId>testng-extentsreport</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>com.aventstack</groupId> <artifactId>extentreports</artifactId> <version>3.0.6</version> </dependency>
package com.ecej.order.basics.service.impl; import com.alibaba.fastjson.JSON; import com.ecej.order.basics.Startup; import com.ecej.order.basics.api.query.OrderQueryService; import com.ecej.order.basics.bean.dto.CombinedOrderDTO; import com.ecej.order.basics.bean.dto.WorkOrderDetailDTO; import com.ecej.order.basics.bean.request.WorkOrderDetailQueryReqParam; import com.ecej.order.basics.bean.request.WorkOrderListQueryReqParam; import com.ecej.order.common.util.DateUtil; import com.ecej.order.model.baseResult.ResultMessage; import com.ecej.order.listener.ExtentTestNGIReporterListener; import com.ecej.order.util.QueryResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.testng.Assert; import org.testng.ITestContext; import org.testng.annotations.DataProvider; import org.testng.annotations.Listeners; import org.testng.annotations.Test; import java.util.Arrays; import java.util.Collections; import java.util.Date; /** * @ClassName: 订单查询测试 * @Author: Administrator * @Description: zlr * @Date: 2020/1/16 17:54 * @Version: 1.0 */ @SpringBootTest(classes = Startup.class) @Listeners(ExtentTestNGIReporterListener.class) public class OrderQueryServiceImplTest extends AbstractTestNGSpringContextTests { private static final Logger logger = LoggerFactory.getLogger(OrderQueryServiceImplTest.class); @Autowired private OrderQueryService orderQueryService; @Test(dataProvider = "createOrderListQueryData", suiteName = "訂單列表單元測試", groups = "queryWorkOrderList", timeOut = 10000) public void queryWorkOrderListPageTest(int paramType, ITestContext testContext, WorkOrderListQueryReqParam param) { logger.info("參數名稱={};測試第[{}]次開始={}",paramType,testContext.getPassedTests().size()+1); ResultMessage<QueryResult<CombinedOrderDTO>> queryWorkOrderListPage = orderQueryService.queryWorkOrderListPage(param); logger.info(JSON.toJSONString(queryWorkOrderListPage)); switch (paramType) { case 1: Assert.assertEquals(queryWorkOrderListPage.getCode(), 1000); break; case 2: Assert.assertEquals(queryWorkOrderListPage.getCode(), 1000); break; case 3: Assert.assertEquals(queryWorkOrderListPage.getCode(), 1000); break; case 4: Assert.assertEquals(queryWorkOrderListPage.getCode(), 200); break; default: Assert.assertEquals(queryWorkOrderListPage.getCode(), 200); } } @Test(dataProvider = "createOrderDetailData", suiteName = "訂單詳情單元測試", groups = "orderDetailAnnotations", timeOut = 10000) public void queryWorkOrderDetailTest(int paramType, WorkOrderDetailQueryReqParam param) { ResultMessage<WorkOrderDetailDTO> resultMessage = orderQueryService.queryWorkOrderDetail(param); switch (paramType) { case 1: Assert.assertEquals(resultMessage.getCode(), 1000); break; case 2: Assert.assertEquals(resultMessage.getCode(), 1000); break; case 3: Assert.assertEquals(resultMessage.getCode(), 200); break; default: Assert.assertEquals(resultMessage.getCode(), 200); } logger.info(JSON.toJSONString(resultMessage)); } /** * 建立訂單列表查詢參數(此處也可根據查詢數據庫做爲參數對象) * 構建多場景測試案例參數(單元測試根據場景 1,2,3,4 進行斷言) */ @DataProvider(name = "createOrderListQueryData") public Object[][] createOrderListQueryData() { //一、構建空對象 WorkOrderListQueryReqParam checkParam = new WorkOrderListQueryReqParam(); //二、構建殘缺參數(requestSource) WorkOrderListQueryReqParam paramRequestSource = new WorkOrderListQueryReqParam(); paramRequestSource.setCityId(2237); paramRequestSource.setCityIdList(Arrays.asList(2237, 2057, 2367)); paramRequestSource.setBookStartTimeBegin(DateUtil.getDate(new Date(), -4)); paramRequestSource.setWorkOrderStatusList(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 150)); paramRequestSource.setBookStartTimeEnd(DateUtil.getDate(new Date(), 4)); paramRequestSource.setStationIdList(Collections.singletonList(35200342)); paramRequestSource.setPageNum(1); paramRequestSource.setPageSize(10); paramRequestSource.setOrderDispatchingModeList(Arrays.asList(1, 2, 3, 4, 5)); //三、構建訂單來源是 99必填參數校驗(缺乏預定時間查詢) WorkOrderListQueryReqParam paramBookStartTime = new WorkOrderListQueryReqParam(); paramBookStartTime.setRequestSource(99); paramBookStartTime.setCityId(2237); paramBookStartTime.setCityIdList(Arrays.asList(2237, 2057, 2367)); paramBookStartTime.setWorkOrderStatusList(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 150)); paramBookStartTime.setStationIdList(Collections.singletonList(35200342)); paramBookStartTime.setPageNum(1); paramBookStartTime.setPageSize(10); paramBookStartTime.setOrderDispatchingModeList(Arrays.asList(1, 2, 3, 4, 5)); //四、完整參數 WorkOrderListQueryReqParam param = new WorkOrderListQueryReqParam(); param.setRequestSource(99); param.setCityId(2237); param.setCityIdList(Arrays.asList(2237, 2057, 2367)); param.setBookStartTimeBegin(DateUtil.getDate(new Date(), -4)); param.setWorkOrderStatusList(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 150)); param.setBookStartTimeEnd(DateUtil.getDate(new Date(), 4)); param.setStationIdList(Collections.singletonList(35200342)); param.setPageNum(1); param.setPageSize(10); param.setOrderDispatchingModeList(Arrays.asList(1, 2, 3, 4, 5)); // param.setWorkOrderNo("4542"); return new Object[][]{ { 1, checkParam }, { 2, paramRequestSource }, { 3, paramBookStartTime }, { 4, param }, }; } /** * 構建多個測試參數:建立訂單接口 */ @DataProvider(name = "createOrderDetailData") public Object[][] createOrderDetailData() { //一、構建空對象 WorkOrderDetailQueryReqParam checkParam = new WorkOrderDetailQueryReqParam(); //二、構建殘缺參數(WorkOrderNo) WorkOrderDetailQueryReqParam checkWorkOrderNoParam = new WorkOrderDetailQueryReqParam(); checkWorkOrderNoParam.setRequestSource(99); //四、完整參數 WorkOrderDetailQueryReqParam param = new WorkOrderDetailQueryReqParam(); param.setRequestSource(99); param.setWorkOrderNo("A201801191022356151"); return new Object[][]{ { 1, checkParam }, { 2, checkWorkOrderNoParam }, { 3, param }, }; }
package com.ecej.order.basics.service.impl; import com.alibaba.fastjson.JSON; import com.ecej.order.basics.Startup; import com.ecej.order.basics.api.query.OrderQueryService; import com.ecej.order.basics.bean.dto.WorkOrderDetailDTO; import com.ecej.order.basics.bean.request.WorkOrderDetailQueryReqParam; import com.ecej.order.model.baseResult.ResultMessage; import com.ecej.order.listener.ExtentTestNGIReporterListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.testng.Assert; import org.testng.annotations.Listeners; import org.testng.annotations.Parameters; import org.testng.annotations.Test; /** * @ClassName: OrderTGXml * @Author: Administrator * @Description: zlr * @Date: 2020/1/16 13:51 * @Version: 1.0 */ @SpringBootTest(classes = { Startup.class }) @Listeners(ExtentTestNGIReporterListener.class) public class OrderQueryServiceImplXmlTest extends AbstractTestNGSpringContextTests { private static final Logger logger = LoggerFactory.getLogger(OrderQueryServiceImplXmlTest.class); @Autowired private OrderQueryService orderQueryService; @Test(groups = "queryWorkOrderDetail") @Parameters({"requestSource","workOrderNo"}) public void queryWorkOrderDetailTest(Integer requestSource,String workOrderNo){ WorkOrderDetailQueryReqParam param = new WorkOrderDetailQueryReqParam(); param.setRequestSource(requestSource); param.setWorkOrderNo(workOrderNo); ResultMessage<WorkOrderDetailDTO> resultMessage = orderQueryService.queryWorkOrderDetail(param); Assert.assertEquals(resultMessage.getCode(), 200); logger.info(JSON.toJSONString(resultMessage)); } }
基本上,和日常寫代碼區別不大,只需額外維護TG的xml和它本身的一些註解【架構的案例已經知足】
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="訂單基礎服務單元測試報告" parallel="classes" thread-count="1"> <listeners><listener class-name="com.ecej.order.listener.ExtentTestNGIReporterListener"/></listeners> <test verbose="1" preserve-order="true" name="訂單查詢"> <parameter name="requestSource" value="99" /> <parameter name="workOrderNo" value="A201801191022356151"/> <groups> <define name="queryWorkOrderListPageTest"> <!--能夠是多個,也能夠分開寫--> <include name="queryWorkOrderList"/> <!--<include name="queryWorkOrderDetail"/>--> </define> <define name="queryWorkOrderDetailTest"> <include name="queryWorkOrderDetail"/> </define> <run> <include name="queryWorkOrderListPageTest"/> <include name="queryWorkOrderDetailTest"/> </run> </groups> <classes> <!-- 測試類能夠多個 --> <class name="com.ecej.order.basics.service.impl.OrderQueryServiceImplTest" /> <class name="com.ecej.order.basics.service.impl.OrderQueryServiceImplXmlTest" /> </classes> </test> </suite>
xml是在測試類的基礎上二次編排,最終的測試效果是以xml爲準
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-K2hhy4e4-1581562336483)(C:Users86151AppDataRoamingTyporatypora-user-images1580782613491.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fZjOfE51-1581562336483)(C:Users86151AppDataRoamingTyporatypora-user-images1580782642269.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5THIiuIq-1581562336485)(C:Users86151AppDataRoamingTyporatypora-user-images1580782705068.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-k6C9V2B2-1581562336485)(C:Users86151AppDataRoamingTyporatypora-user-images1580782761162.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-h5QUh17b-1581562336485)(C:Users86151AppDataRoamingTyporatypora-user-images1580782774727.png)]
Debug和正常程序同樣,可使用debug模式啓動測試案例,對其中的某些入參、返回作斷點,查看參數、返回
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cR7dgk8C-1581562336486)(C:Users86151AppDataRoamingTyporatypora-user-images1580782877064.png)]
本案例中執行mvn test
的運行
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-BQjPjJpq-1581562336487)(C:Users86151AppDataRoamingTyporatypora-user-images1580783218995.png)]
若是出現結果錯誤時,須要對Failed tests
中的錯誤測試,進行修復,直至Build Success
爲止
某些狀況下,控制檯能夠看到測試結果,可是對於項目發佈,咱們最好仍是從測試結果報告中查看統計信息
咱們能夠從兩種類型的報告中獲取測試案例的運行狀況,哪些成功、失敗,從而對它們進行修復。
具體查看哪種報告,看我的習慣。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zzZpRQHQ-1581562336487)(C:Users86151AppDataRoamingTyporatypora-user-images1580783311156.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oQJqiPaa-1581562336487)(C:Users86151AppDataRoamingTyporatypora-user-images1580783343860.png)]
對於某些方法,對數據完整性依賴性較高,且手動構建數據複雜,測試場景須要全面的時候,須要經過測試案例來模擬完整的:入庫-查詢-修改-刪除場景時,那麼咱們在運行某些query的測試案例前,須要插入一些數據來支撐其餘測試運行,在運行完測試以後,咱們又須要擦除利用完了的數據至此,咱們須要有相應的手段和case來覆蓋這些場景
那麼在編寫測試案例中,對於service、dao層的curd操做,所產生的髒數據問題,咱們須要從兩個角度去考慮:
對於本項目中service、dao數據的回滾,能夠從如下3個方面考慮:
有兩個類須要說明一下:
1.AbstractTestNGSpringContextTests
測試類只有繼承了該類才能擁有注入實例Bean的能力,不然注入報錯
總結:【適合處理查詢的測試案例】
2.AbstractTransactionalTestNGSpringContextTests
測試類繼承該類後擁有注入實例能力,同時擁有事物控制能力
總結:【適應於任何場景,推薦使用】
因此,處理本地項目中的service
和dao
,對於數據庫產生的數據,咱們只須要將測試類繼承AbstractTransactionalTestNGSpringContextTests
便可,測試案例中全部對數據庫的操做將都只停留在測試階段,一旦測試案例運行完成,TestNG
會自動幫助咱們回滾數據,沒有任何的代碼侵入。
示例:
/** * 演示Test Curd操做【遠程服務事物回滾:編程式回滾】 * * @author liulei, liuleiba@ecej.com * @version 1.0 */ @SpringBootTest(classes = Startup.class) @Listeners(ExtentTestNGIReporterListener.class) public class TestNgCurdServiceImplTest extends AbstractTestNGSpringContextTests { private static final Logger logger = LoggerFactory.getLogger(TestNgCurdServiceImplTest.class); @Autowired TestNgCurdService testNgCurdService; @DataProvider private Object[][] saveOrUpdateParam() { SysMenuParam po = new SysMenuParam(); po.setLevels(1); po.setMenuSort(1); po.setMenuName("測試菜單"); po.setMenuUrl("menu—url"); po.setPmenuId("1"); return new Object[][]{{po}}; } /** * 1.測試保存 */ @Rollback @Test(groups = "saveOrUpdate", dataProvider = "saveOrUpdateParam") public void testSaveOrUpdate(SysMenuParam po) { logger.info("測試保存開始:{}", po); ResultMessage<Boolean> result = testNgCurdService.saveOrUpdate(po); Assert.assertEquals(result.getCode(), 200); logger.info("測試保存結束", result); } //... }
測試類繼承AbstractTestNGSpringContextTests
類,搭配使用@Rollback
註解,對部分測試方法進行事物回滾,避免測試案例過程當中的測試數據最後成爲了髒數據,運行完測試案例後,TestNG
會自動幫助咱們回滾對應註解了@Rollback方法所產生的數據,其餘沒有加註解的方法則會真實做用於數據庫層面,慎用。
示例:
public class TestNgCurdServiceImplTest extends AbstractTestNGSpringContextTests { //.... //寫法保持不變,不須要改動任何代碼,只須要在須要回滾的方法上加上註解@Rollback註解便可 //.... }
手動編程,將測試案例運行先後,產生的全部數據,手動調用相關的刪除delete接口逐一擦除【適合遠程服務】
下面章節【遠程服務數據回滾】有相關的案例和說明!
若是一個測試案例以非Mock方式運行,而且有對遠程服務進行調用,產生了髒數據,那麼此時只能經過編程式回滾數據,即在執行完測試案例的先後,調用相關delete方法,進行清除,若是測試案例上下文較爲複雜,對數據的回收分析就變得比較重要,而且服務間鏈式調用過長,一旦測試案例產生了錯誤,那麼會產生不可預知的一些問題,須要謹慎使用。
本例中,咱們也有相關的case覆蓋:
核心流程:
測試遠程服務的增刪改查,則必然須要在查詢方法前,咱們須要插入準備數據,才能測試查詢接口,咱們能夠經過TestNg
提供的執行機制,在運行完測試案例以後調用相關的delete方法,清除運行期間臨時準備的測試數據,不然會污染數據庫。
有興趣的小夥伴能夠根據如下代碼自行測試一下
/** * TestNg Curd測試案例【無實際用途】 * @author liulei, lei.liu@htouhui.com * @version 1.0 */ public interface TestNgCurdService { ResultMessage<Boolean> saveOrUpdate(SysMenuParam sysMenuParam); ResultMessage<Boolean> delete(SysMenuParam sysMenuParam); ResultMessage<List<SysMenuDTO>> queryList(SysMenuReqParam sysMenuReqParam); }
package com.ecej.order.basics.service.impl; import com.ecej.order.basics.Startup; import com.ecej.order.basics.api.query.TestNgCurdService; import com.ecej.order.listener.ExtentTestNGIReporterListener; import com.ecej.order.model.baseResult.ResultMessage; import com.ecej.order.strategy.bean.dto.SysMenuDTO; import com.ecej.order.strategy.bean.request.SysMenuParam; import com.ecej.order.strategy.bean.request.SysMenuReqParam; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.testng.Assert; import org.testng.annotations.*; import java.util.List; /** * 演示Test Curd操做【遠程服務事物回滾:編程式回滾】 * * @author liulei, liuleiba@ecej.com * @version 1.0 */ @SpringBootTest(classes = Startup.class) @Listeners(ExtentTestNGIReporterListener.class) public class TestNgCurdServiceImplTest extends AbstractTestNGSpringContextTests { private static final Logger logger = LoggerFactory.getLogger(TestNgCurdServiceImplTest.class); @Autowired TestNgCurdService testNgCurdService; @DataProvider private Object[][] saveOrUpdateParam() { SysMenuParam po = new SysMenuParam(); po.setLevels(1); po.setMenuSort(1); po.setMenuName("測試菜單"); po.setMenuUrl("menu—url"); po.setPmenuId("1"); return new Object[][]{{po}}; } /** * 1.測試保存 */ @Test(groups = "saveOrUpdate", dataProvider = "saveOrUpdateParam") public void testSaveOrUpdate(SysMenuParam po) { logger.info("測試保存開始:{}", po); ResultMessage<Boolean> result = testNgCurdService.saveOrUpdate(po); Assert.assertEquals(result.getCode(), 200); logger.info("測試保存結束:{}", result); } @DataProvider private Object[][] queryListParam() { SysMenuReqParam po = new SysMenuReqParam(); po.setMenuName("測試菜單"); return new Object[][]{{po}}; } /** * 2.測試查詢【xml文件中別忘記使用allow-return-values="true" 註解來強制返回測試案例結果,以便手動清除數據】 */ @Test(groups = "queryList", dataProvider = "queryListParam", dependsOnGroups = "saveOrUpdate") public List<SysMenuDTO> testQueryList(SysMenuReqParam po) { logger.info("測試查詢開始:{}", po); ResultMessage<List<SysMenuDTO>> result = testNgCurdService.queryList(po); Assert.assertEquals(result.getCode(), 200); logger.info("測試查詢結束:{}", result); return result.getData(); } /** * 3.測試根據menuId刪除方法【依賴於插入方法】 */ public void testDelete(SysMenuParam po) { logger.info("測試刪除開始:{}", po); ResultMessage<Boolean> result = testNgCurdService.delete(po); //預期刪除成功,但目前的遠程接口,插入、查詢後返回菜單主鍵,因此此處會失敗【注意】 Assert.assertEquals(result.getCode(), 303); logger.info("測試刪除結束:{}", result); } /** * 因爲是遠程服務,沒法經過test-ng的回滾機制來回顧測試數據 * 因此,對於遠程服務測試案例的curd,必需要經過手動清除測試數據,來保證數據的純潔度 */ @AfterSuite public void clearData() { //此處因爲插入方法沒有返回主鍵,咱們須要將新插入的主鍵查詢出來後進行刪除 SysMenuReqParam po = new SysMenuReqParam(); po.setMenuName("測試菜單"); SysMenuParam sysMenuParam = new SysMenuParam(); List<SysMenuDTO> sysMenuDTO = testQueryList(po); for (SysMenuDTO dto : sysMenuDTO) { SysMenuParam param = new SysMenuParam(); //此處因爲遠程接口沒有返回主鍵ID,因此運行刪除此處會報錯,可是目前也沒法更改遠程接口,因此你們注意便可 BeanUtils.copyProperties(dto, param); testDelete(sysMenuParam); } } }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="TestNg測試Curd套件" allow-return-values="true" parallel="classes" thread-count="1"> <listeners><listener class-name="com.ecej.order.listener.ExtentTestNGIReporterListener"/></listeners> <test verbose="1" preserve-order="true" name="訂單查詢"> <parameter name="requestSource" value="99" /> <parameter name="workOrderNo" value="A201801191022356151"/> <groups> <define name="saveOrUpdate"/> <define name="queryList"/> <dependencies> <group name="queryList" depends-on="saveOrUpdate"/> </dependencies> </groups> <classes> <!-- 測試類能夠多個 --> <class name="com.ecej.order.basics.service.impl.TestNgCurdServiceImplTest" /> </classes> </test> </suite>
至此,事物的回滾已經完成,可是也存在一些問題,就是這一整個鏈路任何一個環節要是出問題,都會產生髒數據,好比新增完以後,delete方法報錯,那麼插入數據庫中的數據就會保留,須要手動去清除庫,調用鏈路若是比較長,涉及面較廣的時候,存在不肯定性!
章節【1】中是處理測試案例數據的一種方式,是以測試方法爲維度進行處理另一種方式,則是利用
TestNG
的監聽器,來作數據埋點,預先在數據庫中初始化將要使用到的數據,咱們以套件爲單位來作埋點,範圍過大則不推薦,一旦部分程序出現問題,容易致使數據混亂,很差處理。
該場景,咱們仍然有相關的case覆蓋:
執行測試套件suit
以前,進行數據埋點,整個套件測試案例執行完以後,進行數據銷燬
以套件爲單位,以sql
腳本爲介質進行處理
有興趣的小夥伴能夠根據如下代碼自行測試一下:
package com.ecej.order.listener; import com.ecej.order.util.TestDataHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.ISuite; import org.testng.ISuiteListener; /** * 測試數據埋點處理【須要埋點的測試類,直接使用此監聽器便可】 * * @author liulei, liuleiba@ecej.com * @version 1.0 */ public class TestCaseDataPrepareListener implements ISuiteListener { private static final Logger logger = LoggerFactory.getLogger(TestCaseDataPrepareListener.class); /** * 埋點數據sql初始化腳本 */ private static String INIT_FILE = "order_init.sql"; /** * 埋點數據sql銷燬腳本 */ private static String DESTROY_FILE = "order_destroy.sql"; private static TestDataHandler TestDataHandler = new TestDataHandler(); /** * 測試套件執行前 * * @param suite 套件 */ @Override public void onStart(ISuite suite) { logger.info("測試套件開始初始化測試數據"); TestDataHandler.testDataOperate(INIT_FILE, false); logger.info("測試套件完成初始化測試數據"); } /** * 測試套件執行後 * * @param suite 套件 */ @Override public void onFinish(ISuite suite) { logger.info("測試套件開始初始化銷燬數據"); TestDataHandler.testDataOperate(DESTROY_FILE, true); logger.info("測試套件完成初始化銷燬數據"); } }
package com.ecej.order.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import java.io.*; import java.sql.*; import java.util.ArrayList; /** * TODO 優化存儲成ThreadLocalMap,初始化一次數據源便可後續複用數據源便可 * * @author liulei, lei.liu@htouhui.com * @version 1.0 */ public class TestDataHandler { private static final Logger logger = LoggerFactory.getLogger(TestDataHandler.class); private static String DB_DRIVER = "com.mysql.jdbc.Driver"; private static String DB_URL = "jdbc:mysql://10.4.98.14:3306/ecejservice?useunicode=true&characterencoding=utf-8&zeroDateTimeBehavior=convertToNull"; private static String DB_USER = "dev_user"; private static String DB_PWD = "123qweasd"; /** * 運行環境 * TODO 待改爲動態 */ private static String PROFILE = "dev"; private static Connection connection; static { try { //加載mysql的驅動類 Class.forName(DB_DRIVER); } catch (Exception e) { e.printStackTrace(); } } /** * 構造函數,包括鏈接數據庫等操做 */ public TestDataHandler() { try { //加載mysql的驅動類 Class.forName(DB_DRIVER); //獲取數據庫鏈接 connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PWD); } catch (Exception e) { e.printStackTrace(); connection = null; } } /** * 自定義數據庫鏈接 * * @param dbUrl 數據庫鏈接 * @param User 用戶 * @param Password 密碼 */ public TestDataHandler(String dbUrl, String User, String Password) { try { //獲取數據庫鏈接 connection = DriverManager.getConnection(dbUrl, User, Password); } catch (Exception e) { e.printStackTrace(); connection = null; } } /** * 獲取鏈接 * * @return 鏈接conn */ public Connection getConnection() { return connection; } /** * 釋放數據庫鏈接 */ public static void ReleaseConnect() { if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } /** * 批量執行SQL語句 * * @param sql 包含待執行的SQL語句的ArrayList集合 * @param ifClose 是否關閉數據庫鏈接 * @return int 影響的函數 */ public int executeSqlFile(ArrayList<String> sql, boolean ifClose) { try { Statement st = connection.createStatement(); for (String subsql : sql) { st.addBatch(subsql); } st.executeBatch(); return 1; } catch (Exception e) { e.printStackTrace(); return 0; } finally { if (ifClose) { ReleaseConnect(); } } } /** * 以行爲單位讀取文件,並將文件的每一行格式化到ArrayList中,經常使用於讀面向行的格式化文件 * * @param filePath 文件路徑 */ private static ArrayList<String> readFileByLines(String filePath) throws Exception { ArrayList<String> listStr = new ArrayList<>(); StringBuffer sb = new StringBuffer(); BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF-8")); String tempString; int flag = 0; // 一次讀入一行,直到讀入null爲文件結束 while ((tempString = reader.readLine()) != null) { // 顯示行號,過濾空行 if (tempString.trim().equals("")) continue; if (tempString.substring(tempString.length() - 1).equals(";")) { if (flag == 1) { sb.append(tempString); listStr.add(sb.toString()); sb.delete(0, sb.length()); flag = 0; } else listStr.add(tempString); } else { flag = 1; sb.append(tempString); } } reader.close(); } catch (IOException e) { e.printStackTrace(); throw e; } finally { if (reader != null) { try { reader.close(); } catch (IOException e1) { } } } return listStr; } /** * 讀取文件內容到SQL中執行 * * @param file SQL文件的路徑 * @param ifClose 是否關閉數據庫鏈接 */ public void testDataOperate(String file, boolean ifClose) { try { Resource resource = new ClassPathResource("sql" + File.separator + PROFILE + File.separator + file); ArrayList<String> sqlStr = readFileByLines(resource.getFile().getAbsolutePath()); if (sqlStr.size() > 0) { int num = executeSqlFile(sqlStr, ifClose); if (num > 0) logger.info("sql[{}]執行成功", sqlStr); else logger.error("有未執行的SQL語句", sqlStr); } else { logger.info("sql執行結束"); } } catch (Exception e) { e.printStackTrace(); } } }
咱們根據不一樣的運行環境,設置不一樣的sql腳本,以避免數據混亂,核心屬性
Profile
【開發、測試環境】
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Goszc2CT-1581562336489)(C:Users86151AppDataRoamingTyporatypora-user-images1580965050323.png)]
package com.ecej.order.basics.service.impl; import com.alibaba.fastjson.JSON; import com.ecej.order.basics.Startup; import com.ecej.order.basics.api.query.OrderQueryService; import com.ecej.order.basics.bean.dto.WorkOrderDetailDTO; import com.ecej.order.basics.bean.request.WorkOrderDetailQueryReqParam; import com.ecej.order.listener.ExtentTestNGIReporterListener; import com.ecej.order.listener.TestCaseDataPrepareListener; import com.ecej.order.model.baseResult.ResultMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.testng.Assert; import org.testng.annotations.Listeners; import org.testng.annotations.Optional; import org.testng.annotations.Parameters; import org.testng.annotations.Test; /** * 訂單埋點測試,預先插入數據,運行完測試用例後自動刪除埋點數據 * <p> * 埋點監聽器TestCaseDataPrepareListener * * @author liulei, liuleiba@ecej.com * @version 1.0 */ @SpringBootTest(classes = Startup.class) @Listeners({ExtentTestNGIReporterListener.class, TestCaseDataPrepareListener.class}) public class TestNgDataPrepareTest extends AbstractTestNGSpringContextTests { private static final Logger logger = LoggerFactory.getLogger(OrderQueryServiceImplXmlTest.class); @Autowired private OrderQueryService orderQueryService; @Parameters({"requestSource", "workOrderNo"}) @Test(groups = "queryWorkOrderDetailTest") public void queryWorkOrderDetailTest(@Optional("99") Integer requestSource, @Optional("A201801191022356151") String workOrderNo) { WorkOrderDetailQueryReqParam param = new WorkOrderDetailQueryReqParam(); param.setRequestSource(requestSource); param.setWorkOrderNo(workOrderNo); ResultMessage<WorkOrderDetailDTO> resultMessage = orderQueryService.queryWorkOrderDetail(param); Assert.assertEquals(resultMessage.getCode(), 200); //斷言埋點數據的值和sql匹配 Assert.assertEquals(resultMessage.getData().getOrderServiceInfo().getWorkOrderId().intValue(), 2000000); Assert.assertEquals(resultMessage.getData().getOrderServiceInfo().getWorkOrderNo(), "A201801191022356151"); logger.info("訂單詳細工做信息:{}", JSON.toJSONString(resultMessage)); } }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="訂單基礎服務單元測試報告2" parallel="classes" thread-count="1"> <listeners> <listener class-name="com.ecej.order.listener.ExtentTestNGIReporterListener"/> <listener class-name="com.ecej.order.listener.TestCaseDataPrepareListener"/> </listeners> <test verbose="1" preserve-order="true" name="訂單查詢"> <groups> <run> <include name="queryWorkOrderDetailTest"/> </run> </groups> <classes> <!-- 測試類能夠多個 --> <class name="com.ecej.order.basics.service.impl.TestNgDataPrepareTest" /> </classes> </test> </suite>
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-dNQfpEQI-1581562336489)(C:Users86151AppDataRoamingTyporatypora-user-images1580965229125.png)]
1.對於本地服務,很顯然【自動回滾】的方式最佳,無代碼侵入,安全、乾淨,實現簡便。
2.對於遠程服務,顯然是Mock的方式處理測試案例效果更好,mock測試自己就是一種假定,不會對數據庫的數據產生實際影響,設計者只須要關心測試案例的核心業務,而不須要操心因環境、數據所帶來的額外問題。
在某些狀況下,對於某些深度依賴的bean或者是遠程服務bean,並且這些bean或者服務基本能夠確保沒有問題,只是本地環境有限,沒法產生實際的調用時,就可使用mockito對這些bean進行mock,這樣不會產生實際的調用,可是又可以在測試案例中完整的模擬出調用的功能時。使用傳統的測試案例任何一點出差錯,都會致使運行結果失敗。
mockito與章節8中的testng測試案例無任何區別,只是在編寫java測試類這一環節有區別
1.註解Mockito監聽器MockitoTestExecutionListener
2.引入測試類的實現bean,以及它所直接依賴的bean
3.將直接依賴的bean進行Mock,即@MockBean
4.編寫測試案例,對測試方法內部的實現進行mock級別的預言和對結果的斷言
package com.ecej.order.basics.service.impl; import com.alibaba.fastjson.JSON; import com.ecej.model.po.SvcOrderDailyStatisticsPo; import com.ecej.order.base.dao.order.OrderDailyStatisticsDao; import com.ecej.order.basics.api.query.OrderQueryService; import com.ecej.order.basics.bean.dto.SvcOrderDailyStatisticsDTO; import com.ecej.order.basics.bean.request.OrderDailyStatisticsReqParam; import com.ecej.order.basics.manager.OrderQueryManager; import com.ecej.order.common.enums.MessageEnum; import com.ecej.order.common.util.DateUtil; import com.ecej.order.model.baseResult.ResultMessage; import com.ecej.order.test.listener.ExtentTestNGIReporterListener; import com.ecej.order.test.testng.OrderStatisticsMockitoTest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.testng.Assert; import org.testng.annotations.*; import java.util.Arrays; import java.util.Date; import java.util.List; import static org.mockito.Mockito.*; /** * 訂單查詢:TestNg + Mock簡單測試 * * @author liuleiba@ecej.com * @date 2020年1月16日 下午5:37:09 */ @TestExecutionListeners(listeners = MockitoTestExecutionListener.class) @Listeners(ExtentTestNGIReporterListener.class) @ContextConfiguration(classes = {OrderQueryServiceImpl.class, OrderDailyStatisticsDao.class, OrderQueryManager.class}) public class OrderQueryServiceImplMockitoTest extends AbstractTestNGSpringContextTests { private static final Logger logger = LoggerFactory.getLogger(OrderStatisticsMockitoTest.class); @Autowired private OrderQueryService orderQueryService; @MockBean private OrderDailyStatisticsDao orderDailyStatisticsDao; @MockBean private OrderQueryManager orderQueryManager; /** * 構建多個測試參數,儘量覆蓋全部可能出現的場景 */ @DataProvider private Object[][] mockParam() { //1.構建空對象 OrderDailyStatisticsReqParam param1 = new OrderDailyStatisticsReqParam(); //2.構建殘缺參數1 OrderDailyStatisticsReqParam param2 = new OrderDailyStatisticsReqParam(); param2.setQueryTime(DateUtil.getDate(new Date(), -2)); //3.構建殘缺參數2 OrderDailyStatisticsReqParam param3 = new OrderDailyStatisticsReqParam(); param3.setStationId(35200372); //4.構建完整參數 OrderDailyStatisticsReqParam param4 = new OrderDailyStatisticsReqParam(); param4.setQueryTime(DateUtil.getDate(new Date(), -2)); param4.setStationId(35200372); return new Object[][]{{1, param1}, {2, param2}, {3, param3}, {4, param4}}; } /** * 訂單查詢測試案例 * * @param index 參數索引值 * @param param 實際測試的入參 */ @Test(groups = "orderSearchManager", dataProvider = "mockParam", alwaysRun = true) public void orderSearchManageServiceMockTest(int index, OrderDailyStatisticsReqParam param) { logger.info("測試第[{}]次開始:訂單日報統計查詢入參:{}", index, JSON.toJSONString(param)); //1.實際調用對應test的方法 ResultMessage<SvcOrderDailyStatisticsDTO> result = orderQueryService.queryOrderDailyStatistics(param); logger.info("測試第[{}]次結束:訂單日報統計查詢結果:{}", index, result.getMessage()); if (index < 4) { //對多個測試實例的錯誤測試的結果,進行斷言 Assert.assertEquals(result.getCode(), 1000); return; } //2.對測試方法運行過程當中,對可能存在的dao調用進行mock模擬,並預言返回值 ResultMessage<List<SvcOrderDailyStatisticsPo>> daoResult = new ResultMessage(MessageEnum.SUCCESS.getValue(), MessageEnum.SUCCESS.getDesc(), Arrays.asList()); when(orderDailyStatisticsDao.queryOrderDailyStatistics(any())).thenReturn(daoResult); //3.對測試service方法產生的dao調用次數進行斷言 verify(orderDailyStatisticsDao, times(1)).queryOrderDailyStatistics(any()); //4.對返回結果作斷言 Assert.assertEquals(result.getCode(), 200); } }
【官網】 https://testng.org/doc/docume...