Mokito 单元测试与 Spring-Boot 集成测试
Mokito 单元测试与 Spring-Boot 集成测试
版本说明
Java:1.8
JUnit:5.x
Mokito:3.x
H2:1.4.200
spring-boot-starter-test:2.3.9.RELEASE
前言:通常任何软件都会划分为不同的模块和组件。单独测试一个组件时,我们叫做单元测试。单元测试用于验证相关的一小段代码是否正常工作。
单元测试不验证应用程序代码是否和外部依赖正常工作。它聚焦与单个组件并且 Mock 所有和它交互的依赖。
集成测试主要用于发现用户端到端请求时不同模块交互产生的问题。
集成测试范围可以是整个应用程序,也可以是一个单独的模块,取决于要测试什么。
典型的 Spring boot CRUD 应用程序,单元测试可以分别用于测试控制器(Controller)层、DAO 层等。它不需要任何嵌入服务,例如:Tomcat、Jetty、Undertow。
在集成测试中,我们应该聚焦于从控制器层到持久层的完整请求。应用程序应该运行嵌入服务(例如:Tomcat)以创建应用程序上下文和所有 bean。这些 bean 有的可能会被 Mock 覆盖。
单元测试
单元测试的动机,单元测试不是用于发现应用程序范围内的 bug,或者回归测试的 bug,而是分别检测每个代码片段。
几个要点
- 快,极致的快,500ms 以内
- 同一个单元测试可重复运行 N 次
- 每次运行应得到相同的结果
- 不依赖任何模块
Gradle 引入
plugins {
    id 'java'
    id "org.springframework.boot" version "2.3.9.RELEASE"
    id 'org.jetbrains.kotlin.jvm' version '1.4.32'
}
apply from: 'config.gradle'
apply from: file('compile.gradle')
group rootProject.ext.projectDes.group
version rootProject.ext.projectDes.version
repositories {
    mavenCentral()
}
dependencies {
    implementation rootProject.ext.dependenciesMap["lombok"]
    annotationProcessor rootProject.ext.dependenciesMap["lombok"]
    implementation rootProject.ext.dependenciesMap["commons-lang3"]
    implementation rootProject.ext.dependenciesMap["mybatis-plus"]
    implementation rootProject.ext.dependenciesMap["spring-boot-starter-web"]
    implementation rootProject.ext.dependenciesMap["mysql-connector"]
    implementation rootProject.ext.dependenciesMap["druid"]
    testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '2.3.9.RELEASE'
    testImplementation rootProject.ext.dependenciesMap["h2"]
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
}
test {
    useJUnitPlatform()
}
引入 spring-boot-starter-test 做为测试框架。该框架已经包含了 JUnit5 和 Mokito 。
对 Service 层进行单元测试
工程结构

- Domain 中定义 student 对象。 - @Data
 @AllArgsConstructor
 public class Student { public Student() {
 this.createTime = LocalDateTime.now();
 } /**
 * 学生唯一标识
 */
 @TableId(type = AUTO)
 private Integer id; /**
 * 学生名称
 */
 private String name; /**
 * 学生地址
 */
 private String address; private LocalDateTime createTime; private LocalDateTime updateTime;
 }
 
- Service 层定义 student 增加和检索的能力。 - public interface StudentService extends IService<Student> { /**
 * 创建学生
 * <p>
 * 验证学生名称不能为空
 * 验证学生地址不能为空
 *
 * @param dto 创建学生传输模型
 * @throws BizException.ArgumentNullException 无效的参数,学生姓名和学生住址不能为空
 */
 void create(CreateStudentDto dto) throws BizException.ArgumentNullException; /**
 * 检索学生信息
 *
 * @param id 学生信息 ID
 * @return 学生信息
 * @throws DbException.InvalidPrimaryKeyException 无效的主键异常
 */
 StudentVo retrieve(Integer id) throws DbException.InvalidPrimaryKeyException;
 }
 
- Service 实现,单元测试针对该实现进行测试。 - @Service
 public class StudentServiceImpl extends ServiceImpl<StudentRepository, Student> implements StudentService { private final Mapper mapper; public StudentServiceImpl(Mapper mapper) {
 this.mapper = mapper;
 } @Override
 public void create(CreateStudentDto dto) throws BizException.ArgumentNullException {
 if (stringNotEmptyPredicate.test(dto.getName())) {
 throw new BizException.ArgumentNullException("学生名称不能为空,不能创建学生");
 }
 if (stringNotEmptyPredicate.test(dto.getAddress())) {
 throw new BizException.ArgumentNullException("学生住址不能为空,不能创建学生");
 } Student student = mapper.map(dto, Student.class);
 save(student);
 } @Override
 public StudentVo retrieve(Integer id) throws DbException.InvalidPrimaryKeyException {
 if (integerLessZeroPredicate.test(id)) {
 throw new DbException.InvalidPrimaryKeyException("无效的主键,主键不能为空");
 } Student student = getById(id);
 return mapper.map(student, StudentVo.class);
 } }
 
