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. Dart Web

      Dart Web Dart for Web https://dart.dev/platforms dart2js xgqfrms 2012-2020 www.cnblogs.com 发布文章使用:只允 ...

    2. vue页面切换过渡

      <!--<transition name="slide-left">--> <router-view></router-view>& ...

    3. 【快速掌握】Redis 的五种数据类型

      不同于MySQL的表结构所带来的复杂语句,Redis只需要维护好它的[key-value]结构就可以,因此相比于MySQL,语句非常简单. 今天介绍一下Redis 五种常用的数据类型: 这五种数据类型 ...

    4. springboot学习过程随记

      1.整合shiro+jwt(若忘记需结合测试代码springboot-mybatisplus-shiro-demo看) 配置比较简单 定义一个类继承AuthorizingRealm 如下: (1)pu ...

    5. 喜忧参半的SQL Server触发器

      SQL Server触发器在非常有争议的主题.它们能以较低的成本提供便利,但经常被开发人员.DBA误用,导致性能瓶颈或维护性挑战. 本文简要回顾了触发器,并深入讨论了如何有效地使用触发器,以及何时触发 ...

    6. Maven 打包项目到私服 (deploy)

      一.配置maven 在maven安装目录 /conf/setting.xml 中的servers下添加: 1 <servers> 2 <server> 3 <id> ...

    7. Java流程控制:循环结构

      一.简介 顺序结构的程序语句只能被执行一次,如果您想要同样的操作执行多次,就需要使用循环结构. Java中有三种主要的循环结构: 'while'循环 'do...while'循环 'for'循环 在J ...

    8. Docker私有仓库的搭建与使用

      目录 Docker搭建私有仓库 一.搭建registry私有仓库 1.拉取私有仓库registry镜像 2.启动私有仓库容器 3.修改deamon.json 4.重启docker 5.启动regist ...

    9. 授权认证登录之 Cookie、Session、Token、JWT 详解

      一.先了解几个基础概念 什么是认证(Authentication) 通俗地讲就是验证当前用户的身份. 互联网中的认证: 用户名密码登录 邮箱发送登录链接 手机号接收验证码 只要你能收到邮箱/验证码,就 ...

    10. 如何用Eggjs从零开始开发一个项目(1)

      前言 "纸上得来终觉浅,绝知此事要躬行."虽然node一直在断断续续地学,但总是东一榔头西一榔头的,没有一点系统,所以打算写一个项目来串联一下之前的学习成果. 为什么选择Eggjs ...