Java单元测试用例的编写,有什么技巧?(三)

发表于:2021-5-10 09:32

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

 作者:阿里技术    来源:知乎

分享:
  6. verify语句
  验证是确认在模拟过程中,被测试方法是否已按预期方式与其任何依赖方法进行了交互。
  格式:
  Mockito.verify(mockObject[,times(int)]).someMethod(somgArgs);
  用途:
  用于模拟对象方法,直接返回期望的值、异常、应答,或调用真实的方法,无需执行原始方法。
  案例:
  6.1. 验证调用方法
  public class ListTest {
      @Test
      public void testGet() {
          List<Integer> mockList = PowerMockito.mock(List.class);
          PowerMockito.doNothing().when(mockList).clear();
          mockList.clear();
          Mockito.verify(mockList).clear();
      }
  }
  6.2. 验证调用次数
  public class ListTest {
      @Test
      public void testGet() {
          List<Integer> mockList = PowerMockito.mock(List.class);
          PowerMockito.doNothing().when(mockList).clear();
          mockList.clear();
          Mockito.verify(mockList, Mockito.times(1)).clear();
      }
  }
  除times外,Mockito还支持atLeastOnce、atLeast、only、atMostOnce、atMost等次数验证器。
  6.3. 验证调用顺序
  public class ListTest {
      @Test
      public void testAdd() {
             List<Integer> mockedList = PowerMockito.mock(List.class);
          PowerMockito.doReturn(true).when(mockedList).add(Mockito.anyInt());
          mockedList.add(1);
          mockedList.add(2);
          mockedList.add(3);
          InOrder inOrder = Mockito.inOrder(mockedList);
          inOrder.verify(mockedList).add(1);
          inOrder.verify(mockedList).add(2);
          inOrder.verify(mockedList).add(3);
      }
  }
  6.4. 验证调用参数
  public class ListTest {
      @Test
      public void testArgumentCaptor() {
          Integer[] expecteds = new Integer[] {1, 2, 3};
          List<Integer> mockedList = PowerMockito.mock(List.class);
          PowerMockito.doReturn(true).when(mockedList).add(Mockito.anyInt());
          for (Integer expected : expecteds) {
              mockedList.add(expected);
          }
          ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);
          Mockito.verify(mockedList, Mockito.times(3)).add(argumentCaptor.capture());
          Integer[] actuals = argumentCaptor.getAllValues().toArray(new Integer[0]);
          Assert.assertArrayEquals("返回值不相等", expecteds, actuals);
      }
  }
  6.5. 确保验证完毕
  Mockito提供Mockito.verifyNoMoreInteractions方法,在所有验证方法之后可以使用此方法,以确保所有调用都得到验证。如果模拟对象上存在任何未验证的调用,将会抛出NoInteractionsWanted异常。
public class ListTest {
      @Test
      public void testVerifyNoMoreInteractions() {
          List<Integer> mockedList = PowerMockito.mock(List.class);
          Mockito.verifyNoMoreInteractions(mockedList); // 执行正常
          mockedList.isEmpty();
          Mockito.verifyNoMoreInteractions(mockedList); // 抛出异常
      }
  }
  备注:Mockito.verifyZeroInteractions方法与Mockito.verifyNoMoreInteractions方法相同,但是目前已经被废弃。
  6.6. 验证静态方法
  Mockito没有静态方法的验证方法,但是PowerMock提供这方面的支持。
请  @RunWith(PowerMockRunner.class)
  @PrepareForTest({StringUtils.class})
  public class StringUtilsTest {
      @Test
      public void testVerifyStatic() {
          PowerMockito.mockStatic(StringUtils.class);
          String expected = "abc";
          StringUtils.isEmpty(expected);
          PowerMockito.verifyStatic(StringUtils.class);
          ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
          StringUtils.isEmpty(argumentCaptor.capture());
          Assert.assertEquals("参数不相等", argumentCaptor.getValue(), expected);
      }
  }
  7. 私有属性
  7.1. ReflectionTestUtils.setField方法
  在用原生JUnit进行单元测试时,我们一般采用ReflectionTestUtils.setField方法设置私有属性值。