- 创建单元测试,Mock 一切。 - class StudentServiceImplTest { @Spy
 @InjectMocks
 private StudentServiceImpl studentService; @Mock
 private Mapper mapper; @Mock
 private StudentRepository studentRepository; @BeforeEach
 public void setUp() {
 MockitoAnnotations.initMocks(this);
 } @Test
 public void testCreateStudent_NullName_ShouldThrowException() {
 CreateStudentDto createStudentDto = new CreateStudentDto("", "一些测试地址");
 String msg = Assertions.assertThrows(BizException.ArgumentNullException.class, () -> studentService.create(createStudentDto)).getMessage();
 String expected = "学生名称不能为空,不能创建学生";
 Assertions.assertEquals(expected, msg);
 } @Test
 public void testCreateStudent_NullAddress_ShouldThrowException() {
 CreateStudentDto createStudentDto = new CreateStudentDto("小明", "");
 String msg = Assertions.assertThrows(BizException.ArgumentNullException.class, () -> studentService.create(createStudentDto)).getMessage();
 String expected = "学生住址不能为空,不能创建学生";
 Assertions.assertEquals(expected, msg);
 } @Test
 public void testCreateStudent_ShouldPass() throws BizException.ArgumentNullException {
 CreateStudentDto createStudentDto = new CreateStudentDto("小明", "住址测试"); when(studentService.getBaseMapper()).thenReturn(studentRepository);
 when(studentRepository.insert(any(Student.class))).thenReturn(1);
 Student student = new Student();
 when(mapper.map(createStudentDto, Student.class)).thenReturn(student);
 studentService.create(createStudentDto);
 } @Test
 public void testRetrieve_NullId_ShouldThrowException() {
 String msg = Assertions.assertThrows(DbException.InvalidPrimaryKeyException.class, () -> studentService.retrieve(null)).getMessage();
 String expected = "无效的主键,主键不能为空";
 Assertions.assertEquals(expected, msg);
 } @Test
 public void testRetrieve_ShouldPass() throws DbException.InvalidPrimaryKeyException {
 when(studentService.getBaseMapper()).thenReturn(studentRepository); Integer studentId = 1;
 String studentName = "小明";
 String studentAddress = "学生地址";
 LocalDateTime createTime = LocalDateTime.now();
 LocalDateTime updateTime = LocalDateTime.now();
 Student student = new Student(studentId, studentName, studentAddress, createTime, updateTime);
 when(studentRepository.selectById(studentId)).thenReturn(student);
 StudentVo studentVo = new StudentVo(studentId, studentName, studentAddress, createTime, updateTime);
 when(mapper.map(student, StudentVo.class)).thenReturn(studentVo); StudentVo studentVoReturn = studentService.retrieve(studentId); Assertions.assertEquals(studentId, studentVoReturn.getId());
 Assertions.assertEquals(studentName, studentVoReturn.getName());
 Assertions.assertEquals(studentAddress, studentVoReturn.getAddress());
 Assertions.assertEquals(createTime, studentVoReturn.getCreateTime());
 Assertions.assertEquals(updateTime, studentVoReturn.getUpdateTime());
 }
 }
 - @RunWith(MockitoJUnitRunner.class):添加该 Class 注解,可以自动初始化 @Mock 和 @InjectMocks 注解的对象。
- MockitoAnnotations.initMocks():该方法为 @RunWith(MockitoJUnitRunner.class) 注解的替代品,正常情况下二选一即可。但是我在写单元测试的过程中发现添加 @RunWith(MockitoJUnitRunner.class) 注解不生效。我怀疑和 Junit5 废弃 @Before 注解有关,各位可作为参考。查看源码找到问题是更佳的解决方式。
- @Spy:调用真实方法。
- @Mock:创建一个标注类的 mock 实现。
- @InjectMocks:创建一个标注类的 mock 实现。此外依赖注入 Mock 对象。在上面的实例中 StudentServiceImpl被标注为 @InjectMocks 对象,所以 Mokito 将为StudentServiceImpl创建 Mock 对象,并依赖注入Mapper和StudentRepository对象。
 
- 结果  
集成测试
- 集成测试的目的是测试不同的模块一共工作能否达到预期。
- 集成测试不应该有实际依赖(例如:数据库),而是模拟它们的行为。
- 应用程序应该在 ApplicationContext 中运行。
- Spring boot 提供 @SpringBootTest 注解创建运行上下文。
- 使用 @TestConfiguration 配置测试环境。例如 DataSource。
我们把集成测试集中在 Controller 层。
- 创建 Controller ,语法使用了 Kotlin - Mokito 单元测试与 Spring-Boot 集成测试的更多相关文章- Springboot 系列(一)Spring Boot 入门篇
		注意:本 Spring Boot 系列文章基于 Spring Boot 版本 v2.1.1.RELEASE 进行学习分析,版本不同可能会有细微差别. 前言 由于 J2EE 的开发变得笨重,繁多的配置, ... 
