單篇長文TestNG從入門到精通

測試開發剛哥發表於2022-01-15

簡介

TestNG是Test Next Generation的縮寫,它的靈感來自於JUnit和NUnit,在它們基礎上增加了很多很牛的功能,比如說:

  • 註解。
  • 多執行緒,比如所有方法都在各自執行緒中,一個測試類一個執行緒等。
  • 驗證程式碼是否多執行緒安全。
  • 靈活的測試配置。
  • 支援資料驅動(@DataProvider)。
  • 支援引數化。
  • 強大的執行機制(不需要TestSuite)。
  • 能跟各種工具結合(比如IDEA、Maven等)。
  • 內嵌BeanShell。
  • 提供執行時和日誌的JDK函式(不需要新增dependencies)。
  • 提供應用伺服器測試依賴的方法。

一句話總結就是,TestNG強大到能覆蓋所有測試型別:單元測試、功能測試、端到端測試、整合測試等等等。

一個簡單的示例如下:

package example1;
 
import org.testng.annotations.*;
 
public class SimpleTest {
 
 @BeforeClass
 public void setUp() {
   // code that will be invoked when this test is instantiated
 }
 
 @Test(groups = { "fast" })
 public void aFastTest() {
   System.out.println("Fast test");
 }
 
 @Test(groups = { "slow" })
 public void aSlowTest() {
    System.out.println("Slow test");
 }
 
}
  • setUp()會在測試類建立後,測試方法執行前執行。
  • 測試方法名字可以任意的,TestNG通過@Test註解來標識。
  • 可以把測試方法按group分組。

然後使用xml來配置執行策略:

<project default="test">
 
 <path id="cp">
   <pathelement location="lib/testng-testng-5.13.1.jar"/>
   <pathelement location="build"/>
 </path>
 
 <taskdef name="testng" classpathref="cp"
          classname="org.testng.TestNGAntTask" />
 
 <target name="test">
   <testng classpathref="cp" groups="fast">
     <classfileset dir="build" includes="example1/*.class"/>
   </testng>
 </target>
 
</project>

使用ant呼叫(下面第2節給出了最新的執行方式):

c:> ant
Buildfile: build.xml
 
test:
[testng] Fast test
[testng] ===============================================
[testng] Suite for Command line test
[testng] Total tests run: 1, Failures: 0, Skips: 0
[testng] ===============================================
 
 
BUILD SUCCESSFUL
Total time: 4 seconds

最後就能看報告了:

start test-output\index.html (on Windows)

如何執行TestNG

除了前面提到的ant呼叫,更常見的執行TestNG的方式是以下兩種:

  1. IDEA
  2. Maven

IDEA

在IDEA中執行TestNG第①種方式是點選方法或類前面的小箭頭:

第②種方式是在方法或類內部點選右鍵:

第③種方式是使用testng.xml,IDEA可以右鍵testng.xml來執行,在檔名和檔案內部點選均可:

testng.xml支援很多種配置,比如配置測試類:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >
  
<suite name="Suite1" verbose="1" >
  <test name="Nopackage" >
    <classes>
       <class name="NoPackageTest" />
    </classes>
  </test>
 
  <test name="Regression1">
    <classes>
      <class name="test.sample.ParameterSample"/>
      <class name="test.sample.ParameterTest"/>
    </classes>
  </test>
</suite>

配置測試包:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >
 
<suite name="Suite1" verbose="1" >
  <test name="Regression1"   >
    <packages>
      <package name="test.sample" />
   </packages>
 </test>
</suite>

配置組和方法:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >
 
<suite name="Suite1" verbose="1" >
  <test name="Regression1">
    <groups>
      <run>
        <exclude name="brokenTests"  />
        <include name="checkinTests"  />
      </run>
    </groups>

    <classes>
      <class name="test.IndividualMethodsTest">
        <methods>
          <include name="testMethod" />
        </methods>
      </class>
    </classes>
  </test>
</suite>

預設TestNG會按xml順序執行,可以設定 preserve-order為false變成隨機順序:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >
 
<suite name="Suite1" verbose="1" >
  <test name="Regression1" preserve-order="false">
    <classes>

      <class name="test.Test1">
        <methods>
          <include name="m1" />
          <include name="m2" />
        </methods>
      </class>

      <class name="test.Test2" />

    </classes>
  </test>
</suite>

Maven

官方教程給出了命令列執行TestNG:

java org.testng.TestNG testng1.xml [testng2.xml testng3.xml ...]

