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 层进行单元测试

工程结构

  1. 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;
    }
  2. 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;
    }
  3. 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);
    } }
  4. 创建单元测试,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 对象,并依赖注入 MapperStudentRepository 对象。
  5. 结果

集成测试

  • 集成测试的目的是测试不同的模块一共工作能否达到预期。
  • 集成测试不应该有实际依赖(例如:数据库),而是模拟它们的行为。
  • 应用程序应该在 ApplicationContext 中运行。
  • Spring boot 提供 @SpringBootTest 注解创建运行上下文。
  • 使用 @TestConfiguration 配置测试环境。例如 DataSource。

我们把集成测试集中在 Controller 层。

  1. 创建 Controller ,语法使用了 Kotlin

    Mokito 单元测试与 Spring-Boot 集成测试的更多相关文章

    1. Springboot 系列(一)Spring Boot 入门篇

      注意:本 Spring Boot 系列文章基于 Spring Boot 版本 v2.1.1.RELEASE 进行学习分析,版本不同可能会有细微差别. 前言 由于 J2EE 的开发变得笨重,繁多的配置, ...

    2. 笔记:Spring Boot 项目构建与解析

      构建 Maven 项目 通过官方的 Spring Initializr 工具来产生基础项目,访问 http://start.spring.io/ ,如下图所示,该页面提供了以Maven构建Spring ...

    3. Spring Boot 系列总目录

      一.Spring Boot 系列诞生原因 上学那会主要学的是 Java 和 .Net 两种语言,当时对于语言分类这事儿没什么概念,恰好在2009年毕业那会阴差阳错的先找到了 .Net 的工作,此后就开 ...

    4. Spring Boot - 项目构建与解析

      构建 Maven 项目 通过官方的 Spring Initializr 工具来产生基础项目,访问 http://start.spring.io/ ,如下图所示,该页面提供了以Maven构建Spring ...

    5. (2)Spring Boot配置

      文章目录 配置文件 YAML 语法 单元测试 配置文件值自动注入 @Value 获取配置文件属性的值 加载指定配置文件 优先级问题 加载Spring 的配置文件 为容器中添加组件 随机数 & ...

    6. Spring Boot 第一弹,问候一下世界!!!

      持续原创输出,点击上方蓝字关注我吧 目录 前言 什么是Spring Boot? 如何搭建一个Spring Boot项目? 第一个程序 Hello World 依赖解读 什么是配置文件? 什么是启动类? ...

    7. Spring Boot 的单元测试和集成测试

      学习如何使用本教程中提供的工具,并在 Spring Boot 环境中编写单元测试和集成测试. 1. 概览 本文中,我们将了解如何编写单元测试并将其集成在 Spring Boot 环境中.你可在网上找到 ...

    8. 学习 Spring Boot:(二十九)Spring Boot Junit 单元测试

      前言 JUnit 是一个回归测试框架,被开发者用于实施对应用程序的单元测试,加快程序编制速度,同时提高编码的质量. JUnit 测试框架具有以下重要特性: 测试工具 测试套件 测试运行器 测试分类 了 ...

    9. Spring Boot实战之单元测试

      Spring Boot实战之单元测试 本文介绍使用Spring测试框架提供的MockMvc对象,对Restful API进行单元测试 Spring测试框架提供MockMvc对象,可以在不需要客户端-服 ...

    随机推荐

    1. javascript IIFE in depth

      javascript IIFE in depth function type 函数表达式 x = function (){ console.log(x); } ƒ (){ console.log(x) ...

    2. React In Depth

      React In Depth React Component Lifecycle https://reactjs.org/docs/react-component.html https://react ...

    3. vscode & peacock extension

      vscode & peacock extension https://marketplace.visualstudio.com/items?itemName=johnpapa.vscode-p ...

    4. js 最简单的发布订阅模式

      let _subscriber: any; function autorun(subscriber: Function) { _subscriber = subscriber; _subscriber ...

    5. 备战春招!开源社区系统 Echo 超全文档助力面试

      博主东南大学硕士在读,寒假前半个月到现在差不多一个多月,断断续续做完了这个项目,现在终于可以开源出来了,我的想法是为这个项目编写一套完整的教程,包括技术选型分析.架构分析.业务逻辑分析.核心技术点分析 ...

    6. 10_MySQL数据表的基本查询

      为了更好的练习数据表的操作,我们需要有些数据来供我们练习,这里给大家分享一份数据,大家可以拿去自己练习使用. 文件地址:https://files.cnblogs.com/files/waterr/d ...

    7. C++使用libcurl进行http通讯

      借着curl 7.75.0版本更新, 最近又下载下来玩了玩, 在此做个简单记录 1.环境搭建 首先是libcurl动态库, 自己下载源码编译的话如果要使用https协议还要下载OpenSSL和libs ...

    8. redis slowlog 慢查询日志

      设置 config set slowlog-log-slower-than 10000(微秒) //查看redis时间超过上面设置的阀值的key slowlog len 有几个key slowlog ...

    9. 痞子衡嵌入式:盘点国内RISC-V内核MCU厂商

      大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是国内RISC-V内核MCU厂商. 虽然RISC-V风潮已经吹了好几年,但2019年才是其真正进入主流市场的元年,最近国内大量芯片公司崛起 ...

    10. 【转载】Android异步消息处理机制详解及源码分析

      PS一句:最终还是选择CSDN来整理发表这几年的知识点,该文章平行迁移到CSDN.因为CSDN也支持MarkDown语法了,牛逼啊! [工匠若水 http://blog.csdn.net/yanbob ...