技術部忽然宣佈:JAVA開發人員所有要會自動化測試框架

寫在前邊

用單元測試Junit徹底能夠知足平常開發自測,爲何還要學習TestNG,都影響了個人開發進度!html

最近技術部老大忽然宣佈:全體開發人員必須熟練掌握自動化測試框架TestNG,就有了上邊同事們的抱怨,是的,開始我也在抱怨,由於並不知道它是個什麼東東,但從開始接觸到慢慢編寫測試用例,應用到項目後,我發現它真的超實用。java

咱們來一塊兒看看它比Junit好在哪?node


1、TestNG初識

TestNG[後面都簡稱爲TG]是一款爲了大量測試(好比測試時多接口數據依賴)須要,所誕生的一款測試框架,從簡單的單元測試再到集成測試甚至是框架級別的測試,均可以覆蓋到,所以是一款很是強大的測試框架!mysql

1.編寫步驟

常規的TG的測試案例有三個步驟linux

  1. 編寫業務測試代碼時,插入TG提供的註解
  2. 將要測試的類、group等進行編排,寫入到testng.xml或build.xml
  3. 運行單個TG測試案例【idea、eclipse、ant、cmd等均可以運行】
  4. 運行整個項目的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

2.相關注解

註解 做用
@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

3.名詞解釋

suite

​ 由xml文件表示,包含一個或者多個測試案例,使用<suite>標籤包裹

​ 通常來說,一個xml的<suite>對應一個java類,除非特殊狀況,在java中須要特別指定<suite>

​ 不然xml對應java類的全部@Test註解屬性suiteName默認都是xml中定義的<suite name='xxx'>

test

​ 由<test>標籤表示,包含一個或者多個TestNG的類

1.Check-in tests 登記類測試

這些測試類須要在提交新代碼前運行,保證基本功能不會被破壞

2.Functional tests 功能類測試

這些測試應該覆蓋軟件的全部功能,而且天天至少運行一次,即便有些狀況下你不想運行它

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編排中,必定會運行的測試案例

擴展現例
1.正則匹配
@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的命名所引誘】

2.正則排除

【官方原話,不建議使用此類寫法】

若是您開始重構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

test class

​ java類,包含至少一個TG的註解,由<class>表示

test method

​ java方法,含有@Test註解,默認狀況下,test方法的返回值都會忽略,除非聲明須要返回

<suite allow-return-values="true">
<!--或者-->
<test allow-return-values="true">

test group

​ 測試方法組,不只能夠定義方法屬於哪一個group,還能夠設置group包含哪些子group,TG會自動調用

​ 能夠在testng.xml的<test>or<suite>中定義

​ 若是在<suite>中指定組「a」,在<test>中指定組「b」,則「a」和「b」都將包括在內

示例1:基礎

@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」屬性單獨禁用測試。

示例2:組合

@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組

groups of groups

​ 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

​ testng.xml的每一個section部分,均可以在ant、命令行對應的文檔中找到【另外2種調用TG的方式】

我的理解:

通常來說,直接在idea、eclipse中運行@Test類也能夠,但爲何咱們須要testng.xml?

緣由:若是須要對java類、方法的測試案例進行編排,不使用xml進行編排,僅僅提供xml文件之外的方式,很難作到高度靈活的業務邏輯測試!

parameters

測試案例的參數,測試方法的入參配置,執行方法時用到

測試案例的參數

1.@Parameters
1.1 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>
  1. XML參數映射到Java參數的順序與註釋中的順序相同,若是數量不匹配,TestNG將發出一個錯誤。
  2. 參數的做用域:

    在testng.xml中,能夠在<suite>標記下或<test>下聲明它們。

    若是兩個參數具備相同的名稱,則在<test>中定義的參數具備優先權。若是您須要指定一個適用於全部測試的參數,並僅對某些測試重寫其值,則這很方便。

1.2 @Optional注入
@Parameters("hello")
@Test(groups = {"functest"})
public void testMethod4(@Optional("hello") String hello) {
    System.out.println(hello);
}

【官方說明】

@Parameters註解,一樣適用於 @Before/After and @Factory此類的註解!結果:輸出hello字符串

總結:以上的方式,適合簡單參數配置,不適合作複雜對象注入

2.@DataProvider

這種方式是爲了彌補第一種方式而衍生的,若是參數構建比較複雜,複雜對象沒法在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可動態地建立測試,通常用來建立一個測試類的多個實例,每一個實例中的全部測試用例都會被執行,@Factory構造實例的方法必須返回Object[]。

下一個小節的dependencyes,就有對其應用,此處不作過多的說明了就,官網示例和其相差不大。

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得到軟依賴性。

group-by-instances

測試案例按照實例進行分組運行

一般的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

verbose="2" 標識的就是記錄的日誌級別,共有0-10的級別,其中0表示無,10表示最詳細

preserve-order

默認就是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>

4.併發單元測試

若是您運行多個套件文件(例如「java org.testng.testng testng1.xml testng2.xml」),而且但願這些套件在單獨的線程中運行,那麼這很是有用。可使用如下命令行標誌指定線程池的大小:

1.cmd命令

java org.testng.TestNG -suitethreadpoolsize 3 testng1.xml testng2.xml testng3.xml

2.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將在同一線程中運行同一實例中的全部方法,但兩個不一樣實例上的兩個方法將在不一樣線程中運行。-->

3.註解配置

從三個不一樣的線程調用函數testServer十次。10秒的超時保證沒有一個線程會永遠阻塞這個線程。

@Test(threadPoolSize = 3, invocationCount = 10, timeOut = 10000)//timeOut無論是否多線程都有效
public void testServer() {
    ...
    ...
}

5.失敗的測試

如何找到

每次在套件中測試失敗時,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的重試步驟以下:

  1. 構建一個java類,實現org.testng.IRetryAnalyzer接口
  2. 將第一步構建的類綁定到@Test註釋上,例如@Test(retryAnalyzer=LocalRetry.class)
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();
  }
}

6.執行方式

1.命令行

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」,不然將忽略此選項。

2.ant

https://testng.org/doc/ant.html

3.eclipse

https://testng.org/doc/eclips...

4.idea

https://testng.org/doc/idea.html

  • Package: 指定一個package運行.包下的測試案例都會運行.
  • Group: 指定一個TestNG group運行.
  • Suite: 指定一個testng.xml 文件運行
  • Class: 運行對應類中的全部測試案例.
  • Method: 運行單個方法的測試案例.

7.擴展模塊

編程式測試

本例建立一個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...

BeanShell

若是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,這將容許您根據日期以不一樣的方式運行測試)。

  • 爲了方便編寫BeanShell條件,TG準備瞭如下幾個參數:

    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.

  • CDATA聲明(如上所示)將表達式包圍起來,以免冗長地引用保留的XML字符。

註解轉換器

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)。

  • 任何@Before方法或@Test方法均可以聲明類型爲ITestContext的參數。
  • 任何@AfterMethod方法均可以聲明類型爲ITestResult的參數,該參數將反映剛剛運行的測試方法的結果。
  • 任何@Before和@After方法(@BeforeSuite和@AfterSuite除外)均可以聲明一個XmlTest類型的參數,它包含當前的<test>標記。
  • 任何@BeforeMethod(或@AfterMethod)能夠聲明java.lang.reflect.Method類參數。這個參數能夠接受@BeforeMethod運行完以後調用的測試方法(或在方法運行@AfterMethod以後)。
  • 任何@BeforeMethod均可以聲明類型爲Object[]的參數。該參數包含注入下一個測試方法的參數列表,這些參數由TestNG注入,例如java.lang.reflect.Method或@DataProvider。
  • 任何@DataProvider均可以聲明類型爲ITestContext或java.lang.reflect.Method的參數。
註解 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
@NoInjection
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斷言成功!

8.測試報告

關於測試報告,技術選型不少種,我選用的是比較簡單、好看的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)]

9.TestNg與Springboot結合

1. 編碼規範

對於單元測試,咱們但願您按照如下幾種規則去設計、處理、編碼:

1.命名規範

​ 測試包的根目錄:必須在src/test/java[源碼構建時會跳過此目錄,單元測試框架默認是掃描此目錄]

​ 測試包中java類的包路徑:與實際要測試的類,保持一致[ 參考編寫流程中的截圖]

​ 測試包的java類名:遵循OrderQueryService.java -> OrderQueryServiceTest.java規則

​ 測試包的xml路徑:在實際要測試的類的包下,新建xml包便可,存放各個測試類型testng.xml

​ 測試包的監聽器:在實際要測試的類的包下,根據監聽器做用範圍,新建listerners包便可,