實際呼叫是調不通的,可以藉助Maven來實現

先在pom.xml新增:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.19.1</version>
            <configuration>
                <suiteXmlFiles>
                    <suiteXmlFile>testng.xml</suiteXmlFile>
                </suiteXmlFiles>
            </configuration>
        </plugin>
    </plugins>
</build>

然後執行mvn clean test命令就行啦。

測試方法和測試類

使用了@Test註解的方法就是測試方法,包含測試方法的類就是測試類。比如:

package example1;
 
import org.testng.annotations.*;
 
public class SimpleTest {
 
 @BeforeClass
 public void setUp() {
   // code that will be invoked when this test is instantiated
 }
 
 @Test(groups = { "fast" })
 public void aFastTest() {
   System.out.println("Fast test");
 }
 
 @Test(groups = { "slow" })
 public void aSlowTest() {
    System.out.println("Slow test");
 }
 
}

@Test也能放在測試類上面,這樣測試類下的所有方法都是測試方法,比如:

@Test
public class Test1 {
  public void test1() {
  }
 
  public void test2() {
  }
}

而且可以在測試類和測試方法上同時使用@Test,比如給某個Test加入分組:

@Test
public class Test1 {
  public void test1() {
  }
 
  @Test(groups = "g1")
  public void test2() {
  }
}

有個點需要注意的是@Test註解的測試方法,預設是會忽略返回值的,除非在testng.xml配置:

<suite allow-return-values="true">
 
or
 
<test allow-return-values="true">

測試組

測試組其實就是給測試方法打標記,比如冒煙測試用例和功能測試用例:

public class Test1 {
  @Test(groups = { "functest", "checkintest" })
  public void testMethod1() {
  }
 
  @Test(groups = {"functest", "checkintest"} )
  public void testMethod2() {
  }
 
  @Test(groups = { "functest" })
  public void testMethod3() {
  }
}

如果只配置functest,就會執行全部測試方法:

<test name="Test1">
  <groups>
    <run>
      <include name="functest"/>
    </run>
  </groups>
  <classes>
    <class name="example1.Test1"/>
  </classes>
</test>

如果只配置checkintest,就會只執行前面2個方法:

<test name="Test1">
  <groups>
    <run>
      <include name="checkintest"/>
    </run>
  </groups>
  <classes>
    <class name="example1.Test1"/>
  </classes>
</test>

除了指定完整name,也可以使用正規表示式:

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

進一步,測試組除了include測試方法,還可以exclude:

<test name="Test1">
  <classes>
    <class name="example1.Test1">
      <methods>
        <include name=".*enabledTestMethod.*"/>
        <exclude name=".*brokenTestMethod.*"/>
      </methods>
     </class>
  </classes>
</test>

還有一個辦法是使用@Test@Before/Afterenabled屬性。

測試組也能進行巢狀:

<test name="Regression1">
  <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="test.sample.Test1"/>
  </classes>
</test>

<define></define>定義了父分組,裡面<include>的就是子分組。

同樣的,巢狀分組也可以對子分組進行exclude:

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

最後,對於分組的位置,如果測試類和測試方法都標記了分組,那麼測試類的分組會作用到所有方法中,比如:

@Test(groups = {"checkin-test"})
public class All {
    @Test(groups = {"func-test")
    public void method1() { ...}

    public void method2() { ...}
}

method2()屬於checkin-test分組,method1屬於func-test和checkin-test兩個分組。

TestNG引數化

TestNG引數化有兩種方式,第一種是從testng.xml讀取資料,第二種是通過程式碼讀取資料。

從testng.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">
  <-- ... -->
  • @Parameters指定引數化名字。

  • 測試方法入參與引數化名字一一對應。

  • testng.xml中<parameter>定義引數化的值。

