作者 | 崔莹峰
审校 | 孙淑娟
参数化测试是TestNG的一个重要特性。 在很多情况下,我们会遇到业务逻辑需要大量测试的场景。 参数化测试允许我们使用不同的值一次又一次地运行相同的测试。
本文介绍了TestNG 参数化注入的三种不同方式,并在此基础上,介绍了测试代码和测试数据、测试数据和测试环境的分离实践以及一种利用Nacos将测试代码和测试数据维护分离的有效实践。另外,本文将会引导读者完成一个实例,该示例将会较好地演示TestNG 参数的注入方式以及分离测试代码、数据与环境的有效实践。
示例项目准备
这里我们使用IDEA 社区版演示本实践全过程,读者也可以使用Eclipse和VSCode参考执行,但笔者并不推荐。
填写项目信息
Language选择Java,Build system选择Maven,JDK选择1.8后,点“Create” 进入工程页面。
pom.xml里加入dependency:
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.4.0</version>
<scope>test</scope>
</dependency>
TestNG 参数化注入的方式
TestNG可以通过如下三种不同的方式将参数直接传递给测试方法:
- 使用注解Parameters配合testng.xml
- 使用注解Parameters配合-D命令行参数
- 使用注解DataProvider
使用注解Parameters配合testng.xml
我们通过在testng.xml定义参数,然后在代码里面可以直接引用。我们先写一个测试用例如下:
src/test/java/com/fastjrun.example/testng/ParameterizedTest1.java
package com.fastjrun.example.testng;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;
public class ParameterizedTest1 {
@Test
@Parameters("myName")
public void parameterTest(String myName) {
System.out.println("Parameterized value is : " + myName);
}
}
编写testng.xml如下:
<?xml version = "1.0" encoding = "UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name = "Suite1">
<test name = "test1">
<parameter name = "myName" value="manisha"/>
<classes>
<class name = "com.fastjrun.example.testng.ParameterizedTest1" />
</classes>
</test>
</suite>
执行测试。
在testng.xml编辑窗口点击鼠标右键弹出下拉菜单后,从中选择“Run .../src/test/resources/testng.xml”即可执行该单元测试用例。
执行结果如下:
Parameterized value is : manisha
===============================================
Suite1
Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
===============================================
另外我们也可以如下图所示,在ParameterizedTest1类编辑窗口选中需要测试的方法名parameterTest,点击鼠标右键弹出下拉菜单后,从中选择“Run parameterTest()”即可执行该单元测试用例。
但通过如上方式运行单元测试,结果如下:
……
[Utils] [ERROR] [Error] org.testng.TestNGException:
Parameter 'myName' is required by @Test on method parameterTest but has not been marked @Optional or defined
in /Users/cuiyingfeng/Library/Caches/JetBrains/IdeaIC2022.1/temp-testng-customsuite.xml
显然如上方式触发的测试用例不能指定依赖的testng.xml,myName参数没有被初始化,导致测试失败。
使用testng.xml 执行测试的方式适合对测试用例批量执行的场景;如果我们在IDE中只想执行某个具体的测试方法,还是需要从类中触发测试方法的方式,也就是要通过非testng.xml配置的方式解决myName初始化的问题。
解决myName初始化问题的方法一共有两种,其中一种是在测试方法parameterTest输入参数前加@Optional注解,给myName一个默认的初始值如下:
package com.fastjrun.example.testng;
import org.testng.annotations.Optional;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;
public class ParameterizedTest1 {
@Test
@Parameters("myName")
public void parameterTest(@Optional("optional") String myName) {
System.out.println("Parameterized value is : " + myName);
}
}
另外就是使用-D命令行参数。
使用注解Parameters配合-D命令行参数
使用-D命令行参数就是让参数以系统参数的方式加载入JVM,该方式支持Java、Maven命令行执行。idea配置-D命令行参数的方式如下:
使用注解DataProvider
我们先写一个业务类PrimeNumberChecker如下所示,该类中只有一个方法validate,检查输入参数primeNumber是否为质数,如果是返回True,如果不是返回False。
src/main/java/com/fastjrun.example/testng/PrimeNumberChecker.java
package com.fastjrun.example.testng;
public class PrimeNumberChecker {
public Boolean validate(final Integer primeNumber) {
for (int i = 2; i < (primeNumber / 2); i++) {
if (primeNumber % i == 0) {
return false;
}
}
return true;
}
}
再写一个测试类PrimeNumberCheckerTest:
- 定义一个方法primeNumbers(), 该方法返回值是一个对象数组,该方法使用了注解DataProvider,其值为test1,方法体中只是简单返回了一个新建的对象数据。
- 新增一个方法 testPrimeNumberChecker(),该方法定义了两个输入参数整型变量inputNumber和布尔变量expectedResult。该方法体调用PrimeNumberChecker的validate方法,来验证inputNumber为质数的结果是否为expectedResult。
- 方法 testPrimeNumberChecker()其实是一个测试用例,该方法使用注解@Test(dataProvider = "test1"),表明该测试用例的dataProvider设置为test1,也就是说在执行该测试用例的时候,会使用primeNumbers()返回的对象数组为该方法的输入参数分别赋值。
src/test/java/com/fastjrun.example/testng/PrimeNumberCheckerTest.java
package com.fastjrun.example.testng;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class PrimeNumberCheckerTest {
private PrimeNumberChecker primeNumberChecker;
public void initialize() {
primeNumberChecker = new PrimeNumberChecker();
}
(name = "test1")
public Object[][] primeNumbers() {
return new Object[][] {{2, true}, {6, false}, {19, true}, {22, false}, {23, true}};
}
// This test will run 4 times since we have 5 parameters defined
(dataProvider = "test1")
public void testPrimeNumberChecker(Integer inputNumber, Boolean expectedResult) {
System.out.println(inputNumber + " " + expectedResult);
Assert.assertEquals(expectedResult, primeNumberChecker.validate(inputNumber));
}
}
在PrimeNumberCheckerTest类编辑窗口选中需要测试的方法名testPrimeNumberChecker,点击鼠标右键弹出下拉菜单后,从中选择“Run testPrimeNumberChecker()”即可执行该单元测试用例。
执行结果如下:
2 true
6 false
19 true
22 false
23 true
===============================================
Default Suite
Total tests run: 5, Passes: 5, Failures: 0, Skips: 0
===============================================
从结果可以看出,该测试用例通过DataProvider一次性注入了5组测试数据。这种特性非常有助于我们在研发过程中采用TDD(测试驱动开发)。关于TDD,并不是本文探讨的内容,这里就不展开了。
TestNG 测试分离实践
基于TestNG参数化注入的方式,我们可以很容易地做到测试代码、数据与环境的相互分离;我们可以统一用文件来管理测试数据,并能灵活地在不同测试环境切换不同的测试数据;我们甚至可以将测试数据文件和测试用例代码进一步分离,各自独立维护,只有在具体执行测试的时候才动态关联。
测试代码和测试数据分离实践
从测试代码中分离数据,将数据集中写在文件中,文件格式可以用txt、properties、yaml、json等,本文会采用properties格式来做示范,因为这个确实比较简单;但具体到真实研发环境,笔者推荐yaml,yaml格式更方便维护和解读,尤其是需要维护大量数据且有中文字符的场景。
先写一个工具类TestUtils:
- 定义一个静态方法initParam(),该方法会读取类路径下的一个properties文件,将文件内容以Properties格式返回,properties文件路径由输入参数proptiesFileInClassPath指定。
- 定义一个静态方法loadParam(),该方法需要三个输入参数。第一个为Properties类型的propParams,另外两个是String类型,分别是测试类名和测试类中的方法。该方法的作用是用来将propParams中匹配测试类和测试方法的测试数据以二维对象数组的类型返回。
src/test/java/com/fastjrun.example/testng/utils/TestUtils.java
package com.fastjrun.example.testng.utils;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
public class TestUtils {
public static Properties initParam(String proptiesFileInClassPath) throws IOException {
Properties properties = new Properties();
InputStream inParam = TestUtils.class.getResourceAsStream(proptiesFileInClassPath);
properties.load(inParam);
assert inParam != null;
inParam.close();
return properties;
}
public static Object[][] loadParam(Properties propParams, String className, String methodName) {
assert propParams != null;
Set<String> keys = propParams.stringPropertyNames();
List<Object[]> valueObjects = new ArrayList<>();
for(String key: keys) {
if (key.startsWith(className.concat(".").concat(methodName).concat("."))) {
String value = propParams.getProperty(key);
String[] params = value.split(",");
Object[] valueObject = new Object[params.length];
for(int i=0;i<params.length;i++){
if(params[i].split(":").length>1){
String type = params[i].split(":")[1];
switch (type){
case "Integer":
valueObject[i]=Integer.parseInt(params[i].split(":")[0]);
break;
case "Boolean":
valueObject[i]=Boolean.parseBoolean(params[i].split(":")[0]);
break;
default:
valueObject[i]=params[i].split(":")[0];
break;
}
}else{
valueObject[i]=params[i];
}
}
valueObjects.add(valueObject);
}
}
Object[][] object = new Object[valueObjects.size()][];
for(int i = 0; i < object.length; ++i) {
object[i] = new Object[valueObjects.get(i).length];
System.arraycopy(valueObjects.get(i), 0, object[i], 0, valueObjects.get(i).length);
}
return object;
}
}
再写一个测试类PrimeNumberCheckerTest2:
- 该类和PrimeNumberCheckerTest只有方法primeNumbers不同。
- 在PrimeNumberCheckerTest2的primeNumbers方法定义中,多了类型为Method的输入参数method,方法体也换成了从classpath下的dev.properties文件获取测试参数。
src/test/java/com/fastjrun.example/testng/PrimeNumberCheckerTest2.java
package com.fastjrun.example.testng;
import com.fastjrun.example.testng.utils.TestUtils;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Properties;
public class PrimeNumberCheckerTest2 {
private PrimeNumberChecker primeNumberChecker;
public void initialize() {
primeNumberChecker = new PrimeNumberChecker();
}
(name = "test1")
public Object[][] primeNumbers(Method method) {
Properties properties;
try {
properties = TestUtils.initParam("/dev.properties");
} catch (IOException e) {
throw new RuntimeException(e);
}
return TestUtils.loadParam(properties, this.getClass().getSimpleName(), method.getName());
}
(dataProvider = "test1")
public void testPrimeNumberChecker(Integer inputNumber, Boolean expectedResult) {
System.out.println(inputNumber + " " + expectedResult);
Assert.assertEquals(expectedResult, primeNumberChecker.validate(inputNumber));
}
}
准备测试数据文件内容
src/test/resources/dev.properties
PrimeNumberCheckerTest2.testPrimeNumberChecker.1=2:Integer,true:Boolean
PrimeNumberCheckerTest2.testPrimeNumberChecker.2=6:Integer,false:Boolean
PrimeNumberCheckerTest2.testPrimeNumberChecker.3=19:Integer,true:Boolean
PrimeNumberCheckerTest2.testPrimeNumberChecker.4=22:Integer,false:Boolean
PrimeNumberCheckerTest2.testPrimeNumberChecker.5=23:Integer,true:Boolean
在PrimeNumberCheckerTest2类编辑窗口选中需要测试的方法名testPrimeNumberChecker,点击鼠标右键弹出下拉菜单后,从中选择“Run testPrimeNumberChecker()”即可执行该单元测试用例。
执行结果如下:
23 true
22 false
19 true
6 false
2 true
===============================================
Default Suite
Total tests run: 5, Passes: 5, Failures: 0, Skips: 0
===============================================
从结果可以看出,该测试用例也可以一次性执行5组测试数据。如果需要调整测试数据,只需要直接修改dev.properties就可以了。
测试数据和测试环境分离实践
写一个测试类PrimeNumberCheckerTest3,该类在PrimeNumberCheckerTest2的基础上做了重构,重构要点如下:
- 新增了类型为Properties的成员变量properties,用来装载从测试数据文件读入的测试数据。
- 读取测试数据文件的逻辑从primeNumbers方法移入了initialize方法,同时使用了注解BeforeClass,确保该方法在JVM加载PrimeNumberCheckerTest3后第一时间执行,并将测试数据装载到成员变量properties。
- initialize方法使用注解Parameters来初始化测试数据文件名,以便后续执行测试用例的时候可以通过-D命令参数来调整具体的测试数据文件。
- primeNumbers方法修改为从成员变量properties匹配测试类和测试方法的测试数据以二维对象数组的类型返回。
src/test/java/com/fastjrun.example/testng/PrimeNumberCheckerTest3.java
package com.fastjrun.example.testng;
import com.fastjrun.example.testng.utils.TestUtils;
import org.testng.Assert;
import org.testng.annotations.*;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Properties;
public class PrimeNumberCheckerTest3 {
private PrimeNumberChecker primeNumberChecker;
private Properties properties= new Properties();
({"envName"})
public void initialize( ("dev") String envName) {
primeNumberChecker = new PrimeNumberChecker();
try {
properties = TestUtils.initParam("/" + envName + ".properties");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
(name = "test1")
public Object[][] primeNumbers(Method method) {
return TestUtils.loadParam(properties, this.getClass().getSimpleName(), method.getName());
}
(dataProvider = "test1")
public void testPrimeNumberChecker(Integer inputNumber, Boolean expectedResult) {
System.out.println(inputNumber + " " + expectedResult);
Assert.assertEquals(expectedResult, primeNumberChecker.validate(inputNumber));
}
}
准备测试数据文件内容,在dev.properties文件里新增一行。
PrimeNumberCheckerTest3.testPrimeNumberChecker.1=2:Integer,true:Boolean
执行PrimeNumberCheckerTest3的testPrimeNumberChecker方法,执行结果如下:
2 true
===============================================
Default Suite
Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
===============================================
显然,我们可以通过-D命令行参数的方式,调整PrimeNumberCheckerTest3的initialize方法的输入参数envName的值,来达到读取类路径下不同properties文件的目的。这一特性对自动化测试非常友好。
测试代码和测试数据维护分离实践
在上一个实践中,我们已经做到将测试数据放在了文件中,甚至做到了用同一套测试用例代码基于不同的测试数据文件执行测试,但这些测试数据文件其实还是和代码在一起维护。虽然我们的开发工程师和测试工程师已经能够基于同一个代码库各自维护测试用例代码和测试数据,但如果有必要,我们还是希望开发工程师和测试工程师能够进一步分离,使得测试代码的维护和测试数据的维护工作也相互可以独立。
比如开发工程师写单元测试代码并用自己准备的测试数据进行测试以达到一个准入标准(这个标准因研发团队可异);测试工程师维护测试数据文件,后续测试工程师通过-D命令行切换成自己准备的测试数据文件来执行,生成测试报告和代码覆盖率报告。
将测试代码和测试数据文件维护分离的方式可以有很多种,本文只介绍一种和Nacos结合使用的方式。Nacos是一个微服务注册和发现平台,同时也能作为配置管理中心使用。Nacos支持直接在控制台维护如properties、json、xml、yaml、html和html格式的配置信息,这里我们就把这些配置信息当做测试数据文件即可。
TestUtils里新增initParamFromNacos方法如下:
public static Properties initParamFromNacos(String path) throws IOException {
Properties properties = new Properties();
HttpURLConnection connection = (HttpURLConnection) new URL(path).openConnection();
int code = connection.getResponseCode();
if (code >= 400) throw new IOException("Server returned error code #" + code);
InputStream inParam = connection.getInputStream();
properties.load(inParam);
assert inParam != null;
inParam.close();
return properties;
}
写一个测试类PrimeNumberCheckerTest4,该类与PrimeNumberCheckerTest3的不同之处只在于:
- initialize方法中读取测试数据文件的逻辑由从类路径下文件路径变更为从Nacos配置中心指定dataId读取。
src/test/java/com/fastjrun.example/testng/PrimeNumberCheckerTest4.java
package com.fastjrun.example.testng;
import com.fastjrun.example.testng.utils.TestUtils;
import org.testng.Assert;
import org.testng.annotations.*;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Properties;
public class PrimeNumberCheckerTest4 {
private PrimeNumberChecker primeNumberChecker;
private Properties properties = new Properties();
({"envName"})
public void initialize( ("dev") String envName) {
primeNumberChecker = new PrimeNumberChecker();
try {
properties = TestUtils.initParamFromNacos("http://192.168.5.10:8848/nacos/v1/cs/configs?dataId=" + envName + ".properties&group=DEFAULT_GROUP");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
(name = "test1")
public Object[][] primeNumbers(Method method) {
return TestUtils.loadParam(properties, this.getClass().getSimpleName(), method.getName());
}
(dataProvider = "test1")
public void testPrimeNumberChecker(Integer inputNumber, Boolean expectedResult) {
System.out.println(inputNumber + " " + expectedResult);
Assert.assertEquals(expectedResult, primeNumberChecker.validate(inputNumber));
}
}
在Nacos控制台维护测试数据:
Nacos控制台输入如下配置信息,也是测试数据。
PrimeNumberCheckerTest4.testPrimeNumberChecker.1=23:Integer,true:Boolean
执行PrimeNumberCheckerTest4的testPrimeNumberChecker方法,执行结果如下:
23 true
===============================================
Default Suite
Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
===============================================
如果需要调整测试数据,只需要在Nacos控制台直接操作dataId,操作完毕后点发布就可以了。
总结
本实践主要介绍了TestNG 参数化注入的三种方式,并在此基础上,介绍了测试代码和测试数据、测试数据和测试环境的分离实践以及一种利用Nacos将测试代码和测试数据维护分离的有效实践。TestNG 的参数化特性有助于研发过程实践TDD(测试驱动开发)和自动化测试,对提高代码和工程质量有积极意义。
配套代码下载链接:https://ost.51cto.com/resource/2082
作者介绍
崔莹峰,51CTO社区编辑,一名70后程序员,拥有10多年工作经验,长期从事 Java 开发,架构设计,容器化等相关工作。精通Java,熟练使用Maven、Jenkins等Devops相关工具链,擅长容器化方案规划、设计和落地。