2.設計原則

  • 全自動執行,而非交互式。mvn test或者運行對整個類的測試案例時,均可以自動運行完全部測試
  • 測試case要可靠,,而且是值得信賴的,對於相關底層的任何核心改動都要可以及時感知
  • 支持重複運行,而且保證覆蓋率【if-else】可以基本覆蓋全部場景。【普通語句70%,核心語句100%】
  • 必需要對運行結果作Assert斷言,不容許使用System.out.print,使用日誌log佔位符輸出測試信息
  • 純數據庫repository層的測試,不容許使用mock進行測試,而且保證要有數據回滾機制,不形成髒數據
  • 每一次項目版本的迭代、修改的同時,維護好測試案例,不容許對已經存在且運行無缺的測試Ignore
  • 複雜接口,儘可能拆解成單獨的測試案例,保持較小的粒度有助於迅速發現而且精準的定位問題

2. 編寫流程

1.目錄結構與包配置

基礎目錄

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-LbjdW9PA-1581562336480)(C:Users86151AppDataRoamingTyporatypora-user-images1580782404369.png)]

測試包配置

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8hS2NtoM-1581562336481)(C:Users86151AppDataRoamingTyporatypora-user-images1580782525627.png)]

2.pom引入依賴

<!--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>

3.編寫測試類

@DataProvider註解入參
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: &#x8ba2;&#x5355;&#x67e5;&#x8be2;&#x6d4b;&#x8bd5;
 * @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 },
        };
    }
@Parameters方式入參
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和它本身的一些註解【架構的案例已經知足】

4.編排測試類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爲準

3. 運行測試案例

1. IDEA運行單個測試

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-K2hhy4e4-1581562336483)(C:Users86151AppDataRoamingTyporatypora-user-images1580782613491.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fZjOfE51-1581562336483)(C:Users86151AppDataRoamingTyporatypora-user-images1580782642269.png)]

2. IDEA運行整個包的測試

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5THIiuIq-1581562336485)(C:Users86151AppDataRoamingTyporatypora-user-images1580782705068.png)]

3. Eclipse運行單個測試

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-k6C9V2B2-1581562336485)(C:Users86151AppDataRoamingTyporatypora-user-images1580782761162.png)]

4. Eclipse運行整個包的測試

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-h5QUh17b-1581562336485)(C:Users86151AppDataRoamingTyporatypora-user-images1580782774727.png)]

5. Debug斷點查看

Debug和正常程序同樣,可使用debug模式啓動測試案例,對其中的某些入參、返回作斷點,查看參數、返回

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cR7dgk8C-1581562336486)(C:Users86151AppDataRoamingTyporatypora-user-images1580782877064.png)]

6. MVN運行整個項目測試

本案例中執行mvn test的運行

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-BQjPjJpq-1581562336487)(C:Users86151AppDataRoamingTyporatypora-user-images1580783218995.png)]

若是出現結果錯誤時,須要對Failed tests中的錯誤測試,進行修復,直至Build Success爲止

4. 查看測試案例報告

某些狀況下,控制檯能夠看到測試結果,可是對於項目發佈,咱們最好仍是從測試結果報告中查看統計信息

咱們能夠從兩種類型的報告中獲取測試案例的運行狀況,哪些成功、失敗,從而對它們進行修復。

具體查看哪種報告,看我的習慣。

report-ng原始報告

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zzZpRQHQ-1581562336487)(C:Users86151AppDataRoamingTyporatypora-user-images1580783311156.png)]

external-report美化報告

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oQJqiPaa-1581562336487)(C:Users86151AppDataRoamingTyporatypora-user-images1580783343860.png)]

5.測試案例的數據處理【1】

場景分析

對於某些方法,對數據完整性依賴性較高,且手動構建數據複雜,測試場景須要全面的時候,須要經過測試案例來模擬完整的:入庫-查詢-修改-刪除場景時,那麼咱們在運行某些query的測試案例前,須要插入一些數據來支撐其餘測試運行,在運行完測試以後,咱們又須要擦除利用完了的數據

至此,咱們須要有相應的手段和case來覆蓋這些場景

那麼在編寫測試案例中,對於service、dao層的curd操做,所產生的髒數據問題,咱們須要從兩個角度去考慮:

1.本地服務數據擦除

對於本項目中service、dao數據的回滾,能夠從如下3個方面考慮:

1.自動回滾【推薦使用】

有兩個類須要說明一下:

1.AbstractTestNGSpringContextTests

​ 測試類只有繼承了該類才能擁有注入實例Bean的能力,不然注入報錯