    在testng.xml中,<parameter>既可以定義在<suite>中也可以定義在<test>中,如果有同名的,會以<test>的覆蓋<suite>

@Parameters既可以作用到@Test,也可以作用到 @Before/After@Factory,比如:

@Parameters({ "datasource", "jdbcDriver" })
@BeforeMethod
public void beforeTest(String ds, String driver) {
  m_dataSource = ...;                              // look up the value of datasource
  m_jdbcDriver = driver;
}

也可以作用到測試類的構造方法中,但是隻能最多一個構造方法,這樣就能在初始化類的時候,進行引數化賦值,便於測試方法使用

@Optional用於標識引數是否可選,比如:

@Parameters("db")
@Test
public void testNonExistentParameter(@Optional("mysql") String db) { ... }
  • 如果db這個引數取不到名字,那麼就會取mysql的值。

通過程式碼讀取資料

第一種引數化方式其實比較雞肋,第二種方式才是TestNG引數化的靈魂,用到了@DataProvider,它會返回一個二維陣列:

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class DPTest {
    @DataProvider(name = "test1")
    public Object[][] createData1() {
        return new Object[][] {
                { "Cedric", 36},
                { "Anne", 37},
        };
    }

    @Test(dataProvider = "test1")
    public void verifyData1(String n1, Integer n2) {
        System.out.println(n1 + " " + n2);
    }
}
  • @DataProvider用於生產資料,name是唯一標識。
  • 在@Test中通過dataProvider屬性指定name。
  • 測試方法的入參跟陣列中元素一一對應。

預設@DataProvider和@Test是在同一個類中,如果想放在不同的類,那麼需要定義為靜態方法(或者無引數構造方法的類),比如:

import org.testng.annotations.DataProvider;

public class StaticProvider {
    @DataProvider(name = "create")
    public static Object[][] createData() {
        return new Object[][] {
                new Object[] {42}
        };
    }
}
import org.testng.annotations.Test;

public class DiffClazzTest {
    @Test(dataProvider = "create", dataProviderClass = StaticProvider.class)
    public void test(Integer n) {
        System.out.println(n);
    }
}
  • createData()為static。
  • 需要額外通過@Test的dataProviderClass屬性指定@DataProvider所在的類。

@DataProvider的返回值(引數型別)除了已經提到的Object[][],還可以是Iterator<Object[]>,它不會一次性生成所有資料,而是每呼叫一次生成一次,節約記憶體,比如:

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.Iterator;

public class IterTest {
    @DataProvider(name = "test1")
    public Iterator<Object[]> createData1() {
        Object[][] myObjects = new Object[][]{
                {"Cedric", 36},
                {"Anne", 37},
        };
        return Arrays.asList(myObjects).iterator();
    }

    @Test(dataProvider = "test1")
    public void verifyData1(String n1, Integer n2) {
        System.out.println(n1 + " " + n2);
    }
}

看到這裡,對@DataProvider已經有了足夠的認識,它支援兩種引數型別

  • Object[][]
  • Iterator<Object[]>

假如測試方法只有一個入參,是不是隻能用二維來實現:

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.Iterator;

public class IterTest {
    @DataProvider(name = "test1")
    public Iterator<Object[]> createData1() {
        Object[][] myObjects = new Object[][]{{"x"}, {"y"}};
        return Arrays.asList(myObjects).iterator();
    }

    @Test(dataProvider = "test1")
    public void verifyData1(String n) {
        System.out.println(n);
    }
}

其實不是,@DataProvider支援一維陣列:

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class IterTest {
    @DataProvider(name = "test1")
    public Object[] createData1() {
        Object[] myObjects = new Object[]{"x", "y"};
        return myObjects;
    }

    @Test(dataProvider = "test1")
    public void verifyData1(String n) {
        System.out.println(n);
    }
}

以及一維陣列的迭代器:

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.Iterator;

public class IterTest {
    @DataProvider(name = "test1")
    public Iterator<Object> createData1() {
        Object[] myObjects = new Object[]{"x", "y"};
        return Arrays.asList(myObjects).iterator();
    }

