spring具体完善、强大的单元测试体系。下面,我们使用单元测试来测试班级的保存功能。
首先我们来到controller/KlassController.java,使用alt+enter快捷键来快速的生成测试文件:
![](https://img.kancloud.cn/7e/6e/7e6e40b701078ce284c99e87a9781b32_920x409.gif)
如我们所见,经过一系列的操作后IDEA为我们在对应的测试文件夹下生成了对应的测试文件`KlassControolerTest.java`。
```java
package com.mengyunzhi.springBootStudy.controller;
import org.junit.Test;
import static org.junit.Assert.*;
public class KlassControllerTest {
@Test ➊
public void save() {
}
}
```
* ➊ 表示save方法为一个测试用例
![](https://img.kancloud.cn/c3/a4/c3a48574f3990ad6db6d390470221a0c_632x239.png)
点击此文件的任一绿色箭头,便可启动单元测试。此单元测试为基于`junit4`(一个在java下的测试工具)的测试,在此基础上如想进行Spring项目的单元测试,还需要进行以下配置。
## Spring单元测试
将测试类使用`@RunWith(SpringRunner.class)`、`@SpringBootTest`来标明该类为一个spring测试类。
conroller/KlassControllerTest.java
```
package com.mengyunzhi.springBootStudy.controller;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest; ①
import org.springframework.test.context.junit4.SpringRunner; ①
import static org.junit.Assert.*;
@RunWith(SpringRunner.class) ②
@SpringBootTest ②
public class KlassControllerTest {
@Test
public void save() {
}
}
```
## 测试SAVE方法
想测试控制器中的save方法,我们需要一个叫做`MockMvc 模拟MVC`的家伙来帮忙。
controller/KlassControllerTest.java
```
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;
...
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc ➊
public class KlassControllerTest {
@Autowired ➋
MockMvc mockMvc;
@Test
public void save() {
}
}
```
* ➊ 在此类中自动配置MockMvc
* ➋ 自动装配MockMvc对象
### 模拟POST请求
代码如下:
```
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
...
@Test
public void save() throws Exception { ➏
String url = "/Klass"; ➊
MockHttpServletRequestBuilder postRequest = MockMvcRequestBuilders.post(url) ➋
.contentType("application/json;charset=UTF-8") ➌
.content("{\"name\":\"测试单元测试班级\"}") ➍;
this.mockMvc.perform(postRequest)➎
.andDo(MockMvcResultHandlers.print()➑) ➐
.andExpect(MockMvcResultMatchers.status().is(201));➒
}
```
* ➊ 请求地址,注意**不**需要`http://localhost:8080`。
* ➋ 构建一个指向url的模拟POST请求。
* ➌ 当前发送数据的格式为json;发送数据的编码为utf-8(可省略)。
* ➍ 发送的主体内容为:`{"name":"测试单元测试班级"}`,其中`\"`为`"`的转义符号,用于在字符串中输入`"`。
* ➎ 发送刚刚构建的postRequest请求。
* ➏ 该请求可能得不到正确的执行(比如URL地址写错了),未正常执行时将抛出异常。
* ➐ 成功发送请求后...
* ➑ 打印请求的结果到控制台
* ➒ 然后进行断言
* ➓ 断言返回的状态码为201
> 阅读一些长的代码时,要尝试将其做为课文来读。至于这些功能是如何具体实现的,在面向对象的世界里,我们并不关心。
与augular的单元测试一样,spring的单元测试也可以独立进行,所以在执行单元测试前,我们完全停止项目的运行:
![](https://img.kancloud.cn/07/6b/076b40e5bdc87347800f037d25455111_647x98.png)
然后执行单元测试:
![](https://img.kancloud.cn/67/84/6784d510335e44a3fc105ff11c6a3d9b_748x156.png)
在测试方法前显示了绿色的对勾,表明该方式测试通过。
### 测试中加入教师信息
在前面的测试中加入教师信息时,首先在数据库中了添加一条用于测试的教师信息:
![](https://img.kancloud.cn/ef/db/efdb234810f24d091a75bb34ba0fe6ee_678x123.png)
在此时由于后台整个项目未启动,所以此时数据库中并没有任何数据表(每次系统启动时创建新表、停止时删除创建的表),那么当然也就无法向数据表中添加用于测试的教师数据了。其实即使是我们想办法添加了测试的数据,单元测试在启动时也会自动去删除原有的数据表然后创建空的数据的表,所以只要我们的数据表策略是`create-drop`,那么就没有办法使用原来的方法添加测试教师数据。
每次单元测试时都清空数据表在单元测试中非常有必要的,这保证了我们每次启动单元测试时数据的初始环境都是一致的,减少了单元测试在不同环境下的不确定性。那么此时如果想添加测试教师,则需要在代码中进行添加:
#### TeacherReopository
前面我们建立了操作Klass表的KlassRepository,参考该文件我们建立操作Teacher表的TeacherRepository
repository/TeacherRepository.java
```
package com.mengyunzhi.springBootStudy.repository;
import com.mengyunzhi.springBootStudy.entity.Teacher;
import org.springframework.data.repository.CrudRepository;
/**
* 教师仓库
*/
public interface TeacherRepository extends CrudRepository<Teacher, Long> {
}
```
#### 自动装配
接着来到需要使用该仓库进行数据新增的controller/KlasscontrollerTest.java
```java
import com.mengyunzhi.springBootStudy.repository.TeacherRepository;
...
public class KlassControllerTest {
@Autowired
MockMvc mockMvc;
/*教师仓库*/
@Autowired
TeacherRepository teacherRepository; ①
@Test
public void save() throws Exception {
```
### 请求主体中加入教师信息
最后,我们在单元测试中利用teacherRepository来新建一个测试教师,最获取其ID放置到POST请求的主体中。
controller/KlasscontrollerTest.java
```
/*教师仓库*/
@Autowired
TeacherRepository teacherRepository; ①
@Test
public void save() throws Exception {
Teacher teacher = new Teacher(); ➊
teacher.setName("潘杰"); ➋
teacher.setEmail("panjie@yunzhiclub.com"); ➋
teacher.setUsername("panjie"); ➋
teacherRepository.save(teacher); ➌
String content = String.format("{\"name\":\"测试单元测试班级\", \"teacher\": {\"id\":%s}}", teacher.getId()); ➍
String url = "/Klass";
MockHttpServletRequestBuilder postRequest = MockMvcRequestBuilders.post(url)
.contentType("application/json;charset=UTF-8")
.content(content); ②
this.mockMvc.perform(postRequest)
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().is(201));
}
```
* ➊ 实例化教师
* ➋ 设置教师的属性
* ➌ 将教师保存到数据库中
* ➍ 拼接请求主体对应的字符串
测试:
```
MockHttpServletRequest:
HTTP Method = POST
Request URI = /Klass
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8"]
Body = {"name":"测试单元测试班级", "teacher": {"id":1}}
Session Attrs = {}
```
### 添加断言
我们前面的测试确认了以下内容:
* [ ] 请求的地址是正确的
* [ ] 按接口发送数据,后台是能够成功接收的
* [ ] 后台处理后,返回201的状态码
但数据是否真的被保存成功了呢?下面,我们加入数据保存成功的断言。
```java
import org.junit.Assert;
/*班级*/
@Autowired
KlassRepository klassRepository; ①
/**
* 班级保存测试
* 1. 建立一个供测试的教师
* 2. 拼接请求的JSON串
* 3. 模拟请求并断言返回了201
* 4. 断言新增数据成功
* @throws Exception
*/
@Test
public void save() throws Exception {
Teacher teacher = new Teacher();
teacher.setName("潘杰");
teacher.setEmail("panjie@yunzhiclub.com");
teacher.setUsername("panjie");
teacherRepository.save(teacher);
String content = String.format("{\"name\":\"测试单元测试班级\", \"teacher\": {\"id\":%s}}", teacher.getId());
String url = "/Klass";
MockHttpServletRequestBuilder postRequest = MockMvcRequestBuilders.post(url)
.contentType("application/json;charset=UTF-8")
.content(content);
this.mockMvc.perform(postRequest)
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().is(201));
List<Klass> klasses = (List<Klass>)➋ this.klassRepository.findAll(); ➊
Assert.assertEquals(klasses.size(), 1); ➌
Klass klass = klasses.get(0); ➍
Assert.assertEquals(klass.getName(), "测试单元测试班级"); ➎
Assert.assertEquals(klass.getTeacher().getName(), "潘杰"); ➏
}
```
* ➊ findAll()查询出数据表中的全部数据。
* ➋ 将返回的值转换为List<Klass>类型。
* ➌ 断言有1条班级数据
* ➍ 获取该班级数据
* ➎ 断言班级的名称为测试名称
* ➏ 断言班级的中的教师名称为测试名称
最行,执行单元测试,测试通过代码数据添加成功,方法正确。
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.3.5](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.3.5) | - |
| Testing the Web Layer | [https://spring.io/guides/gs/testing-web/](https://spring.io/guides/gs/testing-web/) | 15 |
| Spring MVC Test Framework | [https://docs.spring.io/spring/docs/5.2.1.RELEASE/spring-framework-reference/testing.html#spring-mvc-test-framework](https://docs.spring.io/spring/docs/5.2.1.RELEASE/spring-framework-reference/testing.html#spring-mvc-test-framework) | 30 |
- 序言
- 第一章: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
- 总结
- 开发规范
- 备用