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. perl 打印目录结构

      更多 #!/usr/bin/perl # 递归打印目录结构 use v5.26; use strict; use utf8; use autodie; use warnings; use Encode ...

    2. 亿级流量客户端缓存之Http缓存与本地缓存对比

      客户端缓存分为Http缓存和本地缓存,使用缓存好处很多,例如减少相同数据的重复传输,节省网络带宽资源缓解网络瓶颈,降低了对原始服务器的要求,避免出现过载,这样服务器可以更快响应其他的请求 Http缓存 ...

    3. [C语言学习笔记五]复合语句和操作符的区分

      复合语句的概念和用法 在部分时候,语句必须要与其他语句相结合才能实现应有的功能.放在花括号 {} 里的代码叫做复合语句. 例如: int a,b; if (a == b) ... ... /* 这一部 ...

    4. 详解SSH 框架中对象调用流程

      摘要:SSH=Struts+Spring+Hibernate SSH不是一个框架,而是多个框架(struts+spring+hibernate)的集成,是目前较流行的一种Web应用程序开源集成框架,用 ...

    5. 【死磕JVM】JVM快速入门之前戏篇

      简介 Java是一门可以跨平台的语言,但是Java本身是不可以实现跨平台的,需要JVM实现跨平台.javac编译好后的class文件,在Windows.Linux.Mac等系统上,只要该系统安装对应的 ...

    6. 元类、orm

      目录 一.内置函数exec 二.元类 1. 什么是元类 2. 元类的作用 3. 创建类的两种方法 4. 怎么自定义创建元类 三.ORM 1. ORM中可能会遇到的问题 2. ORM中元类需要解决的问题 ...

    7. finally会执行吗:try/catch的测试

      翻译练习 原博客地址:Will it finally: a try/catch quiz 你知道try和catch是怎么工作的,但是你知道finally是怎么工作的吗?它是在抛出异常后执行还是在ret ...

    8. 【ZeyFraのJavaEE开发小知识02】MybatisPlus&ElementUI

      1.关于如何获得Mybatis-Plus在插入对应为自增长主键但并未对该主键赋值的实体类之后其主键值 对应数据库中某张表并未设置主键值,但其主键为自增长类型的实体类,在使用Mybatis-Plus做i ...

    9. 如何在 ASP.Net Core 中使用 Serilog

      记录日志的一个作用就是方便对应用程序进行跟踪和排错调查,在实际应用上都是引入 日志框架,但如果你的 日志文件 包含非结构化的数据,那么查询起来将是一个噩梦,所以需要在记录日志的时候采用结构化方式. 将 ...

    10. Go Module实战:基于私有化仓库的GO模块使用实践

      新年开工近一月,2021 年第一期 Open Talk 定档 3 月 18 日晚 8 点,本期我们邀请到了又拍云资深后端开发工程师刘云鹏和我们一起聊聊 Go 最新特性 Go Module 实战. 刘云 ...