    @Test(dataProvider = "test1")
    public void verifyData1(String n) {
        System.out.println(n);
    }
}

最精彩的來了,@DataProvider支援反射,也就是反向獲取測試方法的資訊:

@DataProvider(name = "dp")
public Object[][] createData(Method m) {
  System.out.println(m.getName());  // print test method name
  return new Object[][] { new Object[] { "Cedric" }};
}
 
@Test(dataProvider = "dp")
public void test1(String s) {
}
 
@Test(dataProvider = "dp")
public void test2(String s) {
}
  • createData的入參是java.lang.reflect.Method,這樣就能獲取到測試方法的資訊,比如這裡的getName()會依次拿到test1、test2。

@DataProvider還支援併發:

@DataProvider(parallel = true)// ...

預設是10個執行緒,可以在testng.xml中修改:

<suite name="Suite1" data-provider-thread-count="20" >...

一個xml共享一個執行緒池,如果要用多個執行緒池,那麼需要建立多個testng.xml。

錦上添花的是,TestNG的引數化會列印在測試報告中:

指定用例執行順序

TestNG用例的執行順序有兩種方式來指定:註解和XML。

註解

使用@Test的dependsOnMethods屬性:

@Test
public void serverStartedOk() {}
 
@Test(dependsOnMethods = { "serverStartedOk" })
public void method1() {}

或者dependsOnGroups屬性:

@Test(groups = { "init" })
public void serverStartedOk() {}
 
@Test(groups = { "init" })
public void initEnvironment() {}
 
@Test(dependsOnGroups = { "init.*" })
public void method1() {}

@Before/After也能實現初始化,但是它們的結果不會出現在測試報告中。

預設TestNG會強制校驗,依賴的用例必須成功才會執行當前用例,否則當前用例會被標記為SKIP,這叫做強依賴。通過設定alwaysRun=true可以變成弱依賴,無論依賴用例執行成功與否,都會執行當前用例。

需要特別注意的是,依賴測試方法是按照測試類來進行執行的(group by class),比如b()方法依賴的a()方法有多個例項,那麼會按照以下順序執行:

a(1)
a(2)
b(2)
b(2)

舉個實際的例子,登入和登出,如果想達到以下效果:

signIn("us")
signOut("us")
signIn("uk")
signOut("uk")

那麼需要在XML中進行配置:

  <suite name="Factory" group-by-instances="true">
or
  <test name="Factory" group-by-instances="true">

XML

在testng.xml中使用<dependencies>depends-on來指定用例順序:

<test name="My suite">
  <groups>
    <dependencies>
      <group name="c" depends-on="a  b" />
      <group name="z" depends-on="c" />
    </dependencies>
  </groups>
</test>

動態建立測試用例

假設有這樣的測試用例,在測試時需要對網頁訪問多次,那麼在TestNG中會這樣編寫程式碼:

public class TestWebServer {
  @Test(parameters = { "number-of-times" })
  public void accessPage(int numberOfTimes) {
    while (numberOfTimes-- > 0) {
     // access the web page
    }
  }
}
<test name="T1">
  <parameter name="number-of-times" value="10"/>
  <classes>
    <class name= "TestWebServer" />
  </classes>
</test>
 
<test name="T2">
  <parameter name="number-of-times" value="20"/>
  <classes>
    <class name= "TestWebServer"/>
  </classes>
</test>
 
<test name="T3">
  <parameter name="number-of-times" value="30"/>
  <classes>
    <class name= "TestWebServer"/>
  </classes>
</test>
  • 由於訪問次數不一,在testng.xml中定義了3個test,然後藉助引數化將訪問次數傳給@Test測試方法。
  • 在@Test測試方法中迴圈遍歷numberOfTimes。

這種需求可以採用TestNG的Factory來對程式碼進行簡化:

import org.testng.annotations.Factory;

public class WebTestFactory {
    @Factory
    public Object[] createInstances() {
        Object[] result = new Object[3];
        for (int i = 0; i <= 2; i++) {
            result[i] = new WebTest((i + 1) * 10);
        }
        return result;
    }
}
import org.testng.annotations.Test;

public class WebTest {
    private int m_numberOfTimes;

    public WebTest(int numberOfTimes) {
        m_numberOfTimes = numberOfTimes;
    }

    @Test
    public void testServer() {
        for (int i = 0; i < m_numberOfTimes; i++) {
            // access the web page
            System.out.println(i);
        }
    }
}
  • WebTestFactory是工廠函式,返回Object[]。
  • WebTestFactory動態建立了多個WebTest例項。

既可以在IDEA中點選WebTestFactory的執行按鈕執行測試:

image-20220111093212591

注意如果執行WebTest,會提示No tests were found。

也可以在testng.xml中驅動:

<class name="WebTestFactory" />

還可以直接在程式碼中驅動:

TestNG testNG = new TestNG();
testNG.setTestClasses(WebTestFactory.class);
testNG.run();

@Factory和@Test一樣,都能使用dataProvider屬性,比如:

@Factory(dataProvider = "dp")
public FactoryDataProviderSampleTest(int n) {
  super(n);
}
 
@DataProvider
static public Object[][] dp() {
  return new Object[][] {
    new Object[] { 41 },
    new Object[] { 42 },
  };
}

忽略部分測試用例

TestNG可以使用@Ignore註解忽略測試,比如:

import org.testng.annotations.Ignore;
import org.testng.annotations.Test;
 
@Ignore
public class TestcaseSample {
 
    @Test
    public void testMethod1() {
    }
 