请  @Service
  public class UserService {
      @Value("${system.userLimit}")
      private Long userLimit;
      public Long getUserLimit() {
          return userLimit;
      }
  }
  public class UserServiceTest {
      @Autowired
      private UserService userService;
      @Test
      public void testGetUserLimit() {
          Long expected = 1000L;
          ReflectionTestUtils.setField(userService, "userLimit", expected);
          Long actual = userService.getUserLimit();
          Assert.assertEquals("返回值不相等", expected, actual);
      }
  }
  注意:在测试类中,UserService实例是通过@Autowired注解加载的,如果该实例已经被动态代理,ReflectionTestUtils.setField方法设置的是代理实例,从而导致设置不生效。
  7.2. Whitebox.setInternalState方法
  现在使用PowerMock进行单元测试时,可以采用Whitebox.setInternalState方法设置私有属性值。
  @Service
  public class UserService {
      @Value("${system.userLimit}")
      private Long userLimit;
      public Long getUserLimit() {
          return userLimit;
      }
  }
  @RunWith(PowerMockRunner.class)
  public class UserServiceTest {
      @InjectMocks
      private UserService userService;
      @Test
      public void testGetUserLimit() {
          Long expected = 1000L;
          Whitebox.setInternalState(userService, "userLimit", expected);
          Long actual = userService.getUserLimit();
          Assert.assertEquals("返回值不相等", expected, actual);
      }
  }
  注意:需要加上注解@RunWith(PowerMockRunner.class)。
  8. 私有方法
  8.1. 模拟私有方法
  8.1.1. 通过when实现
  public class UserService {
      private Long superUserId;
      public boolean isNotSuperUser(Long userId) {
          return !isSuperUser(userId);
      }
      private boolean isSuperUser(Long userId) {
          return Objects.equals(userId, superUserId);
      }
  }
  @RunWith(PowerMockRunner.class)
  @PrepareForTest({UserService.class})
  public class UserServiceTest {
      @Test
      public void testIsNotSuperUser() throws Exception {
          Long userId = 1L;
          boolean expected = false;
          UserService userService = PowerMockito.spy(new UserService());
          PowerMockito.when(userService, "isSuperUser", userId).thenReturn(!expected);
          boolean actual = userService.isNotSuperUser(userId);
          Assert.assertEquals("返回值不相等", expected, actual);
      }
  }
  8.1.2. 通过stub实现
  通过模拟方法stub(存根),也可以实现模拟私有方法。但是,只能模拟整个方法的返回值,而不能模拟指定参数的返回值。
  @RunWith(PowerMockRunner.class)
  @PrepareForTest({UserService.class})
  public class UserServiceTest {
      @Test
      public void testIsNotSuperUser() throws Exception {
          Long userId = 1L;
          boolean expected = false;
          UserService userService = PowerMockito.spy(new UserService());
          PowerMockito.stub(PowerMockito.method(UserService.class, "isSuperUser", Long.class)).toReturn(!expected);
          boolean actual = userService.isNotSuperUser(userId);
          Assert.assertEquals("返回值不相等", expected, actual;
      }
  }
  8.3. 测试私有方法
  @RunWith(PowerMockRunner.class)
  public class UserServiceTest9 {
      @Test
      public void testIsSuperUser() throws Exception {
          Long userId = 1L;
          boolean expected = false;
          UserService userService = new UserService();
          Method method = PowerMockito.method(UserService.class, "isSuperUser", Long.class);
          Object actual = method.invoke(userService, userId);
          Assert.assertEquals("返回值不相等", expected, actual);
      }
  }
  8.4. 验证私有方法
  @RunWith(PowerMockRunner.class)
  @PrepareForTest({UserService.class})
  public class UserServiceTest10 {
      @Test
      public void testIsNotSuperUser() throws Exception {
          Long userId = 1L;
          boolean expected = false;
          UserService userService = PowerMockito.spy(new UserService());
          PowerMockito.when(userService, "isSuperUser", userId).thenReturn(!expected);
          boolean actual = userService.isNotSuperUser(userId);
          PowerMockito.verifyPrivate(userService).invoke("isSuperUser", userId);
          Assert.assertEquals("返回值不相等", expected, actual);
      }
  }
  这里,也可以用Method那套方法进行模拟和验证方法。
  9. 主要注解
  PowerMock为了更好地支持SpringMVC/SpringBoot项目,提供了一系列的注解,大大地简化了测试代码。
  9.1. @RunWith注解
  @RunWith(PowerMockRunner.class)
  指定JUnit 使用 PowerMock 框架中的单元测试运行器。
  9.2. @PrepareForTest注解
  @PrepareForTest({ TargetClass.class })
  当需要模拟final类、final方法或静态方法时,需要添加@PrepareForTest注解,并指定方法所在的类。如果需要指定多个类,在{}中添加多个类并用逗号隔开即可。
  9.3. @Mock注解
  @Mock注解创建了一个全部Mock的实例,所有属性和方法全被置空(0或者null)。
  9.4. @Spy注解
  @Spy注解创建了一个没有Mock的实例,所有成员方法都会按照原方法的逻辑执行,直到被Mock返回某个具体的值为止。
  注意:@Spy注解的变量需要被初始化,否则执行时会抛出异常。
  9.5. @InjectMocks注解
  @InjectMocks注解创建一个实例,这个实例可以调用真实代码的方法,其余用@Mock或@Spy注解创建的实例将被注入到用该实例中。
  @Service
  public class UserService {
      @Autowired
      private UserDAO userDAO;
      public void modifyUser(UserVO userVO) {
          UserDO userDO = new UserDO();
          BeanUtils.copyProperties(userVO, userDO);
          userDAO.modify(userDO);
      }
  }
  @RunWith(PowerMockRunner.class)
  public class UserServiceTest {
      @Mock
      private UserDAO userDAO;
      @InjectMocks
      private UserService userService;
      @Test
      public void testCreateUser() {
          UserVO userVO = new UserVO();
          userVO.setId(1L);
          userVO.setName("changyi");
          userVO.setDesc("test user");
          userService.modifyUser(userVO);
          ArgumentCaptor<UserDO> argumentCaptor = ArgumentCaptor.forClass(UserDO.class);
          Mockito.verify(userDAO).modify(argumentCaptor.capture());
          UserDO userDO = argumentCaptor.getValue();
          Assert.assertNotNull("用户实例为空", userDO);
          Assert.assertEquals("用户标识不相等", userVO.getId(), userDO.getId());
          Assert.assertEquals("用户名称不相等", userVO.getName(), userDO.getName());
          Assert.assertEquals("用户描述不相等", userVO.getDesc(), userDO.getDesc());
      }
  }
  9.6. @Captor注解
  @Captor注解在字段级别创建参数捕获器。但是,在测试方法启动前,必须调用MockitoAnnotations.openMocks(this)进行初始化。@Service
  public class UserService {
      @Autowired
      private UserDAO userDAO;
      public void modifyUser(UserVO userVO) {
          UserDO userDO = new UserDO();
          BeanUtils.copyProperties(userVO, userDO);
          userDAO.modify(userDO);
      }
  }
  @RunWith(PowerMockRunner.class)
  public class UserServiceTest {
      @Mock
      private UserDAO userDAO;
      @InjectMocks
      private UserService userService;
      @Captor
      private ArgumentCaptor<UserDO> argumentCaptor;
      @Before
      public void beforeTest() {
          MockitoAnnotations.openMocks(this);
      }
      @Test
      public void testCreateUser() {
          UserVO userVO = new UserVO();
          userVO.setId(1L);
          userVO.setName("changyi");
          userVO.setDesc("test user");
          userService.modifyUser(userVO);
          Mockito.verify(userDAO).modify(argumentCaptor.capture());
          UserDO userDO = argumentCaptor.getValue();
          Assert.assertNotNull("用户实例为空", userDO);
          Assert.assertEquals("用户标识不相等", userVO.getId(), userDO.getId());
          Assert.assertEquals("用户名称不相等", userVO.getName(), userDO.getName());
          Assert.assertEquals("用户描述不相等", userVO.getDesc(), userDO.getDesc());
      }
  }
  9.7. @PowerMockIgnore注解
  为了解决使用PowerMock后,提示ClassLoader错误。
  10. 相关观点
  10.1. 为什么要使用Mock?
  根据网络相关资料,总结观点如下:
  Mock可以用来解除外部服务依赖,从而保证了测试用例的独立性。
  现在的互联网软件系统,通常采用了分布式部署的微服务,为了单元测试某一服务而准备其它服务,存在极大的依耐性和不可行性。
  Mock可以减少全链路测试数据准备,从而提高了编写测试用例的速度。
  传统的集成测试,需要准备全链路的测试数据,可能某些环节并不是你所熟悉的。最后,耗费了大量的时间和经历,并不一定得到你想要的结果。现在的单元测试,只需要模拟上游的输入数据,并验证给下游的输出数据,编写测试用例并进行测试的速度可以提高很多倍。
  Mock可以模拟一些非正常的流程,从而保证了测试用例的代码覆盖率。
  根据单元测试的BCDE原则,需要进行边界值测试(Border)和强制错误信息输入(Error),这样有助于覆盖整个代码逻辑。在实际系统中,很难去构造这些边界值,也能难去触发这些错误信息。而Mock从根本上解决了这个问题:想要什么样的边界值,只需要进行Mock;想要什么样的错误信息,也只需要进行Mock。
  Mock可以不用加载项目环境配置,从而保证了测试用例的执行速度。
  在进行集成测试时,我们需要加载项目的所有环境配置,启动项目依赖的所有服务接口。往往执行一个测试用例,需要几分钟乃至几十分钟。采用Mock实现的测试用例,不用加载项目环境配置,也不依赖其它服务接口,执行速度往往在几秒之内,大大地提高了单元测试的执行速度。
  10.2. 单元测试与集成测试的区别
  在实际工作中,不少同学用集成测试代替了单元测试,或者认为集成测试就是单元测试。这里,总结为了单元测试与集成测试的区别:
  测试对象不同
  单元测试对象是实现了具体功能的程序单元,集成测试对象是概要设计规划中的模块及模块间的组合。
  测试方法不同
  单元测试中的主要方法是基于代码的白盒测试,集成测试中主要使用基于功能的黑盒测试
  测试时间不同
  集成测试要晚于单元测试。
  测试内容不同
  单元测试主要是模块内程序的逻辑、功能、参数传递、变量引用、出错处理及需求和设计中具体要求方面的测试;而集成测试主要验证各个接口、接口之间的数据传递关系,及模块组合后能否达到预期效果。

      本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
51Testing“十佳作者”计划,投稿不只有稿费!

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号