我们在上个小节中刚刚讲了`规范`,在本节中开始前我们先为idea安装一个规范插件。打开idea,使用`ctrl+,`打开参数设置,然后输入plugins:
![](https://img.kancloud.cn/9e/81/9e8123fceb84b4d121f658f8232803f1_402x532.png)
在右侧的窗口中输入alibaba
![](https://img.kancloud.cn/a0/dd/a0ddbf6984ae510e326332ba396c7486_965x263.png)
点击install安装,完成后重启idea,此时一个代码规范约束的插件便被成功的安装了。以后当我们在书写一不太规范的代码时,该插件则会进行自动提示。
# M层 -- 多态
JAVA的多态性允许我们在StudentService中有了一个findAll方法的同时,再写一个同名findAll方法:
```
/**
* 学生
*/
public interface StudentService {
...
/**
* 查询分页信息
*
* @param pageable 分页条件
* @return 分页数据
*/
Page<Student> findAll(Pageable pageable);
/**
* 综合查询
* @param name containing 姓名
* @param sno beginWith 学号
* @param klassId equal 班级ID
* @param pageable
* @return
*/
Page<Student> findAll(String name, String sno, Long klassId, Pageable pageable); ➊
```
* ➊ 方法名仍然为findAll,但由于参数不同,所以在其它的对象调用StudentService.findAll时,java是可以通过判断参数的数量、类型来区分我们具体是想调用哪个findAll方法。
> 一个findAll方法,有多种形态,称为面向对象的多态性
## 实现类
在实现类中添加方法实现:
```
@Service
public class StudentServiceImpl implements StudentService {
...
@Override
public Page<Student> findAll(String name, String sno, Long klassId, Pageable pageable) {
Klass klass = new Klass(); ➊
klass.setId(klassId); ➊
return this.studentRepository.findAll(name, sno, klass, pageable); ➋
}
}
```
* ➊ 根据klassId构造用Klass
* ➋ 调用数据仓库,完成查询并返回
## 单元测试
找开位于测试文件夹的StudentServiceImplTest并增加测试方法findAllSpecs。该测试的思路和以前的一样,我们只需要测试findAll(String name, String sno, Long klassId, Pageable pageable)是否将数据成功的转发给数据仓库层即可。至于数据仓库层是否成功的执行了查询功能,按分层理论这是数据仓库层需要关心的事情。
```
import java.util.Arrays;
...
@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentServiceImplTest {
private static Logger logger = LoggerFactory.getLogger(StudentServiceImplTest.class);
@MockBean
StudentRepository studentRepository;
...
@Test
public void findAllSpecs() {
/* 参数初始化 */
String name = "hello";
String sno = "032282";
Long klassId = 1L;
Pageable pageable = PageRequest.of(0, 2);
List<Student> students = Arrays.asList();
Page<Student> mockStudentPage = new PageImpl<>(students, pageable, 100L) ➊;
/* 设置模拟返回值 */
Mockito.when(this.studentRepository
.findAll(Mockito.eq(name)➋,
Mockito.eq(sno),
Mockito.any(Klass.class),
Mockito.eq(pageable)))
.thenReturn(mockStudentPage) ➌;
/* 调用测试方法,获取返回值并断言与预期相同 */
Page<Student> returnStudentPage = this.studentService.findAll(name, sno, klassId, pageable);
Assertions.assertThat(returnStudentPage).isEqualTo(mockStudentPage); ➍
}
}
```
* ➊ 调用PageImpl<>(当前页内容, 分页信息, 总条数)来构建返回值
* ➋ 当调用findAll的第一个参数与name相等(eq)时
* ➌ 当参数为符合:`参数1等于(eq)name对象(值:hello),参数2等于(eq)sno对象(值:032282) , 参数1为任意(any)的班级,参数4等于(eq)pageable对象(值:第0页每页2条)`规则时,调用findAll方法将模拟返回mockStudentPage对象。
* 预期返回了mockStudentPage对象。
由于预期返回了mockStudentPage对象,则说明在studentService中调用studentRepository.findAll时,传入的参数符合`参数1等于(eq)name对象(值:hello),参数2等于(eq)sno对象(值:032282) , 参数1为任意(any)的班级,参数4等于(eq)pageable对象(值:第0页每页2条)`规则。进而说明调用name, sno, pageable均是我们传入的值。而是否按我们的预期传入了klass还需要使用参数捕获器来获取:
```
public class StudentServiceImplTest {
...
@Test
public void findAllSpecs() {
...
Assertions.assertThat(returnStudentPage).isEqualTo(mockStudentPage);
/* 获取M层调用studentRepository的findAll方法时klass的参数值,并进行断言 */
ArgumentCaptor<Klass> klassArgumentCaptor = ArgumentCaptor.forClass(Klass.class);
Mockito.verify(this.studentRepository).findAll(Mockito.eq➊(name), Mockito.eq(sno), klassArgumentCaptor.capture()①, Mockito.eq(pageable));
Assertions.assertThat(klassArgumentCaptor.getValue().getId()).isEqualTo(klassId); ②
}
}
```
* ➊ Mockito.eq与前面作用相同
* ① 把第三个参数替换为klassArgumentCaptor.capture()来获取参数的值
* ② 断言klass中id的值
### 请思索
在刚刚的测试中,我们将测试代码做以下替换,同样可以顺利通过测试,你知道其中的原因吗?
```
Mockito.verify(this.studentRepository).findAll(Mockito.any(String.class), Mockito.any(String.class), klassArgumentCaptor.capture(), Mockito.any(Pageable.class));
Assertions.assertThat(klassArgumentCaptor.getValue().getId()).isEqualTo(klassId);
```
## Null值处理
我们在进行方法调用的原则是:如果该参数没有标记为@NotNull,则表示其是可以接收并顺利处理null值的。而如果M层在findAll方法中如果接收的Pageable为null,则会在调用数据仓库层时发生异常,而这是我们不希望看到了。
```
import javax.validation.constraints.NotNull;
...
public interface StudentService {
...
Page<Student> findAll(String name, String sno, Long klassId, @NotNull Pageable pageable);
}
```
实现类:
```
import org.springframework.util.Assert;
import javax.validation.constraints.NotNull;
...
public class StudentServiceImpl implements StudentService {
...
@Override
public Page<Student> findAll(String name, String sno, Long klassId, @NotNull Pageable pageable) {
Assert.notNull(pageable, "Pageable不能为null");
...
}
}
```
### null值测试
```
public class StudentServiceImplTest {
...
@Test(expected = IllegalArgumentException.class) ①
public void findAllSpecsNullValidate() {
try {
this.studentService.findAll(null, null, null, null);
} catch (Exception e②) {
Assertions.assertThat(e.getMessage()).isEqualTo("Pageable不能为null");
throw e; ③
}
}
}
```
* ① 该测试应该会抛出 IllegalArgumentException
* ② 所有的异常都继承了Exception (java.lang.Exception属于java内置的类,使用时无需import),所以只要有异常抛出,并必然被此catch捕获到
* ③ 将获取到的异常向上抛出,并被 ① 获取
# 重写C层
打开StudentController -> findAll方法,在原代码的基础上加入参数name,sno以及klassId:
```
public class StudentController {
...
@GetMapping
public Page<Student> findAll(
@RequestParam String name, ✚
@RequestParam String sno, ✚
@RequestParam Long klassId, ✚
@RequestParam int page,
@RequestParam int size) {
return this.studentService.findAll(PageRequest.of(page, size));
}
```
修改调用方法:
```
public Page<Student> findAll(
@RequestParam String name,
@RequestParam String sno,
@RequestParam Long klassId,
@RequestParam int page,
@RequestParam int size) {
return this.studentService.findAll(
name, ✚
sno, ✚
klassId, ✚
PageRequest.of(page, size));
}
```
## 单元测试
前面我们一直在强调单元测试是对自己代码功能的保障,此时我们刚刚变动了StudentController的代码,那么我们校验下单元测试是否起到了应有的作用。我们打开单元测试文件StudentControllerTest,找到并找行findAll单元测试:
最终得到了以下错误提示:
```
java.lang.AssertionError: Status
Expected :200
Actual :400
<Click to see difference>
```
期待是200,最终返回了400。在控制台中找到错误提示行并进行点击,报错的行数如上:
```
public class StudentControllerTest {
...
@Test
public void findAll() throws Exception {
...
MvcResult mvcResult = this.mockMvc.perform(
MockMvcRequestBuilders.get(url)
.param("page", "1")
.param("size", "2"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk()) ★
.andReturn();
```
说明返回的状态码为400,并不是我们期望的200。而400通常代表参数绑定错误,在我们当前的用例中:由于我们在C层的方法中声明需要接收参数name、sno及klassId,而单元测试在测试时,只传入了参数page与size。所以在参数的绑定过程中spring不清楚需要用什么值来绑定name、sno、klassId,进而发生了400错误。
> 在生产环境中,C层的单元测试报400错误是需要引起重视的,因为这意味着我们后台的请求接口的规范发生了变化,而前台如果想适应这个变化就必须同步进行修改。
单元测试报错的原因可以分为两种:第一种是功能的修正导致历史的单元测试代码不能适应新的功能需求;第二种是在增加、修正关联代码时对历史的功能造成了影响。如果情况属性第一种,则应该按新功能修正单元测试代码,如果情况属性第二种,则应该进行近一步的排查。我们此处的单元测试则属于第一种,解决的方法是修正单元测试代码:
在原请求的参数的基础上,我们加入以下三个参数:
```
public class StudentControllerTest {
...
@Test
public void findAll() throws Exception {
...
MvcResult mvcResult = this.mockMvc.perform(
MockMvcRequestBuilders.get(url)
.param("name", "testName") ✚
.param("sno", "testSno") ✚
.param("klassId", "1") ✚
.param("page", "1")
.param("size", "2"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
```
加全请求参数后,重新运行该单元测试,400的错误消息,得到了新的错误:
```
java.lang.IllegalArgumentException: json string can not be null or empty
at com.jayway.jsonpath.internal.Utils.notEmpty(Utils.java:386)
at com.jayway.jsonpath.internal.ParseContextImpl.parse(ParseContextImpl.java:36)
at com.jayway.jsonpath.JsonPath.parse(JsonPath.java:599)
at com.mengyunzhi.springbootstudy.controller.StudentControllerTest.findAll(StudentControllerTest.java:87) ★
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
```
★处对应的代码如下:
```
public class StudentControllerTest {
...
@Test
public void findAll() throws Exception {
...
logger.info("将返回值由string转为json,并断言接收到了分页信息");
LinkedHashMap returnJson = JsonPath.parse(mvcResult.getResponse().getContentAsString()).json(); ★
Assertions.assertThat(returnJson.get("totalPages")).isEqualTo(2); // 总页数
```
该错误的出现说明了两点:1. 未发生400错误,说明参数绑定成功,证明刚刚修正的代码正确。2. 将返回值转换为JSON时发生错误,说明返回不再符合预期,按错误提示猜测试返回值为empty或null。
既然对返回数据产生了怀疑,则可以添加断点并启用debug模式来对问题进行调试,在错误的行上打个断点:
![](https://img.kancloud.cn/c5/73/c5731fddffc7e1eb2213239b9e21eb9b_1654x132.png)
使用debug模式运行测试
![](https://img.kancloud.cn/e3/98/e398fc926ed2502c7e33f76c1258e434_596x207.png)
在控制台中查看返回值mvcResult:
![](https://img.kancloud.cn/ac/71/ac716cac988a037c8944dc6aaee10a84_885x273.png)
结果显示返回了空字符串,而不是我们预期的分页信息了。这是由于C层的代码在改变了调用M层的方法引起的,我们再来查看C层代面在引入综合查询前后的变化
```
public class StudentController {
...
@GetMapping
public Page<Student> findAll(
@RequestParam String name, ✚ ①
@RequestParam String sno, ✚ ①
@RequestParam Long klassId, ✚ ①
@RequestParam int page,
@RequestParam int size) {
return this.studentService.findAll(PageRequest.of(page, size)); ✘ ②
return this.studentService.findAll(
name,
sno,
klassId,
PageRequest.of(page, size)); ✚ ②
}
```
* ① 代码变动、引起了400问题
* ② 代码变动,引起了返回空值的问题
在前面的测试代码中,我们测试的逻辑是:C层代码调用的是`this.studentService.findAll(Pageable)`方法。而修改后的逻辑变更为调用`this.studentService.findAll(Strng, String, Long, Pageable)`方法。此时单元测试的报错及时的提醒我们C层调用的逻辑发生了变化。如果逻辑是正确的则应该修正单元测试代码;如果逻辑是错误的,则应该修正单元测试代码。则修正单元测试代码如下:
```
public class StudentControllerTest {
...
@Test
public void findAll() throws Exception {
...
Mockito.when(this.studentService.findAll(Mockito.any(Pageable.class)))
.thenReturn(mockOutStudentPage); ✘
Mockito.when(this.studentService
.findAll(Mockito.anyString()➊,
Mockito.anyString(),
Mockito.anyLong()➋,
Mockito.any(Pageable.class)))
.thenReturn(mockOutStudentPage); ✚
...
```
* ➊ 请求的参数值类型为String时
* ➋ 请求的参数值类型为Long时
再次运行单元测试,测试通过。
## 可选参数及空参数
按接口的设计,前台在与后台进行交互时,查询参数name、sno及klassId为optional(可选项),也就是说:我们允许用户在查询时不输入此参数。而刚刚在测试的过程中我们发现:如果不输入name sno或klassId则会得到一个400错误。在spring中,可以为@RequestParam设置`required=false`来解决该问题:
```
public class StudentController {
...
@GetMapping
public Page<Student> findAll(
@RequestParam(required = false➊) String name,
@RequestParam(required = false) String sno,
@RequestParam(required = false) Long klassId,
@RequestParam int page,
@RequestParam int size) {
return this.studentService.findAll(
name,
sno,
klassId,
PageRequest.of(page, size));
}
```
* ➊ 该参数是可选项(非required),当用户未传入该参数时,name的值设置为null。
重新运行单元测试通过,表示我们的此处的修改并未对原功能造成影响。
### 单元测试
本着单元测试粒度最小的原则,我们StudentControllerTest中新建findAllRequestParam方法:
```
public class StudentControllerTest {
...
/**
* 请求参数测试
* @throws Exception
*/
@Test
public void findAllRequestParam() throws Exception {
String url = "/Student";
logger.info("只传入page size,不报错");
this.mockMvc.perform(
MockMvcRequestBuilders.get(url)
.param("page", "1")
.param("size", "2"))
.andExpect(MockMvcResultMatchers.status().isOk());
logger.info("不传page报错");
this.mockMvc.perform(
MockMvcRequestBuilders.get(url)
.param("size", "2"))
.andExpect(MockMvcResultMatchers.status().is(HttpStatus.BAD_REQUEST.value()));
logger.info("不传size报错");
this.mockMvc.perform(
MockMvcRequestBuilders.get(url)
.param("page", "1"))
.andExpect(MockMvcResultMatchers.status().is(400));
}
```
# 小测试
在C层的单元测试findAll中我们虽然断言了`必然调用studentService.findAll方法`,但却没有对调用该方法时向个参数的传入值进行断言。也就是说如果C层的代码被不小心写成:`return this.studentService.findAll(sno, name, klassId, PageRequest.of(page, size))`的话,我们也无从察觉。请参数M层的测试补充该部分,以确保C层的转发是正确的。
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.5](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.5) | - |
| mockito anyString | [https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#anyString--](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#anyString--) | 2 |
| mockito anyLong | [https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#anyLong--](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#anyLong--) | 2 |
| mockito eq | [https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#eq-T-](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#eq-T-) | 2 |
- 序言
- 第一章: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
- 总结
- 开发规范
- 备用