技術部突然宣布:JAVA開發人員全部要會接口自動化測試框架


整理了一些Java方面的架構、面試資料(微服務、集群、分布式、中間件等),有需要的小伙伴可以關注公眾號【程序員內點事】,無套路自行領取

寫在前邊

用單元測試Junit完全可以滿足日常開發自測,為什么還要學習TestNG,都影響了我的開發進度!

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

我們來一起看看它比Junit好在哪?


一、TestNG初識

TestNG[后面都簡稱為TG]是一款為了大量測試(比如測試時多接口數據依賴)需要,所誕生的一款測試框架,從簡單的單元測試再到集成測試甚至是框架級別的測試,都可以覆蓋到,因此是一款非常強大的測試框架!

1.編寫步驟

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

  1. 編寫業務測試代碼時,插入TG提供的注解

  2. 將要測試的類、group等進行編排,寫入到testng.xml或build.xml

  3. 運行單個TG測試案例【idea、eclipse、ant、cmd等都可以運行】

  4. 運行整個項目的test-ng測試案例【如果是多模塊項目,則是進入到對應的模塊目錄運行命令或者配置】

    • xml

      <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命令

      此處的命令會優先於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方法都為測試方法

備注:相關對應的屬性配置值,點進去對應類中查詢即可,不在一一贅述

3.名詞解釋

suite

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

​ 一般來講,一個xml的<suite>對應一個java類,除非特殊情況,在java中需要特別指定<suite>

​ 否則xml對應java類的所有@Test注解屬性suiteName默認都是xml中定義的<suite name='xxx'>

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中定義的1、2、3方法都會被運行,如果換成check-intest,則只運行方法1、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的注解,由 表示

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>中定義

​ 如果在 中指定組“a”,在 中指定組“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中,可以在 標記下或 下聲明它們。

    如果兩個參數具有相同的名稱,則在 中定義的參數具有優先權。如果您需要指定一個適用於所有測試的參數,並僅對某些測試重寫其值,則這很方便。

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/08/19/dependent-test-methods/

軟性依賴

即使有些方法失敗了,你也會一直在追求你所依賴的方法。

當您只想確保您的測試方法以特定的順序運行,但它們的成功並不真正依賴於其他方法的成功時,這非常有用。

通過在@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:\Users\dell\AppData\Roaming\Typora\typora-user-images\1579156460298.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 逗號分割的測試名稱 只有測試案例的 匹配上此處的配置才會運行
-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/eclipse.html

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/cbeust/testng/master/javadoc/org/testng/package-summary.html

BeanShell

如果testng.xml中的 標記不足以滿足您的需要,您可以使用BeanShell表達式來決定某個測試方法是否應包含在測試運行中。

您可以在 標記下指定此表達式:

<test name="BeanShell test">
   <method-selectors>
     <method-selector>
       <script language="beanshell"><![CDATA[
         groups.containsKey("test1")
       ]]></script>
     </method-selector>
   </method-selectors>
  <!-- ... -->

當在testng.xml中找到script標記時,testng將忽略當前 標記中組和方法的后續 ,您的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/2f934240699e

監聽類型

​ 有幾個接口允許您修改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類型的參數,它包含當前的 標記。
  • 任何@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插件,配合自定義的監聽器實現

備注:target\surefire-reports\index.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:\Users\dell\AppData\Roaming\Typora\typora-user-images\1579085189960.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:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782404369.png)]

測試包配置

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8hS2NtoM-1581562336481)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782525627.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() {
        //1、構建空對象
        WorkOrderListQueryReqParam checkParam = new WorkOrderListQueryReqParam();
        //2、構建殘缺參數(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));
        //3、構建訂單來源是 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));
        //4、完整參數
        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() {
        //1、構建空對象
        WorkOrderDetailQueryReqParam checkParam = new WorkOrderDetailQueryReqParam();
        //2、構建殘缺參數(WorkOrderNo)
        WorkOrderDetailQueryReqParam checkWorkOrderNoParam = new WorkOrderDetailQueryReqParam();
        checkWorkOrderNoParam.setRequestSource(99);
        //4、完整參數
        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:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782613491.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fZjOfE51-1581562336483)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782642269.png)]

2. IDEA運行整個包的測試

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5THIiuIq-1581562336485)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782705068.png)]

3. Eclipse運行單個測試

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-k6C9V2B2-1581562336485)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782761162.png)]

4. Eclipse運行整個包的測試

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-h5QUh17b-1581562336485)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782774727.png)]

5. Debug斷點查看

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

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cR7dgk8C-1581562336486)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782877064.png)]

6. MVN運行整個項目測試

本案例中執行mvn test的運行

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-BQjPjJpq-1581562336487)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580783218995.png)]

如果出現結果錯誤時,需要對Failed tests中的錯誤測試,進行修復,直至Build Success為止

4. 查看測試案例報告

某些情況下,控制台可以看到測試結果,但是對於項目發布,我們最好還是從測試結果報告中查看統計信息

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

具體查看哪一種報告,看個人習慣。

report-ng原始報告

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zzZpRQHQ-1581562336487)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580783311156.png)]

external-report美化報告

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oQJqiPaa-1581562336487)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580783343860.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:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580965050323.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:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580965229125.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/documentation-main.html

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

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


今天就說這么多,如果本文對您有一點幫助,希望能得到您一個點贊👍哦

您的認可才是我寫作的動力!


整理了一些Java方面的架構、面試資料(微服務、集群、分布式、中間件等),有需要的小伙伴可以關注公眾號【程序員內點事】,無套路自行領取


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM