您还未登录! 登录 | 注册 | 帮助  

您的位置: 首页 > 软件测试技术 > 单元测试 > 正文

TestNG + PowerMock 单元测试

发表于:2021-05-06 作者:乌塔卡 来源:运维开发故事

单元测试(Unit Testing),是指对软件或项目中最小可测试单元进行正确性检验的测试工作。单元是人为规定最小可测试的功能模块,可以是一个模块,一个函数或者一个类。单元测试需要与模块开发进行隔离情况下进行测试。

在程序开发完成后,我们往往不能保证程序 100% 的正确,通过单元测试的编写,我们可以通过自动化的测试程序将我们的输入输出程序进行定义,通过断言来 Check 各个 Case 的结果,检测我们的程序。以提高程序的正确性,稳定性,可靠性,节省程序开发时间。我们在项目中主要用到的单元测试框架有 Spring-Boot-Test TestNG、PowerMock 等。

TestNG,即 Testing, Next Generation,下一代测试技术,是一套根据 JUnit 和 NUnit 思想而构建的利用注释来强化测试功能的一个测试框架,即可以用来做单元测试,也可以用来做集成测试。

PowerMock 也是一个单元测试模拟框架,它是在其它单元测试模拟框架的基础上做出的扩展。通过提供定制的类加载器以及一些字节码篡改技巧的应用,PowerMock 现了对静态方法、构造方法、私有方法以及 Final 方法的模拟支持,对静态初始化过程的移除等强大的功能。

常用注解

1. TestNG 注解

  • @BeforeSuite 在该套件的所有测试都运行在注释的方法之前,仅运行一次
  • @AftereSuite 在该套件的所有测试都运行在注释方法之后,仅运行一次
  • @BeforeClass 在调用当前类的第一个测试方法之前运行,注释方法仅运行一次
  • @AftereClass 在调用当前类的第一个测试方法之后运行,注释方法仅运行一次
  • @BeforeMethod 注释方法将在每个测试方法之前运行
  • @AfterMethod 注释方法将在每个测试方法之后运行
  • @BeforeTest 注释的方法将在属于test标签内的类的所有测试方法运行之前运行
  • @AfterTest 注释的方法将在属于test标签内的类的所有测试方法运行之后运行
  • @DataProvider 标记一种方法来提供测试方法的数据。注释方法必须返回一个Object [] [],其中每个Object []可以被分配给测试方法的参数列表。要从该DataProvider接收数据的@Test方法需要使用与此注释名称相等的dataProvider名称
  • @Parameters 描述如何将参数传递给@Test方法 ;适用于 xml 方式的参数化方式传值
  • @Test 将类或方法标记为测试的一部分,此标记若放在类上,则该类所有公共方法都将被作为测试方法

2. PowerMock 注解

  • @Mock 注解实际上是 Mockito.mock() 方法的缩写,我们只在测试类中使用它;
  • @InjectMocks 主动将已存在的 mock 对象注入到 bean 中, 按名称注入, 但注入失败不会抛出异常;
  • @Spy 封装一个真实的对象,以便可以像其他 mock 的对象一样追踪、设置对象的行为;

示例代码

1. 添加 pom.xml 依赖

以 Spring-Boot 项目为例,首先我们需要添加 TestNG + ProwerMock 依赖依赖如下:


  1. <dependency> 
  2.     <groupId>org.springframework.boot</groupId> 
  3.     <artifactId>spring-boot-starter-test</artifactId> 
  4.     <scope>test</scope> 
  5. </dependency> 
  6. <dependency> 
  7.     <groupId>org.testng</groupId> 
  8.     <artifactId>testng</artifactId> 
  9.     <version>${testng.version}</version> 
  10.     <scope>test</scope> 
  11. </dependency> 
  12. <dependency> 
  13.     <groupId>org.powermock</groupId> 
  14.     <artifactId>powermock-api-mockito2</artifactId> 
  15.     <version>${powermock.version}</version> 
  16.     <scope>test</scope> 
  17. </dependency> 
  18. <dependency> 
  19.     <groupId>org.powermock</groupId> 
  20.     <artifactId>powermock-module-junit4</artifactId> 
  21.     <version>${powermock.version}</version> 
  22.     <scope>test</scope> 
  23. </dependency> 
  24. <dependency> 
  25.     <groupId>org.powermock</groupId> 
  26.     <artifactId>powermock-module-testng</artifactId> 
  27.     <version>${powermock.version}</version> 
  28.     <scope>test</scope> 
  29. </dependency> 

2. 增加单元测试

