TestNG + PowerMock 单元测试

发表于:2022-5-25 09:15

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

 作者:心城以北    来源:51CTO博客

分享:
  单元测试(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 的对象一样追踪、设置对象的行为;
  @PrepareForTest 对于静态方法,私有方法,final 方法,在用powermock做单元测试的时候,需要增加注解; 这个注解的作用就是:该注释告诉PowerMock(ito)列出的类将需要在字节码级别上进行操作。
  示例代码
  1、添加 pom.xml 依赖
  以 Spring-Boot 项目为例,首先我们需要添加 TestNG + ProwerMock 依赖依赖如下:
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>${testng.version}</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-api-mockito2</artifactId>
      <version>${powermock.version}</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-module-junit4</artifactId>
      <version>${powermock.version}</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-module-testng</artifactId>
      <version>${powermock.version}</version>
      <scope>test</scope>
  </dependency>

  2、增加单元测试
  增加测试代码:
  import com.test.testng.dto.OrderDto;
  import com.test.testng.dto.UserDto;
  import org.mockito.*;
  import org.powermock.modules.testng.PowerMockTestCase;
  import org.testng.annotations.BeforeMethod;
  import org.testng.annotations.Test;
  import static org.junit.jupiter.api.Assertions.*;
  import static org.mockito.Mockito.when;
  public class OrderServiceTest extends PowerMockTestCase {
      @BeforeMethod
      public void before() {
          MockitoAnnotations.openMocks(this);
      }
      @InjectMocks
      private OrderService orderService;
      @Mock
      private UserService userService;
      // 正常测试
      @Test
      public void testCreateOrder() {
          //1. mock method start
          UserDto userDto = new UserDto();
          userDto.setId(100);
          when(userService.get()).thenReturn(userDto);
          //2. call business method
          OrderDto order = orderService.createOrder(new OrderDto());
          //3. assert
          assertEquals(order.getId(), 100);
      }
      // 异常测试
      @Test
      public void testCreateOrderEx() {
          //1. mock method start
          when(userService.get()).thenThrow(new RuntimeException());
          Exception exception = null;
          try {
              //2. call business method
              orderService.createOrder(new OrderDto());
          } catch (RuntimeException e) {
              exception = e;
          }
          //3. assert
          assertNotNull(exception);
      }
  }

  常用 Mock 方式
  1. Mock 静态方法
  //静态方法
  UserDto dto = new UserDto();
  dto.setId(100000);
  PowerMockito.mockStatic(UserService.class);
  PowerMockito.when(UserService.loginStatic()).thenReturn(dto);
  UserDto userDto = UserService.loginStatic();
  assertEquals(100000, userDto.getId().intValue());

  2. Mock 私有属性
  //字段赋值
  ReflectionTestUtils.setField(orderService, "rateLimit", 99);

  3. Mock 私有方法
  mock 私有方法需要注意需要增加 @PrepareForTest注解。
  // 模拟私有方法
  MemberModifier.stub(MemberMatcher.method(UserService.class, "get1")).toReturn(new UserDto());
  // 测试私有方法
  Method method = PowerMockito.method(UserService.class, "get1", Integer.class);
  Object userDto = method.invoke(userService, 1);
  assertTrue(userDto instanceof UserDto);

  进阶使用
  1. 参数化批量测试
  在测试数据比较多的时候,我们可以通过 @DataProvider 生成数据源,通过 @Test(dataProvider = "xxx") 使用数据, 如下所示:
  import com.test.testng.BaseTest;
  import com.test.testng.dto.UserDto;
  import org.mockito.InjectMocks;
  import org.testng.annotations.DataProvider;
  import org.testng.annotations.Test;
  import static org.testng.Assert.assertFalse;
  import static org.testng.AssertJUnit.assertTrue;
  public class UserServiceTest2 extends BaseTest {
      @InjectMocks
      private UserService userService;
      // 定义数据源
      @DataProvider(name = "test")
      public static Object[][] userList() {
          UserDto dto1 = new UserDto();
          UserDto dto2 = new UserDto();
          dto2.setSex(1);
          UserDto dto3 = new UserDto();
          dto3.setSex(1);
          dto3.setFlag(1);
          UserDto dto4 = new UserDto();
          dto4.setSex(1);
          dto4.setFlag(1);
          dto4.setAge(1);
          return new Object[][] {{dto1, null}, {dto2, null}, {dto3, null}, {dto4, null}};
      }
      // 正确场景
      @Test
      public void testCheckEffectiveUser() {
          UserDto dto = new UserDto();
          dto.setSex(1);
          dto.setFlag(1);
          dto.setAge(18);
          boolean result = userService.checkEffectiveUser(dto);
          assertTrue(result);
      }
      // 错误场景
      @Test(dataProvider = "test")
      public void testCheckEffectiveUser(UserDto dto, Object object) {
          boolean result = userService.checkEffectiveUser(dto);
          assertFalse(result);
      }
  }

  3. 复杂判断保证测试覆盖率
  案例:
  判断有效用户: 年龄大于 18 并且 sex = 1 并且 flag = 1
  public boolean checkEffectiveUser(UserDto dto) {
      // 判断有效用户: 年龄大于 18 并且 sex = 1 并且 flag = 1
      return Objects.equals(dto.getSex(), 1) &&
          Objects.equals(dto.getFlag(), 1) &&
          dto.getAge() != null && dto.getAge() >= 18;
  }

  拆分逻辑。将其转换为最简单的 if ... else 语句。然后增加的单元测试,如下所示:
  public boolean checkEffectiveUser(UserDto dto) {
      if (!Objects.equals(dto.getSex(), 1)) {
          return false;
      }
      if (!Objects.equals(dto.getFlag(), 1)) {
          return false;
      }
      if (dto.getAge() == null) {
          return false;
      }
      if (dto.getAge() < 18) {
          return false;
      }
      return true;
  }

  拆分后我们可以看到,咱们只需要 5 条单元测试就能做到全覆盖。
  public class UserServiceTest extends BaseTest {
      @InjectMocks
      private UserService userService;
      // 覆盖第一个 return 
      @Test
      public void testCheckEffectiveUser_0() {
          UserDto dto =new UserDto();
          boolean result = userService.checkEffectiveUser(dto);
          assertFalse(result);
      }
      // 覆盖第二个 return 
      @Test
      public void testCheckEffectiveUser_1() {
          UserDto dto =new UserDto();
          dto.setSex(1);
          boolean result = userService.checkEffectiveUser(dto);
          assertFalse(result);
      }
      // 覆盖第三个 return 
      @Test
      public void testCheckEffectiveUser_2() {
          UserDto dto =new UserDto();
          dto.setSex(1);
          dto.setFlag(1);
          boolean result = userService.checkEffectiveUser(dto);
          assertFalse(result);
      }
      // 覆盖第四个 return
      @Test
      public void testCheckEffectiveUser_3() {
          UserDto dto =new UserDto();
          dto.setSex(1);
          dto.setFlag(1);
          dto.setAge(1);
          boolean result = userService.checkEffectiveUser(dto);
          assertFalse(result);
      }
      // 覆盖第五个 return
      @Test
      public void testCheckEffectiveUser_4() {
          UserDto dto =new UserDto();
          dto.setSex(1);
          dto.setFlag(1);
          dto.setAge(18);
          boolean result = userService.checkEffectiveUser(dto);
          assertTrue(result);
      }
  }

  单测覆盖率检测检测:
  3. 通过断言校验方法参数
  assert:断言是 java 的一个保留字,用来对程序进行调试,后接逻辑运算表达式,如下:
  int a = 0, b = 1;
  assert a == 0 && b == 0;
  // 使用方法:javac编译源文件,再java -ea class文件名即可。

  在 Spring-Boot 中可以使用 Spring 提供的 Assert 类的方法对前端来的参数进行校验,如:
  // 检查年龄 >= 18 岁
  public boolean checkUserAge(UserDto dto){
      Assert.notNull(dto.getAge(), "用户年龄不能为空");
      Assert.isTrue(dto.getAge() >= 18, "用户年龄不能小于 18 岁");
      return Boolean.TRUE;
  }

  如果是需要转换为,rest api 返回的统一相应消息,我们可以通过:
  @ControllerAdvice
  public class GlobalExceptionHandler {
      @ResponseBody
      @ExceptionHandler(value = IllegalArgumentException.class)
      public Response<String> handleArgError(IllegalArgumentException e){
          return new Response().failure().message(e.getMessage());
      }
  }

  如何设计程序
  在功能模块的设计过程中我们因该遵循一下原则(参考 《软件工程-结构化设计准则》):
  ·模块大小适中
  · 合适的系统调用深度
  · 多扇入、少扇出(增加复用度, 减少依赖程度)
  · 单入口,单出口
  · 模块的作用域,应该在模块内
  · 功能应该可以预测的
  · 高内聚,低耦合
  · 系统分解有层次
  · 较少的数据冗余

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号