在你想要了解如何编写单元测试的时候,想必对于单元测试的概念和重要性都已经有了比较充足的了解。
本篇不讲概念、不说废话,仅展示一些单元测试编写的技巧和方法,希望能够帮助大家实际用起来。
计划做一个单元测试的专栏,后续补充一些关于覆盖率检查、编写效率提升、重复执行、自动执行相关的文章。
全示例代码,无需关注、无需开会员,需要的可以持续关注。
一、引言
单元测试,编程中熟知的有效工具,擅长预先发现错误、全面覆盖异常场景并助力代码重构。若你的项目面临复杂业务难以测试或旧代码维护难题,不妨尝试单元测试,它将为这些问题带来有效解决方案。当单元测试覆盖率达到一定指标后,将显著提升代码质量和项目的可持续发展能力。
二、测试框架选择
JUnit优点: 提供了基础的单元测试能力, 可以通过JUnit提供的注解、断言来组织和运行测试用例。例如:@Test、@BeforeEach、@AfterEach、@BeforeAll、@AfterAll、@ExtendWith等;不足: 不支持对被测对象、依赖、参数、返回值的模拟能力,不能对方法调用进行验证。
Mockito优点: 提供了对测试对象、依赖对象、方法参数、方法返回值进行模拟的能力,能够对返回值、方法调用进行验证;不足: 需要依赖JUnit,不能对私有方法、final方法、构造方法进行测试。
PowerMock优点: 提供对私有方法、final方法、构造方法进行测试的能力;不足: 需要与JUnit、Mockito进行配合使用,版本太老存在不兼容问题。
三、基本用法
被测试代码示例
@ServicepublicclassOrderService{@AutowiredprivateProductService productService;publicvoidsetProductService(ProductService productService){this.productService = productService;}publicOrdergetById(Long id){returnnewOrder().setId(id).setTotalAmount(BigDecimal.valueOf(15).multiply(BigDecimal.valueOf(id))).setProduct(productService.getById(id));}publicvoiddelete(Long id){System.out.println("Order delete called.");
productService.delete(id);}privateStringgetOrderUser(Long id){return"Order User: "+ id;}publicstaticStringstaticMethod(){return"Order static method called.";}}@ServicepublicclassProductService{publicProductgetById(Long id){returnnewProduct().setId(id).setName("Product: "+ id);}publicvoiddelete(Long id){System.out.println("Product delete called.");}publicstaticStringstaticMethod(){return"Static method called.";}}@Data@NoArgsConstructor@Accessors(chain =true)publicclassOrder{privateLong id;privateBigDecimal totalAmount;privateProduct product;privateString user;publicOrder(Long id){this.id = id;this.totalAmount =BigDecimal.valueOf(10).multiply(BigDecimal.valueOf(id));}}@Data@Accessors(chain =true)publicclassProduct{privateLong id;privateString name;}// 使用Mockito扩展@ExtendWith(MockitoExtension.class)publicclassOrderServiceTest{// 模拟掉ProductService所有方法,仅测试OrderService@MockprivateProductService productService;@Spy@InjectMocksprivateOrderService orderService;}
- 构造测试对象 a. 实例化创建
OrderService orderService =newOrderService();
b. 依赖注入@AutowiredprivateOrderService orderService;
c. Mock创建// Mock注解创建@MockprivateOrderService orderService;// Mock方法创建OrderService orderService =Mockito.mock(OrderService.class);
d. Spy创建// Spy注解创建@SpyprivateOrderService orderService;// Spy方法创建OrderService orderService =Mockito.spy(OrderService.class);
- 构造依赖对象 a. 依赖注入
@AutowiredprivateOrderService orderService;
b. 手动设置OrderService orderService =newOrderService();// new ProductService也可以替换为Mock、Spy生成的对象orderService.setProductService(newProductService());
c. InjectMock注入// 需要注入的对象,此处可以用@Spy替换@Mock,不过需注意@Spy和@Mock对象方法调用上的区别@MockprivateProductService productService;// 依赖OrderService的被测试对象@InjectMocksprivateOrderService orderService;
- 模拟方法调用。以下示例可通过
Junit+Mockito
进行实现。 a. 返回特定值// getById本应返回id=1的Product,此处模拟返回id=2的ProductMockito.doReturn(newProduct().setId(2L)).when(productService).getById(1L);Assertions.assertEquals(2L, productService.getById(1L).getId(),"应该返回Stub的值为2L!");// 验证productService.getById(1L)被调用过Mockito.verify(productService).getById(Mockito.anyLong());// 验证productService.delete(1L)未被调用过Mockito.verify(productService,Mockito.never()).delete(1L);// 验证productService.getById(1L)被调用过1次Mockito.verify(productService,Mockito.times(1)).getById(1L);
b. 返回动态值。前三次
调用时依次返回Id为1、2、3的Product对象,第4次及之后
只返回Id为3的Product对象。Mockito.doAnswer(invocation ->{// 获取传参Long productId = invocation.getArgument(0);// 构造一个自定义的Product对象returnnewProduct().setId(productId);}).when(productService).getById(Mockito.anyLong());// 断言Assertions.assertEquals(3L, productService.getById(3L).getId(),"传3L应返回3L");Assertions.assertEquals(2L, productService.getById(2L).getId(),"传2L应返回2L");Assertions.assertEquals(1L, productService.getById(1L).getId(),"传1L应返回1L");// 验证方法被调用了三次Mockito.verify(productService,Mockito.times(3)).getById(Mockito.anyLong());
c. 每次返回不一样的值。// 设置3个模拟返回值Mockito.doReturn(newProduct().setId(1L),newProduct().setId(2L),newProduct().setId(3L)).when(productService).getById(1L);// 断言// 第一次调用Assertions.assertEquals(1L, productService.getById(1L).getId(),"第一次调用返回1L");// 第二次调用Assertions.assertEquals(2L, productService.getById(1L).getId(),"第二次调用返回2L");// 第三次调用Assertions.assertEquals(3L, productService.getById(1L).getId(),"第三次调用返回2L");// 第四次调用Assertions.assertEquals(3L, productService.getById(1L).getId(),"第四次调用返回2L");// 第五次调用Assertions.assertEquals(3L, productService.getById(1L).getId(),"第五次调用返回2L");
d. 模拟抛出异常。// 调用getById方法时抛出异常Mockito.doThrow(newInvalidParamException("Invalid Param")).when(productService).getById(1L);// 断言,方法调用抛出InvalidParamException异常InvalidParamException exception =Assertions.assertThrows(InvalidParamException.class,()-> productService.getById(1L),"此处应抛出异常!");// 此处可以拿到异常,对异常信息再次校验Assertions.assertEquals("Invalid Param", exception.getMessage(),"异常信息应为Invalid Param");
e. 模拟调用静态方法。自Mockito 3以后,静态方法的调用不再需要依赖PowerMock。// 创建模拟对象try(MockedStatic<ProductService> mockedStatic =Mockito.mockStatic(ProductService.class)){// 利用Stub进行返回值模拟 mockedStatic.when(ProductService::staticMethod).thenReturn("Mock static method called.");// 调用静态方法String staticMethodReturn =ProductService.staticMethod();// 断言// 应该返回模拟结果Assertions.assertEquals("Mock static method called.", staticMethodReturn,"应当返回模拟结果!");// 不应该返回真实结果Assertions.assertNotEquals("Static method called.", staticMethodReturn,"不应该返回实际结果!");}
f. 调用Mock对象真实方法。// 模拟返回值Mockito.doReturn(newProduct().setId(2L)).when(productService).getById(1L);// 断言// Mock生效。返回Mock值Assertions.assertEquals(2L, productService.getById(1L).getId(),"调用模拟方法返回2L");// 验证方法被调用过至少一次Mockito.verify(productService,Mockito.atLeastOnce()).getById(1L);// 调用实际方法Mockito.doCallRealMethod().when(productService).getById(1L);// 断言// 调用真实方法,返回实际值Assertions.assertEquals(1L, productService.getById(1L).getId(),"调用真实方法返回1L");// 验证方法调用次数增加一次,为2次Mockito.verify(productService,Mockito.times(2)).getById(1L);
- 特殊方法模拟。借助PowerMock对私有方法、静态方法进行测试。 测试代码示例
// 使用PowerMockRunner进行测试@RunWith(PowerMockRunner.class)// 将静态方法、私有方法的类在这里声明@PrepareForTest({OrderService.class})publicclassOrderServiceTest{@MockprivateProductService productService;@InjectMocksprivateOrderService orderService;}
a. 模拟私有方法的调用Long id =1L;// 使用PowerMock模拟orderService方法的执行orderService =PowerMockito.spy(orderService);// 将orderService的getOrderUser方法返回值改为“Mocked order User:”String mockedReturn ="Mocked order User: "+ id;PowerMockito.doReturn(mockedReturn).when(orderService,"getOrderUser",Mockito.any());// 调用orderService的getById方法,getById调用静态方法getOrderUserOrder order = orderService.getById(id);// 验证私有方法被调用过PowerMockito.verifyPrivate(orderService).invoke("getOrderUser",Mockito.any());// 验证orderService的getOrderUser方法被调用了1次PowerMockito.verifyPrivate(orderService,Mockito.times(1)).invoke("getOrderUser",Mockito.any());// 验证order的user值为Mock的返回值Assertions.assertEquals(mockedReturn, order.getUser());
b. 真实调用私有方法Long id =1L;// 使用PowerMock模拟orderService方法的执行orderService =PowerMockito.spy(orderService);// 调用orderService的私有方法getOrderUserString orderUser =Whitebox.invokeMethod(orderService,"getOrderUser", id);// 验证私有方法被调用过PowerMockito.verifyPrivate(orderService).invoke("getOrderUser",Mockito.any());// 验证orderService的getOrderUser方法被调用了1次PowerMockito.verifyPrivate(orderService,Mockito.times(1)).invoke("getOrderUser",Mockito.any());// 验证order的user值为“Order User:”,未被修改Assertions.assertEquals("Order User: "+ id, orderUser);
c. 模拟静态方法的调用。这里写法有些怪,verify方法后必须再跟一次静态方法调用PowerMockito.mockStatic(OrderService.class);// 将orderService的staticMethod方法返回值改为“Mocked order static method called.”String mockedReturn ="Mocked order static method called.";PowerMockito.when(OrderService.staticMethod()).thenReturn(mockedReturn);// 调用静态方法String staticResult =OrderService.staticMethod();// 验证静态方法被调用过。这里写法有些怪,verify方法后必须跟一次方法调用PowerMockito.verifyStatic(OrderService.class);OrderService.staticMethod();// 验证静态方法被调用过1次。这里写法有些怪,verify方法后必须跟一次方法调用PowerMockito.verifyStatic(OrderService.class,Mockito.times(1));OrderService.staticMethod();// 验证staticResult值为Mock的返回值Assertions.assertEquals(mockedReturn, staticResult);
d. 模拟构造方法的调用// 将Order对象构造方法的返回值改为2LOrder mockOrder =newOrder(2L);PowerMockito.whenNew(Order.class).withArguments(1L).thenReturn(mockOrder);// 调用Order对象构造方法Order mockResult =newOrder(1L);// 验证返回值为2LAssertions.assertEquals(mockResult.getId(), mockOrder.getId());
- 验证方法调用。确认被模拟的依赖方法调用是否符合预期(被调用或未被调用) a. 验证方法从未被调用过
// 模拟方法返回值Mockito.doReturn(newProduct().setId(2L)).when(productService).getById(1L);// 调用productService的getById方法,未调用orderService的getById方法productService.getById(1L);// 验证productService的getById方法被调用过Mockito.verify(productService).getById(Mockito.anyLong());// 验证orderService的getById方法未被调用过Mockito.verify(orderService,Mockito.never()).getById(1L);
b. 验证方法被调用过(至少一次)// 模拟方法返回值Mockito.doReturn(newProduct().setId(2L)).when(productService).getById(1L);// 触发一次调用productService.getById(1L);// 触发第二次调用productService.getById(1L);// 验证方法至少被调用一次Mockito.verify(productService,Mockito.atLeastOnce()).getById(Mockito.anyLong());
c. 验证方法被调用指定次数// 模拟方法返回值Mockito.doReturn(newProduct().setId(2L)).when(productService).getById(1L);// 触发一次调用productService.getById(1L);// 验证方法至少被调用一次Mockito.verify(productService,Mockito.atLeastOnce()).getById(Mockito.anyLong());// 验证方法被调用一次Mockito.verify(productService,Mockito.atLeastOnce()).getById(Mockito.anyLong());// 触发第二次调用productService.getById(1L);// 验证方法被调用两次Mockito.verify(productService,Mockito.times(2)).getById(Mockito.anyLong());
四、完整实例
Controller
代码测试。 a. 被测试代码@RestController@RequestMapping("/order")@RequiredArgsConstructor(onConstructor_ ={@Lazy})publicclassOrderController{privatefinalOrderService orderService;@GetMapping("/{id}")publicOrderinfo(@PathVariableLong id){return orderService.getById(id);}}
b. 单元测试代码@WebMvcTest(OrderController.class)publicclassOrderControllerTest{@AutowiredprivateMockMvc mockMvc;@MockBeanprivateOrderService orderService;@Test@SneakyThrowspublicvoidtestInfo(){// 期望返回的对象Order order =newOrder(1L);// 对OrderService的返回值进行模拟Mockito.doReturn(order).when(orderService).getById(order.getId());// 构造一个请求MockHttpServletRequestBuilder requestBuilder =MockMvcRequestBuilders.get("/order/"+ order.getId()).accept(MediaType.APPLICATION_JSON);// 执行请求ResultActions resultActions = mockMvc.perform(requestBuilder);// 验证Http响应状态码 resultActions.andExpect(status().isOk());// 使用JsonPath验证查询到的Id为1 resultActions.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(order.getId()));// 获取响应对象MockHttpServletResponse response = resultActions.andReturn().getResponse();// 解析响应内容,进行复杂验证Order respObj =JSON.parseObject(response.getContentAsString(),Order.class);Assertions.assertEquals(order.getId(), respObj.getId());}}
注意:
@MockBean
不能使用
Mokicto
中的
@Mock
或
@Spy
进行替换;
Service
代码测试。Service层代码业务逻辑复杂,需要进行全覆盖。 a. 被测试代码@Service@RequiredArgsConstructor(onConstructor_ ={@Lazy})publicclassUserService{privatefinalUserMapper userMapper;publicIntegersave(UserCreateRequest createRequest){// 用户密码不能等于123456if("123456".equals(createRequest.getPassword())){thrownewInvalidParamException("用户密码太简单了!");}// 检查用户是否存在User user = userMapper.selectByUsername(createRequest.getUsername());if(user ==null){// 用户不存在则进行新增returnthis.create(createRequest);}// 用户存在,则按照用户名进行更新// 用户密码不能包含生日this.update(user, createRequest);return user.getId();}/** * 新增用户 * * @param createRequest 用户创建请求 * @return 用户Id */publicIntegercreate(UserCreateRequest createRequest){// 用户不存在则进行新增User createValue =newUser(); createValue.setUsername(createRequest.getUsername());// 从老系统查询数据User oldUser = oldUserFeign.getByUsername(createRequest.getUsername());if(oldUser ==null){// 如果在老系统不存在,使用默认值填充 createValue.setSex("未知");}else{// 如果在老系统存在,使用老系统用户信息进行填充 createValue.setBirthday(oldUser.getBirthday()); createValue.setSex(oldUser.getSex());}// 保存到数据库return userMapper.create(createValue);}/** * 更新用户 * * @param user 用户 * @param createRequest 更新参数 */publicvoidupdate(User user,UserCreateRequest createRequest){String formatUserBirthday = user.getBirthday().replace("-","");if(createRequest.getPassword().contains(formatUserBirthday)){thrownewInvalidParamException("用户密码不能包含生日!");}User updateUser =newUser(); updateUser.setUsername(createRequest.getUsername()); updateUser.setPassword(createRequest.getPassword());// 更新到数据库 userMapper.update(updateUser);}}
b. 单元测试代码@ExtendWith(MockitoExtension.class)publicclassUserServiceTest{@MockprivateUserMapper userMapper;@MockprivateOldUserFeign oldUserFeign;@Spy@InjectMocksprivateUserService userService;/** * 测试使用简单密码保存用户信息 * 1、验证使用简单密码创建用户时,应当抛出异常 * 2、验证非简单密码创建用户,应当能够正确保存 */@TestpublicvoidtestSaveSimplePasswd(){// 验证使用简单密码创建用户时,应当抛出异常UserCreateRequest simplePasswdRequest =newUserCreateRequest("user-01","123456");// 执行被测试方法,并验证异常类型是否匹配InvalidParamException assertThrows =Assertions.assertThrows(InvalidParamException.class,()-> userService.save(simplePasswdRequest),"简单密码应当抛出异常");// 验证异常信息是否匹配Assertions.assertEquals("用户密码太简单了!", assertThrows.getMessage(),"简单密码异常信息不匹配!");// 抛出异常后,或许代码不应该继续执行Mockito.verify(userMapper,Mockito.never()).selectByUsername(simplePasswdRequest.getUsername());// 验证非简单密码创建用户,应当不会抛出异常UserCreateRequest normalPasswdRequest =newUserCreateRequest("user-01","12345678");// 执行被测试方法,且不会抛出异常 userService.save(normalPasswdRequest);// 未抛出异常,后续代码应当执行一次Mockito.verify(userMapper,Mockito.times(1)).selectByUsername(normalPasswdRequest.getUsername());}/** * 测试使用不存在的用户名保存用户信息,应当执行新增操作,且更新操作不会执行 */@TestpublicvoidtestSaveNotExistUsername(){// 用户名不存在的用户请求UserCreateRequest notExistUsernameRequest =newUserCreateRequest("user-01","12345678");// 模拟UserMapper.selectByUsername返回值Mockito.doReturn(null).when(userMapper).selectByUsername(notExistUsernameRequest.getUsername());// 模拟UserService.create方法返回值Mockito.doReturn(1).when(userService).create(notExistUsernameRequest);// 执行被测试方法Integer createdUserId = userService.save(notExistUsernameRequest);// 未抛出异常,UserMapper.selectByUsername应当执行一次Mockito.verify(userMapper,Mockito.times(1)).selectByUsername(notExistUsernameRequest.getUsername());// 未抛出异常,UserService.create应当执行一次Mockito.verify(userService,Mockito.times(1)).create(notExistUsernameRequest);// 用户不存在,UserService.update方法不应该被执行Mockito.verify(userService,Mockito.never()).update(Mockito.any(),Mockito.any());// UserService.create返回1,UserService.save返回值也应当为1Assertions.assertEquals(1, createdUserId,"新增用户Id应该为1!");}/** * 验证使用已存在的用户名保存用户信息,应当执行更新操作,且新增操作不会被执行 */@TestpublicvoidtestSaveExistUsername(){User user =newUser().setId(1);// 用户名存在的用户请求UserCreateRequest existUsernameRequest =newUserCreateRequest("user-01","12345678");// 模拟UserMapper.selectByUsername返回值Mockito.doReturn(user).when(userMapper).selectByUsername(existUsernameRequest.getUsername());// 模拟UserService.update调用,但是没有返回值Mockito.doNothing().when(userService).update(Mockito.any(),Mockito.any());// 执行被测试方法Integer updateUserId = userService.save(existUsernameRequest);// 未抛出异常,UserMapper.selectByUsername应当执行一次Mockito.verify(userMapper,Mockito.times(1)).selectByUsername(existUsernameRequest.getUsername());// 用户存在,UserService.create方法不应该被执行Mockito.verify(userService,Mockito.never()).create(Mockito.any());// 未抛出异常,UserService.update应当执行一次Mockito.verify(userService,Mockito.times(1)).update(Mockito.any(),Mockito.any());}}
版权归原作者 geXingW 所有, 如有侵权,请联系我们删除。