    @Test
    public void testMethod2() {
    }
}

如果作用到測試類上,那麼它會忽略這個類下面的所有測試方法。如果只作用到測試方法上,那麼它就相當於@Test(enabled=false)。另外還能放在包上面:

@Ignore
package com.testng.master;
 
import org.testng.annotations.Ignore;

多執行緒並行測試用例

可以給@Test新增屬性,讓用例以多執行緒並行執行:

@Test(threadPoolSize = 3, invocationCount = 10,  timeOut = 10000)
public void testServer() {
  • threadPoolSize指3個執行緒。
  • invocationCount指執行10次。
  • timeOut指阻塞等待超時。

也可以在testng.xml中設定,thread-count指定執行緒數,parallel設定不同的值有不同的含義:

methods:

<suite name="My suite" parallel="methods" thread-count="5">

所有測試方法在不同的獨立執行緒中執行。

tests:

<suite name="My suite" parallel="tests" thread-count="5">

<test>標籤內的測試方法會在同一個執行緒中執行,不同的<test>標籤會在不同的獨立執行緒中執行。

classes:

<suite name="My suite" parallel="classes" thread-count="5">

同一個類中的測試方法會在同一個執行緒中執行,不同的類會在不同的獨立執行緒中執行。

instances:

<suite name="My suite" parallel="instances" thread-count="5">

同一個例項中的測試方法會在同一個執行緒中執行,不同的例項會在不同的獨立執行緒中執行。(Factory能建立多個例項)

重跑失敗用例

TestNG在執行後會把失敗的用例輸出到testng-failed.xml檔案中,可以直接執行這個檔案來重跑失敗用例。

誇一句,這個設計真棒。

有時候,需要讓用例失敗時自動重試,那麼可以在程式碼中這樣實現:

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();
  }
}
  • 實現IRetryAnalyzer介面的retry方法。
  • 在@Test的retryAnalyzer中指定重試類。

TestNG程式設計

除了IDEA和Maven這兩種執行方式,TestNG還可以直接在程式中呼叫執行:

TestListenerAdapter tla = new TestListenerAdapter();
TestNG testng = new TestNG();
testng.setTestClasses(new Class[] { Run2.class });
testng.addListener(tla);
testng.run();
  • TestListenerAdapter是預設的,可以實現org.testng.ITestListener介面自定義TestListener。
  • setTestClasses新增測試類。
  • run()執行。

還可以通過程式設計建立一個虛擬的testng.xml,org.testng.xml包的XmlClass, XmlTest等提供了這個能力:

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

這段程式碼會建立一個這樣的testng.xml:

<suite name="TmpSuite" >
  <test name="TmpTest" >
    <classes>
      <class name="test.failures.Child"  />
    <classes>
    </test>
</suite>

XmlSuite可以通過TestNG程式呼叫:

List<XmlSuite> suites = new ArrayList<XmlSuite>();
suites.add(suite);
TestNG tng = new TestNG();
tng.setXmlSuites(suites);
tng.run();

XML中寫BeanShell

是的,TestNG的XML中可以寫BeanShell,用來替代<include><exclude>

<test name="BeanShell test">
   <method-selectors>
     <method-selector>
       <script language="beanshell"><![CDATA[
         groups.containsKey("test1")
       ]]></script>
     </method-selector>
   </method-selectors>
  <!-- ... -->
  • CDATA用來防止跟XML標籤語法衝突。
  • 預置了method(當前測試方法)、testngMethod(當前測試方法的描述)、groups(當前測試方法所屬分組)三個物件,可以用來做匹配。
  • 使用<script>後,<include><exclude>會失效。

Listener

TestNG提供了很多Listener用來自定義TestNG行為,類似於Hook那個意思:

  • IAnnotationTransformer
  • IAnnotationTransformer2
  • IHookable
  • IInvokedMethodListener
  • IMethodInterceptor
  • IReporter
  • ISuiteListener
  • ITestListener
  • ITestNGListener
  • IAlterSuiteListener

比如用IAnnotationTransformer動態設定@Test屬性:

public class MyTransformer implements IAnnotationTransformer {
  public void transform(ITest annotation, Class testClass,
      Constructor testConstructor, Method testMethod)
  {
    if ("invoke".equals(testMethod.getName())) {
      annotation.setInvocationCount(5);
    }
  }
}

如果想修改@Factory或@DataProvider,得用IAnnotationTransformer2。

比如用IMethodInterceptor設定用例組的執行順序:

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);
    }
    //讓fast分組第一個執行
    if (groups.contains("fast")) {
      result.add(0, m);
    }
    else {
      result.add(m);
    }
  }
  return result;
}

