TestNG 测试分离实践

发表于:2022-8-08 10:21

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:崔莹峰    来源:51CTO博客

  基于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;
      @BeforeMethod
      public void initialize() {
          primeNumberChecker = new PrimeNumberChecker();
      }
      @DataProvider(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());
      }
      @Test(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();
      @BeforeClass
      @Parameters({"envName"})
      public void initialize( @Optional("dev") String envName) {
          primeNumberChecker = new PrimeNumberChecker();
          try {
              properties = TestUtils.initParam("/" + envName + ".properties");
          } catch (IOException e) {
              throw new RuntimeException(e);
          }
      }
      @DataProvider(name = "test1")
      public Object[][] primeNumbers(Method method) {
          return TestUtils.loadParam(properties, this.getClass().getSimpleName(), method.getName());
      }
      @Test(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();
      @BeforeClass
      @Parameters({"envName"})
      public void initialize(@Optional("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);
          }
      }
      @DataProvider(name = "test1")
      public Object[][] primeNumbers(Method method) {
          return TestUtils.loadParam(properties, this.getClass().getSimpleName(), method.getName());
      }
      @Test(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(测试驱动开发)和自动化测试,对提高代码和工程质量有积极意义。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号