前台准备完毕后。接下来进行后台的对接。后台对接主要实现两个接口:1. 根据ID获取某位学生信息接口。 2. 更新某个学生信息的接口。接口规范在[本章节](https://www.kancloud.cn/yunzhiclub/springboot_angular_guide/1378718)开始时已经给出。
前台的主体编辑功能完成后,再来观察相应的更新接口。此时发现接口的返回值并不符合前台的数据要求:
前台对编辑的返回值处理如下:
```javascript
/**
* 更新学生
* @param id id
* @param student 学生
*/
update(id: number, student: Student): Observable<Student➊> {
const url = `http://localhost:8080/Student/${id}`;
return this.httpClient.put<Student>(url, student);
}
```
* ➊ 更新接口应该返回学生
但前期定义的接口规范返回的却是空内容:
![](https://img.kancloud.cn/74/cb/74cbeed4242a6cbff64327eadb20b009_910x162.png)
可以预见的是:如果按前面规定的接口规范开发,那么后台的更新接口将无法满足前台的功能需求。若想满足前台的功能的需求,则需要变更后台接口相应的返回值。由于此接口有了返回值,状态码也应该由204变更为200。
新的接口返回值如下:
#### 响应(返回值)Responses
| HTTP Code | Description | Schema |
| --- | --- | --- |
| **200** | 学生 | Student |
在相应功能的开发过程中,我们优先选择开发前台,然后再开发后台的开发步骤也是基于这种接口规范可能会变更的现实。这种开发步骤能够有效的避免一些接口定义无法满足前台实际需求的情况。先开发前台再开发后台,变更接口规范对后台造成的影响最小。但如果先开发后台再开发前台就完全不一样了。后台开发完成后,前台在使用的过程中发现适用有问题,此时就需要后台进行修改来适应前台。有时候这种修改会直接推翻后台的逻辑性,使原后台开发的接口的价值为0甚至为负值。
# 接口开发
先Thinking,再Coding:
![](https://img.kancloud.cn/21/bb/21bb73b3b14554c43ffdd39287e8a4fa_826x441.png)
# GetById
按时序图的反方向进行初始化:
数据仓库层由于继承了Crud接口,save方法已经由该接口提供了,所以直接忽略。
服务层初始化:service/StudentService.java
```
public interface StudentService {
...java
/**
* 查找学生
* @param id 学生ID
* @return 学生
*/
Student findById(@NotNull Long id);
```
服务层初始化:service/StudentServiceImpl.java
```java
public class StudentServiceImpl implements StudentService {
...
@Override
public Student findById(@NotNull Long id) {
return null;
}
```
C层初始化:controller/StudentController.java
```java
public class StudentController {
...
/**
* 通过ID查询学生
* @param id 学生ID
* @return 学生
*/
public Student getById(Long id) {
return this.studentService.findById(id);
}
```
> 回看KlassController中的获取某个班级时,会发现其方法名命名为:get;但此处被命名为getById。当某个方法参数较少时,可以采用`xxxBy参数a参数b`的形式来进行命名,当参数较多时,则一般直接命名为xxx。
此时在常规的开发方法中,便可以启用postman或是直接启动前开来进行功能开发了。在教程中对于已经学习过的知识点,我们仍然优先使用单元测试的方法进行功能开发。
## 功能开发
功能开发过程仍然按从后到前的开发步骤,在单元测试还没有并熟练掌握前,这可以更好的支持传统的测试方法。
### M层
service/StudentServiceImpl.java
```java
public class StudentServiceImpl implements StudentService {
...
@Override
public Student findById(@NotNull Long id) {
Assert.notNull(id, "id不能为null"); ➊
return this.studentRepository.findById(id).get(); ➋
}
```
* ➊ 非null校验,当传入null时,直接抛出异常并附带提示信息
* ➋ 调用仓库层返回学生
### M层单元测试
单元测试的过程中,如果单元测试的代码过长或逻辑过于复杂,应该想办法进行拆分,将测试粒度变小。本方法的测试逻辑相对简单,是否将所有的功能放到一个单元测试中来进行测试,可以自主决定。教程中仍然采用粒度最小化原则进行测试。
测试粒度一:null测试 StudentServiceImplTest.java
```java
/**
* 参数为null测试
*/
@Test(expected = IllegalArgumentException.class)
public void findByIdNullArgument() {
this.studentService.findById(null);
}
```
测试粒度2:调用测试
```java
/**
* 调用测试
*/
@Test
public void findById() {
// 准备调用时的参数及返回值
// 发起调用
// 断言返回值与预期相同
// 断言接收到的参数与预期相同
}
```
按注释补充代码如下:
```java
/**
* 调用测试
*/
@Test
public void findById() {
// 准备调用时的参数及返回值
Long id = new Random().nextLong();
Student mockReturnStudent = new Student();
Mockito.when(this.studentRepository.findById(id)).thenReturn(Optional.of(mockReturnStudent));
// 发起调用
Student student = this.studentService.findById(id);
// 断言返回值与预期相同
Assertions.assertThat(student).isEqualTo(mockReturnStudent);
// 断言接收到的参数与预期相同
ArgumentCaptor<Long> longArgumentCaptor = ArgumentCaptor.forClass(Long.class);
Mockito.verify(this.studentRepository).findById(longArgumentCaptor.capture());
Assertions.assertThat(longArgumentCaptor.getValue()).isEqualTo(id);
}
```
单元测试通过,说明功能符合我们的预期。
### C层
相对于M层,C层由于需要与前台对接,所以在测试的过程中测试点相对要多一些。
初始化 StudentControllerTest
```java
@Test
public void getById() {
// 准备传入参数的数据
// 准备服务层替身被调用后的返回数据
// 按接口规范,向url以规定的参数发起get请求。
// 断言请求返回了正常的状态码
// 断言C层进行了数据转发(替身接收的参数值符合预期)
// 断言返回的json数据符合前台要求
}
```
按注释分步完成代码:
```
import org.assertj.core.internal.bytebuddy.utility.RandomString; ➋
@Test
public void getById() throws Exception {
// 准备传入参数的数据
Long id = new Random().nextLong();
// 准备服务层替身被调用后的返回数据
Student student = new Student();
student.setId(id); ➊
student.setSno(new RandomString(6).nextString()➋); ➊
student.setName(new RandomString(8).nextString()); ➊
student.setKlass(new Klass()); ➊
student.getKlass().setId(new Random().nextLong()); ➊
Mockito.when(this.studentService.findById(Mockito.anyLong())).thenReturn(student);
// 按接口规范,向url以规定的参数发起get请求。
// 断言请求返回了正常的状态码
String url = "/Student/" + id.toString() ;
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get(url))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
// 断言C层进行了数据转发(替身接收的参数值符合预期)
ArgumentCaptor<Long> longArgumentCaptor = ArgumentCaptor.forClass(Long.class);
Mockito.verify(this.studentService).findById(longArgumentCaptor.capture());
Assertions.assertThat(longArgumentCaptor.getValue()).isEqualTo(id);
// 断言返回的json数据符合前台要求
DocumentContext documentContext =JsonPath.parse(mvcResult.getResponse().getContentAsString()); ➌
LinkedHashMap studentHashMap = documentContext.json(); ➍
Assertions.assertThat(studentHashMap.get("id")).isEqualTo(Integer.valueOf(id.toString()));
Assertions.assertThat(studentHashMap.get("sno")).isEqualTo(student.getSno());
Assertions.assertThat(studentHashMap.get("name")).isEqualTo(student.getName());
LinkedHashMap klassHashMap = (LinkedHashMap) studentHashMap.get("klass");
Assertions.assertThat(klassHashMap.get("id")).isEqualTo(Integer.valueOf(student.getKlass().getId().toString()));
}
```
* ➊ 前台获取某个学生生,需要将这些值展示到V层或供C层使用,所以返回值在准备好这些数据。
* ➋ 另一种获取随机字符串的方法
* ➌➍ 通过两次转换,将json字符串转换为java中的LinkedHashMap对象
接下来,启动单元测试并按单元测试提示完善功能代码或修正单元测试代码:
```
java.lang.AssertionError: Status
Expected :200
Actual :404
<Click to see difference>
```
404错误说明请求的地址未找到,该错误的产生无非就两个原因:1. 请求时URL不小心拼写错了。 2. 后台没有对应建立好请求地址对应的映射。通过检查发现当前属于第2个原因。
```
@GetMapping("{id}") ✚
public Student getById(Long id) {
return this.studentService.findById(id);
}
```
再次进行测试:
```
Assertions.assertThat(longArgumentCaptor.getValue()).isEqualTo(id);
org.junit.ComparisonFailure:
Expected :555414603L ➊
Actual :null
```
* ➊ 该值是随机生成的,每次执行单元测试都会生成一个随机值,因面你本地显示的值与教程不同是正确的。
该错误提示我们:应该是使用传入的ID值来调用M层,但实际上却使用了null来调用。这是由于我们在C层未能成功的接收传入ID值造成的。
```java
public Student getById(@PathParam("id")✚ Long id) {
return this.studentService.findById(id);
}
```
再次测试仍然是刚刚的错误,这仍然说明C层没有接收到传入的ID值。最终通过检查发现原来获取路径变量的应该使用`@PathVariable`而非`@PathParam`:
```
public Student getById(@PathVariable✚ Long id) {
return this.studentService.findById(id);
}
```
再次进行单元测试,测试通过。单元测试看似写了较多的代码,但其实开发的效率并不低。在这种开发模式下,我们无需向数据库中写入真实的数据(实际上这项工作在一些稍大型的一些有外键约束的项目中非常的沉重),也不会额外启动一个前台或是类似于postman的工作。更重要的是还为此代码在后期项目更新的过程中提供了功能保障。长期来看,其不失为一种高效的开发方式。
## JsonPath
在将json字符串变更为java可识别的对象时,使用了`JsonPath.parse`方法。实际上springboot已经内置了`JsonPah`并将其快速的应用到了模拟请求返回值的断言中。刚刚单元测试中对json数据的断言还可以改写成这样。
```
@Test
public void getById() throws Exception {
// 准备传入参数的数据
Long id = new Random().nextLong();
// 准备服务层替身被调用后的返回数据
Student student = new Student();
student.setId(id);
student.setSno(new RandomString(6).nextString());
student.setName(new RandomString(8).nextString());
student.setKlass(new Klass());
student.getKlass().setId(new Random().nextLong());
Mockito.when(this.studentService.findById(Mockito.anyLong())).thenReturn(student);
// 按接口规范,向url以规定的参数发起get请求。
// 断言请求返回了正常的状态码
String url = "/Student/" + id.toString() ;
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get(url))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("id").value(id)) ✚
.andExpect(MockMvcResultMatchers.jsonPath("sno").value(student.getSno())) ✚
.andExpect(MockMvcResultMatchers.jsonPath("name").value(student.getName())) ✚
.andExpect(MockMvcResultMatchers.jsonPath("klass.id").value(student.getKlass().getId())) ✚
.andExpect(MockMvcResultMatchers.jsonPath("klass.name").value(student.getKlass().getName())) ✚
.andReturn();
// 断言C层进行了数据转发(替身接收的参数值符合预期)
ArgumentCaptor<Long> longArgumentCaptor = ArgumentCaptor.forClass(Long.class);
Mockito.verify(this.studentService).findById(longArgumentCaptor.capture());
Assertions.assertThat(longArgumentCaptor.getValue()).isEqualTo(id);f
}
```
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.7.4](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.7.4) | - |
| JsonPath | [https://github.com/json-path/JsonPath](https://github.com/json-path/JsonPath)| - |
- 序言
- 第一章: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
- 总结
- 开发规范
- 备用