增加测试代码


  1. import com.test.testng.dto.OrderDto; 
  2. import com.test.testng.dto.UserDto; 
  3. import org.mockito.*; 
  4. import org.powermock.modules.testng.PowerMockTestCase; 
  5. import org.testng.annotations.BeforeMethod; 
  6. import org.testng.annotations.Test; 
  7. import static org.junit.jupiter.api.Assertions.*; 
  8. import static org.mockito.Mockito.when; 
  9. public class OrderServiceTest extends PowerMockTestCase { 
  10.     @BeforeMethod 
  11.     public void before() { 
  12.         MockitoAnnotations.openMocks(this); 
  13.     } 
  14.     @InjectMocks 
  15.     private OrderService orderService; 
  16.     @Mock 
  17.     private UserService userService; 
  18.     // 正常测试 
  19.     @Test 
  20.     public void testCreateOrder() { 
  21.         //1. mock method start 
  22.         UserDto userDto = new UserDto(); 
  23.         userDto.setId(100); 
  24.         when(userService.get()).thenReturn(userDto); 
  25.         //2. call business method 
  26.         OrderDto order = orderService.createOrder(new OrderDto()); 
  27.         //3. assert 
  28.         assertEquals(order.getId(), 100); 
  29.     } 
  30.     // 异常测试 
  31.     @Test 
  32.     public void testCreateOrderEx() { 
  33.         //1. mock method start 
  34.         when(userService.get()).thenThrow(new RuntimeException()); 
  35.         Exception exception = null; 
  36.         try { 
  37.             //2. call business method 
  38.             orderService.createOrder(new OrderDto()); 
  39.         } catch (RuntimeException e) { 
  40.             exception = e; 
  41.         } 
  42.         //3. assert 
  43.         assertNotNull(exception); 
  44.     } 

常用 Mock 方式

1. Mock 静态方法


  1. //静态方法 
  2. UserDto dto = new UserDto(); 
  3. dto.setId(100000); 
  4. PowerMockito.mockStatic(UserService.class); 
  5. PowerMockito.when(UserService.loginStatic()).thenReturn(dto); 
  6. UserDto userDto = UserService.loginStatic(); 
  7. assertEquals(100000, userDto.getId().intValue()); 

2. Mock 私有属性


  1. //字段赋值 
  2. ReflectionTestUtils.setField(orderService, "rateLimit", 99); 

3. Mock 私有方法


  1. // 模拟私有方法 
  2. MemberModifier.stub(MemberMatcher.method(UserService.class, "get1")).toReturn(new UserDto()); 
  3. // 测试私有方法 
  4. Method method = PowerMockito.method(UserService.class, "get1", Integer.class); 
  5. Object userDto = method.invoke(userService, 1); 
  6. assertTrue(userDto instanceof UserDto); 

进阶使用

1. 参数化批量测试

在测试数据比较多的时候,我们可以通过 @DataProvider 生成数据源,通过 @Test(dataProvider = "xxx") 使用数据, 如下所示:


  1. import com.test.testng.BaseTest; 
  2. import com.test.testng.dto.UserDto; 
  3. import org.mockito.InjectMocks; 
  4. import org.testng.annotations.DataProvider; 
  5. import org.testng.annotations.Test; 
  6. import static org.testng.Assert.assertFalse; 
  7. import static org.testng.AssertJUnit.assertTrue; 
  8. public class UserServiceTest2 extends BaseTest { 
  9.     @InjectMocks 
  10.     private UserService userService; 
  11.     // 定义数据源 
  12.     @DataProvider(name = "test") 
  13.     public static Object[][] userList() { 
  14.         UserDto dto1 = new UserDto(); 
  15.         UserDto dto2 = new UserDto(); 
  16.         dto2.setSex(1); 
  17.         UserDto dto3 = new UserDto(); 
  18.         dto3.setSex(1); 
  19.         dto3.setFlag(1); 
  20.         UserDto dto4 = new UserDto(); 
  21.         dto4.setSex(1); 
  22.         dto4.setFlag(1); 
  23.         dto4.setAge(1); 
  24.         return new Object[][] {{dto1, null}, {dto2, null}, {dto3, null}, {dto4, null}}; 
  25.     } 
  26.     // 正确场景 
  27.     @Test 
  28.     public void testCheckEffectiveUser() { 
  29.         UserDto dto = new UserDto(); 
  30.         dto.setSex(1); 
  31.         dto.setFlag(1); 
  32.         dto.setAge(18); 
  33.         boolean result = userService.checkEffectiveUser(dto); 
  34.         assertTrue(result); 
  35.     } 
  36.  
  37.     // 错误场景 
  38.     @Test(dataProvider = "test") 
  39.     public void testCheckEffectiveUser(UserDto dto, Object object) { 
  40.         boolean result = userService.checkEffectiveUser(dto); 
  41.         assertFalse(result); 
  42.     } 
  43.  

2. 复杂判断保证测试覆盖率

案例:

1.判断有效用户: 年龄大于 18 并且 sex = 1 并且 flag = 1


  1. public boolean checkEffectiveUser(UserDto dto) { 
  2.     // 判断有效用户: 年龄大于 18 并且 sex = 1 并且 flag = 1 
  3.     return Objects.equals(dto.getSex(), 1) && 
  4.         Objects.equals(dto.getFlag(), 1) && 
  5.         dto.getAge() != null && dto.getAge() >= 18; 

2.拆分逻辑。将其转换为最简单的 if ... else 语句。然后增加的单元测试,如下所示:


  1. public boolean checkEffectiveUser(UserDto dto) { 
  2.     if (!Objects.equals(dto.getSex(), 1)) { 
  3.         return false; 
  4.     } 
  5.     if (!Objects.equals(dto.getFlag(), 1)) { 
  6.         return false; 
  7.     } 
  8.     if (dto.getAge() == null) { 
  9.         return false; 
  10.     } 
  11.     if (dto.getAge() < 18) { 
  12.         return false; 
  13.     } 
  14.     return true; 

3.拆分后我们可以看到,咱们只需要 5 条单元测试就能做到全覆盖。


  1. public class UserServiceTest extends BaseTest { 
  2.     @InjectMocks 
  3.     private UserService userService; 
  4.     // 覆盖第一个 return  
  5.     @Test 
  6.     public void testCheckEffectiveUser_0() { 
  7.         UserDto dto =new UserDto(); 
  8.         boolean result = userService.checkEffectiveUser(dto); 
  9.         assertFalse(result); 
  10.     } 
  11.     // 覆盖第二个 return  
  12.     @Test 
  13.     public void testCheckEffectiveUser_1() { 
  14.         UserDto dto =new UserDto(); 
  15.         dto.setSex(1); 
  16.         boolean result = userService.checkEffectiveUser(dto); 
  17.         assertFalse(result); 
  18.     } 
  19.     // 覆盖第三个 return  
  20.     @Test 
  21.     public void testCheckEffectiveUser_2() { 
  22.         UserDto dto =new UserDto(); 
  23.         dto.setSex(1); 
  24.         dto.setFlag(1); 
  25.         boolean result = userService.checkEffectiveUser(dto); 
  26.         assertFalse(result); 
  27.     } 
  28.     // 覆盖第四个 return 
  29.     @Test 
  30.     public void testCheckEffectiveUser_3() { 
  31.         UserDto dto =new UserDto(); 
  32.         dto.setSex(1); 
  33.         dto.setFlag(1); 
  34.         dto.setAge(1); 
  35.         boolean result = userService.checkEffectiveUser(dto); 
  36.         assertFalse(result); 
  37.     } 
  38.     // 覆盖第五个 return 
  39.     @Test 
  40.     public void testCheckEffectiveUser_4() { 
  41.         UserDto dto =new UserDto(); 
  42.         dto.setSex(1); 
  43.         dto.setFlag(1); 
  44.         dto.setAge(18); 
  45.         boolean result = userService.checkEffectiveUser(dto); 
  46.         assertTrue(result); 
  47.     } 

4.单测覆盖率检测检测

3. 通过断言校验方法参数

1.assert:断言是 java 的一个保留字,用来对程序进行调试,后接逻辑运算表达式,如下:


  1. int a = 0, b = 1; 
  2. assert a == 0 && b == 0; 
  3. // 使用方法:javac编译源文件,再 java -ea class文件名即可。 

2.在 Spring-Boot 中可以使用 Spring 提供的 Assert 类的方法对前端来的参数进行校验,如:


  1. // 检查年龄 >= 18 岁 
  2. public boolean checkUserAge(UserDto dto){ 
  3.     Assert.notNull(dto.getAge(), "用户年龄不能为空"); 
  4.     Assert.isTrue(dto.getAge() >= 18, "用户年龄不能小于 18 岁"); 
  5.     return Boolean.TRUE; 

3.如果是需要转换为,rest api 返回的统一相应消息,我们可以通过:


  1. @ControllerAdvice 
  2. public class GlobalExceptionHandler { 
  3.     @ResponseBody 
  4.     @ExceptionHandler(value = IllegalArgumentException.class) 
  5.     public Response<String> handleArgError(IllegalArgumentException e){ 
  6.         return new Response().failure().message(e.getMessage()); 
  7.     } 

总结

原则上来讲,在功能模块的设计过程中我们应该遵循一下原则(参考 《软件工程-结构化设计准则》):

  1. 模块大小适中
  2. 合适的系统调用深度
  3. 多扇入、少扇出(增加复用度, 减少依赖程度)
  4. 单入口,单出口
  5. 模块的作用域,应该在模块内
  6. 功能应该可以预测的
  7. 高内聚,低耦合
  8. 系统分解有层次
  9. 较少的数据冗余

参考文档

https://testng.org/doc/

https://github.com/powermock/powermock

https://www.netconcepts.cn/detail-41004.html