- 笔记:Spring Boot 项目构建与解析
		构建 Maven 项目 通过官方的 Spring Initializr 工具来产生基础项目,访问 http://start.spring.io/ ,如下图所示,该页面提供了以Maven构建Spring ... 
- Spring Boot 系列总目录
		一.Spring Boot 系列诞生原因 上学那会主要学的是 Java 和 .Net 两种语言,当时对于语言分类这事儿没什么概念,恰好在2009年毕业那会阴差阳错的先找到了 .Net 的工作,此后就开 ... 
- Spring Boot - 项目构建与解析
		构建 Maven 项目 通过官方的 Spring Initializr 工具来产生基础项目,访问 http://start.spring.io/ ,如下图所示,该页面提供了以Maven构建Spring ... 
- (2)Spring Boot配置
		文章目录 配置文件 YAML 语法 单元测试 配置文件值自动注入 @Value 获取配置文件属性的值 加载指定配置文件 优先级问题 加载Spring 的配置文件 为容器中添加组件 随机数 & ... 
- Spring Boot 第一弹,问候一下世界!!!
		持续原创输出,点击上方蓝字关注我吧 目录 前言 什么是Spring Boot? 如何搭建一个Spring Boot项目? 第一个程序 Hello World 依赖解读 什么是配置文件? 什么是启动类? ... 
- Spring Boot 的单元测试和集成测试
		学习如何使用本教程中提供的工具,并在 Spring Boot 环境中编写单元测试和集成测试. 1. 概览 本文中,我们将了解如何编写单元测试并将其集成在 Spring Boot 环境中.你可在网上找到 ... 
- 学习 Spring Boot:(二十九)Spring Boot Junit 单元测试
		前言 JUnit 是一个回归测试框架,被开发者用于实施对应用程序的单元测试,加快程序编制速度,同时提高编码的质量. JUnit 测试框架具有以下重要特性: 测试工具 测试套件 测试运行器 测试分类 了 ... 
- Spring Boot实战之单元测试
		Spring Boot实战之单元测试 本文介绍使用Spring测试框架提供的MockMvc对象,对Restful API进行单元测试 Spring测试框架提供MockMvc对象,可以在不需要客户端-服 ... 
 - 随机推荐- js & class & init
			js & class & init how to call class init method in js when create an instance 在初始化类实例的时候调用,类 ... 
- css-next & grid layout
			css-next & grid layout css3 demo https://alligator.io/ @media only screen and (max-width: 30em) ... 
- 星盟全球投资副总裁DENIEL SOIBIM:如何激发创造力
			丹尼尔·索比姆毕业于加州理工大学,2005年通过创建投资俱乐部对潜力公司进行天使投资,获得了美国Blue Run高层的重视,任营收专家评估师,为Blue Run项目提案做风险评估,09年与泰勒·亚当斯 ... 
- Django自学计划之集装箱货物运输物流仓储一站式ERP系统
			业余开始学习时间:2018年1月 业余学习时间段:每天下班晚饭后时间+无事的星期六和星期天+上班时的空闲时间 自学目标: 1.我们要用管理的思维来写我们的系统! 2.我们要用我们的ERP系统帮助中小集 ... 
- Elasticsearch 7.x配置用户名密码访问 开启x-pack验证
			一.修改elasticsearch 配置文件 1.在配置文件中开启x-pack验证 #进入es安装目录下的config目录 vim elasticsearch.yml # 配置X-Pack http. ... 
- Java方法详解
			Java方法详解 什么是方法? Java方法是语句的集合,它们在一起执行一个功能. 方法是解决一类问题的步骤的有序组合 方法包含于类或对象中 方法在程序中被创建,在其他地方被引用 示例: packag ... 
- ReactElement源码笔记
			ReactElement 源码笔记 ReactElement通过 createElement创建,调用该方法需要 传入三个参数: type config children type指代这个ReactE ... 
- Mybatis系列全解(七):全息视角看Dao层两种实现方式之传统方式与代理方式
			封面:洛小汐 作者:潘潘 一直以来 他们都说为了生活 便追求所谓成功 顶级薪水.名牌包包 还有学区房 · 不过 总有人丢了生活 仍一无所获 · 我比较随遇而安 有些事懒得明白 平日里问心无愧 感兴趣的 ... 
- [个人总结]pytorch中model.eval()会对哪些函数有影响?
			来源于知乎:pytorch中model.eval()会对哪些函数有影响? - 蔺笑天的回答 - 知乎 https://www.zhihu.com/question/363144860/answer/9 ... 
- 漏洞复现-CVE-2014-3120-ElasticSearch 命令执行漏洞
			0x00 实验环境 攻击机:Win 10 靶机也可作为攻击机:Ubuntu18 (docker搭建的vulhub靶场) 0x01 影响版本 < ElasticSearch 1.2的版本 ... 
 
- Springboot 系列(一)Spring Boot 入门篇
		
