前后台均启用MVC的设计模式后则会发现前后台的开发思想大同小异。 # Thinking 正式动手前先画个图: ![](https://img.kancloud.cn/b0/62/b0623d918bede289c810098b9ec61d5b_556x203.png) 在开发顺序上,后台的开发仍然建立由后向前来完成。这样以避免在使用一些不熟悉的方法时带来的对接问题。 # M层 按先接口、再实现类再单元测试的步骤依次进行开发。 ## 接口 service/StudentService.java ```java /** * 删除学生 * @param id 学生id */ void deleteById(Long id); ``` ## 实现类 service/StudentServiceImpl.java ```java @Override public void deleteById(@NotNull Long id) { Assert.notNull(id, "传入的ID不能为NULL"); this.studentRepository.deleteById(id); } ``` ## 单元测试 service/StudentServiceImplTest ```java /** * 参数验证 */ @Test public void deleteByIdValidate() { } /** * 功能测试 */ @Test public void deleteById() { // 替身及模拟返回值准备 // 调用方法 // 预测以期望的参数值调用了期望的方法 } ``` 补充参数校验代码:传入null时发生IllegalArgumentException异常。 service/StudentServiceImplTest ```java @Test(expected = IllegalArgumentException.class) public void deleteByIdValidate() { this.studentService.deleteById(null); } ``` 补充功能测试代码: ```java @Test public void deleteById() { // 替身及模拟返回值准备 Long id = new Random().nextLong(); // studentRepository.deleteById方法的返回值类型为void。 // Mockito已默认为返回值为void默认生了返回值,无需对此替身单元做设置 // 调用方法 this.studentService.deleteById(id); // 预测以期望的参数值调用了期望的方法 ArgumentCaptor<Long> longArgumentCaptor = ArgumentCaptor.forClass(Long.class); Mockito.verify(this.studentRepository).deleteById(longArgumentCaptor.capture()); Assert.assertEquals(longArgumentCaptor.getValue(), id); } ``` * 使用@MockBean注解的studentRepository,在返回值为void的方法中。Mockito已为其自动设置了调用时的返回值。 > Mockito在此对studentRepository默认执行了如下方法:Mockito.doNothing().when(this.studentRepository).deleteById(Mockito.anyLong()); # C层 相对于M层,C层还需要额外测试前台调用此接口时的数据输入与输出是否符合预期。该接口返回状态码204,返回内容为空。 ## 单元测试 按开发规范新建用例如下: controller/StudentControllerTest.java ```java @Test public void deleteById() { // 准备替身、传入数据及返回数据 // 向指定的地址发起请求,并断言返回状态码204 // 断言调用方法符合预期 } ``` 补充代码: ```java @Test public void deleteById() throws Exception { // 准备替身、传入数据及返回数据 Long id = new Random().nextLong(); // studentService.deleteById方法返回类型为void,故无需对替身进行设置 // 向指定的地址发起请求,并断言返回状态码204 String url = "/Student/" + id.toString(); this.mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(MockMvcResultMatchers.status().is(204)) ; // 断言调用方法符合预期 ArgumentCaptor<Long> longArgumentCaptor = ArgumentCaptor.forClass(Long.class); Mockito.verify(this.studentService).deleteById(longArgumentCaptor.capture()); Assert.assertEquals(longArgumentCaptor.getValue(), id); } ``` * 同M层的测试相同,studentService.deleteById的返回类型为void,同样可以省略对替身做的设置。 ## 功能代码 按单元测试的提示补充代码: ``` java.lang.AssertionError: Response status Expected :204 Actual :405 ``` 上述错误提示:找到了请求的路径`/Stduent/xxx`但并不是`delete`请求方法。 controller/StudentController.java ```java @DeleteMapping("{id}") public void deleteById() { } ``` 继续进行单元测试 ``` java.lang.AssertionError: Response status Expected :204 Actual :200 <Click to see difference> ``` 提示状态码返回了200,则修正如下: ``` @DeleteMapping("{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteById() { } ``` 继续测试: ``` Wanted but not invoked: studentServiceImpl bean.deleteById( <Capturing argument> ); -> at com.mengyunzhi.springbootstudy.controller.StudentControllerTest.deleteById(StudentControllerTest.java:304) Actually, there were zero interactions with this mock. ``` 出现一些不常的错误时,最好最有效的做法是:翻译一下,猜猜它到底在说什么。 ``` 想调用但是却没有调用 studentServiceImpl bean.deleteById( <Capturing argument 用于捕获的参数> ); -> 错误发生在单元测试的第304行:Mockito.verify(this.studentService).deleteById(longArgumentCaptor.capture()); 实际上,在这个mock(指studentServiceImpl bean)就没有产生影响 (没有被调用过) ``` 由于一些专用的单词在上学期间并没有接触过,所以刚开始猜可能猜的不着边际。不过没关系,猜多了看到这个单词的时候多了,结合看到这些单词的情景,慢慢就猜准了。而这也应该是在英语学习中应该有境界。接受过基础的教育的我们,英文的学习年限最少也有有6年,而且还是在记忆及学习能力都较优秀的青少年时代,但学习的成果好像还不如1-6周岁时对母语的学习。笔者猜想这是由于语言的学习天生需要"环境"的特性而决定的。 翻译后发现报错信息大概是说:这个(this.studentService)上的deleteById没有如预期被调用。修正如下: controller/StudentController.java ```java @DeleteMapping("{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteById(Long id) { this.studentService.deleteById(id); } ``` 继续测试: ``` java.lang.AssertionError: Expected :null Actual :6076760375028858591 <Click to see difference> ... at com.mengyunzhi.springbootstudy.controller.StudentControllerTest.deleteById(StudentControllerTest.java:305) ``` 提示305行发生错误,期望接收的参数是6076760375028858591这个随机数,但却接收到了null,继续修正: controller/StudentController.java ```java @DeleteMapping("{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteById(@PathVariable Long id) { this.studentService.deleteById(id); } ``` 继续测试后单元测试通过,功能完成。 在开发过程中往往会一次性的写完自己能想到的功能性代码,然后再结合测试来补充一些遗忘的功能性代码或者修正一些单元测试及功能性代码的错误。无论是写单元测试还是功能性的代码都是对业务逻辑的一次实现,对同一业务逻辑的两次实现有效的地降低了一些在书写时的低级错误。 # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.8.3](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.8.3) | - | | mockito void methods | [https://www.baeldung.com/mockito-void-methods](https://www.baeldung.com/mockito-void-methods) | 10 |