前后台均启用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 |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用