比如想自定義測試開始前和測試後的行為,可以先寫個Listener:

public class BeforeAfterLog extends TestListenerAdapter {
 		public void onTestSuccess(ITestResult result) {
        System.out.println("onTestSuccess");
    }

    public void onTestFailure(ITestResult result) {
        System.out.println("onTestFailure");
    }
}

然後在測試類上新增:

@Listeners(BeforeAfterLog.class)
public class MyTest {
}

這樣就不用每個測試類都寫@BeforeClass和@AfterClass了。

依賴注入

TestNG支援在測試方法中新增特定類來進行依賴注入(就是獲取TestNG相關資訊):

  • ITestContext
  • XmlTest 當前<test>標籤
  • Method 當前呼叫的測試方法
  • Object[] 當前測試方法的入參
  • ITestResult 當前測試結果

不同註解支援的方式如下表所示:

image-20220114130719485

示例:

public class NoInjectionTest {
 
  @DataProvider(name = "provider")
  public Object[][] provide() throws Exception {
      return new Object[][] { { CC.class.getMethod("f") } };
  }
 
  @Test(dataProvider = "provider")
  public void withoutInjection(@NoInjection Method m) {
      Assert.assertEquals(m.getName(), "f");
  }
 
  @Test(dataProvider = "provider")
  public void withInjection(Method m) {
      Assert.assertEquals(m.getName(), "withInjection");
  }
}

@NoInjection用於禁止依賴注入。

斷言

TestNG支援Java的assert關鍵字斷言,示例:

@Test
public void verifyLastName() {
  assert "Beust".equals(m_lastName) : "Expected name Beust, for" + m_lastName;
}

也可以用JUnit斷言方法:

import static org.testng.AssertJUnit.*;
//...
@Test
public void verify() {
  assertEquals("Beust", m_lastName);
}

日誌

用Listener來實現,直接看程式碼:

public class DotTestListener extends TestListenerAdapter {
  private int m_count = 0;
 
  @Override
  public void onTestFailure(ITestResult tr) {
    log("F");
  }
 
  @Override
  public void onTestSkipped(ITestResult tr) {
    log("S");
  }
 
  @Override
  public void onTestSuccess(ITestResult tr) {
    log(".");
  }
 
  private void log(String string) {
    System.out.print(string);
    if (++m_count % 40 == 0) {
      System.out.println("");
    }
  }
}

測試報告

TestNG自帶了一個測試報告,執行完後會生成index.html,開啟就是,比較醜,推薦用Allure。

如果想獲取測試報告的資料,那麼可以從org.testng.IReporter介面的方法:

public void generateReport(List<ISuite> suites, String outputDirectory)

根據它的入參去拿。

如果想給測試報告新增資料,那麼可以使用org.testng.Reporter類:

Reporter.log("M3 WAS CALLED");

YAML替代XML

如果你煩透了XML,那麼可以試試YAML。

比如XML:

<suite name="SingleSuite" verbose="2" thread-count="4">
 
  <parameter name="n" value="42" />
 
  <test name="Regression2">
    <groups>
      <run>
        <exclude name="broken" />
      </run>
    </groups>
 
    <classes>
      <class name="test.listeners.ResultEndMillisTest" />
    </classes>
  </test>
</suite>

換成YAML:

name: SingleSuite
threadCount: 4
parameters: { n: 42 }
 
tests:
  - name: Regression2
    parameters: { count: 10 }
    excludedGroups: [ broken ]
    classes:
      - test.listeners.ResultEndMillisTest

舒服多了。不過TestNG本身沒有引入YAML依賴包,需要自己新增:

<dependency>
  <groupid>org.yaml</groupid>
  <artifactid>snakeyaml</artifactid>
  <version>1.23</version>
</dependency>

彩蛋

看完了官方教程,感覺測試框架的功能基本上大同小異,只是技術實現上有所區別。讓我驚喜的是,TestNG執行失敗的用例可以自動生成一份失敗用例的xml檔案,直接拿來重跑就可以了。還有一直以為TestNG只能用XML,沒想到也能用YAML了。最後,它給出了-Dtestng.mode.dryrun=true這個引數,猜猜它是幹嘛的。有時候只想看跑起來會執行哪些測試,而不想真正執行,那麼就可以用這個引數。

image-20220114135434195

參考資料:

https://testng.org/doc/

https://testng.org/doc/documentation-main.html

相關文章