​ 總結:【適合處理查詢的測試案例】

2.AbstractTransactionalTestNGSpringContextTests

​ 測試類繼承該類後擁有注入實例能力,同時擁有事物控制能力

​ 總結:【適應於任何場景,推薦使用】

因此,處理本地項目中的servicedao,對於數據庫產生的數據,咱們只須要將測試類繼承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);
    }
    //...   
}
2.選擇性回滾

測試類繼承AbstractTestNGSpringContextTests類,搭配使用@Rollback註解,對部分測試方法進行事物回滾,避免測試案例過程當中的測試數據最後成爲了髒數據,運行完測試案例後,TestNG會自動幫助咱們回滾對應註解了@Rollback方法所產生的數據,其餘沒有加註解的方法則會真實做用於數據庫層面,慎用

示例:

public class TestNgCurdServiceImplTest extends AbstractTestNGSpringContextTests {
//....
//寫法保持不變,不須要改動任何代碼,只須要在須要回滾的方法上加上註解@Rollback註解便可
//....
}
3.編程式回滾【慎用】

手動編程,將測試案例運行先後,產生的全部數據,手動調用相關的刪除delete接口逐一擦除【適合遠程服務】

下面章節【遠程服務數據回滾】有相關的案例和說明!

2.遠程服務數據擦除

若是一個測試案例以非Mock方式運行,而且有對遠程服務進行調用,產生了髒數據,那麼此時只能經過編程式回滾數據,即在執行完測試案例的先後,調用相關delete方法,進行清除,若是測試案例上下文較爲複雜,對數據的回收分析就變得比較重要,而且服務間鏈式調用過長,一旦測試案例產生了錯誤,那麼會產生不可預知的一些問題,須要謹慎使用。

本例中,咱們也有相關的case覆蓋:

核心流程:

​ 測試遠程服務的增刪改查,則必然須要在查詢方法前,咱們須要插入準備數據,才能測試查詢接口,咱們能夠經過TestNg提供的執行機制,在運行完測試案例以後調用相關的delete方法,清除運行期間臨時準備的測試數據,不然會污染數據庫。

有興趣的小夥伴能夠根據如下代碼自行測試一下

1.service接口及其實現類
/**
 * 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);

}
2.測試類
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);
        }
    }
}
3.xml文件
<?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方法報錯,那麼插入數據庫中的數據就會保留,須要手動去清除庫,調用鏈路若是比較長,涉及面較廣的時候,存在不肯定性!

6.測試案例的數據處理【2】

章節【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&amp;characterencoding=utf-8&amp;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批處理腳本
咱們根據不一樣的運行環境,設置不一樣的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測試
<?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)]

7.測試案例數據問題的總結

1.對於本地服務,很顯然【自動回滾】的方式最佳,無代碼侵入,安全、乾淨,實現簡便。

2.對於遠程服務,顯然是Mock的方式處理測試案例效果更好,mock測試自己就是一種假定,不會對數據庫的數據產生實際影響,設計者只須要關心測試案例的核心業務,而不須要操心因環境、數據所帶來的額外問題。

10.Mockito與TestNg結合案例

Why Mock

  1. 傳統的測試案例,是以啓動整個Spring容器爲代價進行測試,不然對於某些深度DI的bean沒法進行測試,耗時較長。而使用mockito能夠對沒必要要的bean進行過濾,啓動耗時短。
  2. 對於測試案例的內部bean的實現細節,mockito能夠作細節控制,細化到測試方法內部的一些調用參數、執行次數、返回值的控制。更偏向於測試內部細節的把控以及業務編排。傳統的測試案例沒法作到。
  3. Mockito最強大的一點,是能夠對測試類的依賴bean進行mock。

    在某些狀況下,對於某些深度依賴的bean或者是遠程服務bean,並且這些bean或者服務基本能夠確保沒有問題,只是本地環境有限,沒法產生實際的調用時,就可使用mockito對這些bean進行mock,這樣不會產生實際的調用,可是又可以在測試案例中完整的模擬出調用的功能時。使用傳統的測試案例任何一點出差錯,都會致使運行結果失敗。

  4. 和TestNg無縫結合,便可以使用mock來作預言,也可使用TestNg的功能特性。

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);
    }
}

11.參考資料

【官網】 https://testng.org/doc/docume...

【教程】 https://www.bbsmax.com/A/kvJ3...

【實戰】 https://www.jianshu.com/p/880...

相關文章
相關標籤/搜索