使用Spring Boot进行单元测试详情

目录
  • 前言
  • 使用 Spring Boot 进行测试系列文章
  • 依赖项
  • 不要在单元测试中使用Spring
  • 创建一个可测试的类实例
    • 属性注入是不好的
    • 提供一个构造函数
    • 减少模板代码
  • 使用Mockito来模拟依赖项
    • 使用普通Mockito来模拟依赖
    • 通过Mockito的@Mock注解模拟对象
  • 使用AssertJ创建可读断言
  • 结论

前言

本文给你提供在Spring Boot 应用程序中编写好的单元测试的机制,并且深入技术细节。

我们将带你学习如何以可测试的方式创建Spring Bean实例,然后讨论如何使用MockitoAssertJ,这两个包在Spring Boot中都为了测试默认引用了。

本文只讨论单元测试。至于集成测试,测试web层和测试持久层将会在接下来的系列文章中进行讨论。

使用 Spring Boot 进行测试系列文章

这个教程是一个系列:

  • 使用 Spring Boot 进行单元测试(本文)
  • 使用 Spring Boot 和 @WebMvcTest 测试SpringMVC controller层
  • 使用 Spring Boot 和 @DataJpaTest 测试JPA持久层查询
  • 通过 @SpringBootTest 进行集成测试

如果你喜欢看视频教程,可以看看Philip的课程:测试Spring Boot应用程序课程

依赖项

本文中,为了进行单元测试,我们会使用JUnit Jupiter(Junit 5)MockitoAssertJ。此外,我们会引用Lombok来减少一些模板代码:

dependencies{
  compileOnly('org.projectlombok:lombok')
  testCompile('org.springframework.boot:spring-boot-starter-test')
  testCompile 'org.junit.jupiter:junit-jupiter-engine:5.2.0'
  testCompile('org.mockito:mockito-junit-jupiter:2.23.0')
}

MockitoAssertJ会在spring-boot-test依赖中自动引用,但是我们需要自己引用Lombok

不要在单元测试中使用Spring

如果你以前使用Spring或者Spring Boot写过单元测试,你可能会说我们不要在写单元测试的时候用Spring。但是为什么呢?

考虑下面的单元测试类,这个类测试了RegisterUseCase类的单个方法:

@ExtendWith(SpringExtension.class)
@SpringBootTest
class RegisterUseCaseTest {

  @Autowired
  private RegisterUseCase registerUseCase;

  @Test
  void savedUserHasRegistrationDate() {
    User user = new User("zaphod", "zaphod@mail.com");
    User savedUser = registerUseCase.registerUser(user);
    assertThat(savedUser.getRegistrationDate()).isNotNull();
  }

}

这个测试类在我的电脑上需要大概4.5秒来执行一个空的Spring项目。

但是一个好的单元测试仅仅需要几毫秒。否则就会阻碍TDD(测试驱动开发)流程,这个流程倡导“测试/开发/测试”。

但是就算我们不使用TDD,等待一个单元测试太久也会破坏我们的注意力。

执行上述的测试方法事实上仅需要几毫秒。剩下的4.5秒是因为@SpringBootTest告诉了 Spring Boot 要启动整个Spring Boot 应用程序上下文。

所以我们启动整个应用程序仅仅是因为要把RegisterUseCase实例注入到我们的测试类中。启动整个应用程序可能耗时更久,假设应用程序更大、Spring需要加载更多的实例到应用程序上下文中。

所以,这就是为什么不要在单元测试中使用Spring。坦白说,大部分编写单元测试的教程都没有使用Spring Boot

创建一个可测试的类实例

然后,为了让Spring实例有更好的测试性,有几件事是我们可以做的。

属性注入是不好的

让我们以一个反例开始。考虑下述类:

@Service
public class RegisterUseCase {

  @Autowired
  private UserRepository userRepository;

  public User registerUser(User user) {
    return userRepository.save(user);
  }

}

这个类如果没有Spring没法进行单元测试,因为它没有提供方法传递UserRepository实例。因此我们只能用文章之前讨论的方式-让Spring创建UserRepository实例,并通过@Autowired注解注入进去。

这里的教训是:不要用属性注入。

提供一个构造函数

实际上,我们根本不需要使用@Autowired注解:

@Service
public class RegisterUseCase {

  private final UserRepository userRepository;

  public RegisterUseCase(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public User registerUser(User user) {
    return userRepository.save(user);
  }

}

这个版本通过提供一个允许传入UserRepository实例参数的构造函数来允许构造函数注入。在这个单元测试中,我们现在可以创建这样一个实例(或者我们之后要讨论的Mock实例)并通过构造函数注入了。

当创建生成应用上下文的时候,Spring会自动使用这个构造函数来初始化RegisterUseCase对象。注意,在Spring 5 之前,我们需要在构造函数上增加@Autowired注解,以便让Spring找到这个构造函数。

还要注意的是,现在UserRepository属性是final修饰的。这很重要,因为这样的话,应用程序生命周期时间内这个属性内容不会再变化。此外,它还可以帮我们避免变成错误,因为如果我们忘记初始化该属性的话,编译器就报错。

减少模板代码

通过使用Lombok@RequiredArgsConstructor注解,我们可以让构造函数自动生成:

@Service
@RequiredArgsConstructor
public class RegisterUseCase {

  private final UserRepository userRepository;

  public User registerUser(User user) {
    user.setRegistrationDate(LocalDateTime.now());
    return userRepository.save(user);
  }

}

现在,我们有一个非常简洁的类,没有样板代码,可以在普通的 java 测试用例中很容易被实例化:

class RegisterUseCaseTest {

  private UserRepository userRepository = ...;

  private RegisterUseCase registerUseCase;

  @BeforeEach
  void initUseCase() {
    registerUseCase = new RegisterUseCase(userRepository);
  }

  @Test
  void savedUserHasRegistrationDate() {
    User user = new User("zaphod", "zaphod@mail.com");
    User savedUser = registerUseCase.registerUser(user);
    assertThat(savedUser.getRegistrationDate()).isNotNull();
  }

}

还有部分确实,就是如何模拟测试类所依赖的UserReposity实例,我们不想依赖真实的类,因为这个类需要一个数据库连接。

使用Mockito来模拟依赖项

现在事实上的标准模拟库是 Mockito。它提供至少两种方式来创建一个模拟UserRepository实例,来填补前述代码的空白。

使用普通Mockito来模拟依赖

第一种方式是使用Mockito编程:

private UserRepository userRepository = Mockito.mock(UserRepository.class);

这会从外界创建一个看起来像UserRepository的对象。默认情况下,方法被调用时不会做任何事情,如果方法有返回值,会返回null

因为userRepository.save(user)返回null,现在我们的测试代码assertThat(savedUser.getRegistrationDate()).isNotNull()会报空指针异常(NullPointerException)。

所以我们需要告诉Mockito,当userRepository.save(user)调用的时候返回一些东西。我们可以用静态的when方法实现:

@Test
void savedUserHasRegistrationDate() {
  User user = new User("zaphod", "zaphod@mail.com");
  when(userRepository.save(any(User.class))).then(returnsFirstArg());
  User savedUser = registerUseCase.registerUser(user);
  assertThat(savedUser.getRegistrationDate()).isNotNull();
}

这会让userRepository.save()返回和传入对象相同的对象。

Mockito为了模拟对象、匹配参数以及验证方法调用,提供了非常多的特性。想看更多,文档

通过Mockito@Mock注解模拟对象

创建一个模拟对象的第二种方式是使用Mockito@Mock注解结合 JUnit Jupiter的MockitoExtension一起使用:

@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {

  @Mock
  private UserRepository userRepository;

  private RegisterUseCase registerUseCase;

  @BeforeEach
  void initUseCase() {
    registerUseCase = new RegisterUseCase(userRepository);
  }

  @Test
  void savedUserHasRegistrationDate() {
    // ...
  }

}

@Mock注解指明那些属性需要Mockito注入模拟对象。由于JUnit不会自动实现,MockitoExtension则告诉Mockito来评估这些@Mock注解。

这个结果和调用Mockito.mock()方法一样,凭个人品味选择即可。但是请注意,通过使用 MockitoExtension,我们的测试用例被绑定到测试框架。

我们可以在RegisterUseCase属性上使用@InjectMocks注解来注入实例,而不是手动通过构造函数构造。Mockito会使用特定的算法来帮助我们创建相应实例对象:

@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {

  @Mock
  private UserRepository userRepository;

  @InjectMocks
  private RegisterUseCase registerUseCase;

  @Test
  void savedUserHasRegistrationDate() {
    // ...
  }

}

使用AssertJ创建可读断言

Spring Boot 测试包自动附带的另一个库是AssertJ。我们在上面的代码中已经用到它进行断言:

assertThat(savedUser.getRegistrationDate()).isNotNull();

然而,有没有可能让断言可读性更强呢?像这样,例子:

assertThat(savedUser).hasRegistrationDate();

有很多测试用例,只需要像这样进行很小的改动就能大大提高可理解性。所以,让我们在test/sources中创建我们自定义的断言吧:

class UserAssert extends AbstractAssert<UserAssert, User> {

  UserAssert(User user) {
    super(user, UserAssert.class);
  }

  static UserAssert assertThat(User actual) {
    return new UserAssert(actual);
  }

  UserAssert hasRegistrationDate() {
    isNotNull();
    if (actual.getRegistrationDate() == null) {
      failWithMessage(
        "Expected user to have a registration date, but it was null"
      );
    }
    return this;
  }
}

现在,如果我们不是从AssertJ库直接导入,而是从我们自定义断言类UserAssert引入assertThat方法的话,我们就可以使用新的、更可读的断言。

创建一个这样自定义的断言类看起来很费时间,但是其实几分钟就完成了。我相信,将这些时间投入到创建可读性强的测试代码中是值得的,即使之后它的可读性只有一点点提高。我们编写测试代码就一次,但是之后,很多其他人(包括未来的我)在软件生命周期中,需要阅读、理解然后操作这些代码很多次。

如果你还是觉得很费事,可以看看断言生成器

结论

尽管在测试中启动Spring应用程序也有些理由,但是对于一般的单元测试,它不必要。有时甚至有害,因为更长的周转时间。换言之,我们应该使用更容易支持编写普通单元测试的方式构建Spring实例。

Spring Boot Test Starter附带MockitoAssertJ作为测试库。让我们利用这些测试库来创建富有表现力的单元测试!

到此这篇关于使用Spring Boot进行单元测试详情的文章就介绍到这了,更多相关Spring Boot单元测试内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringBoot单元测试没有执行的按钮问题及解决

    目录 单元测试没有执行的按钮 问题说明 解决方法 单元测试没有启动按钮(另辟蹊径) 大致如下图(红圈处,没有启动按钮) 现状 转机 结论 单元测试没有执行的按钮 问题说明 在搭建SpringBoot项目单元测试中,突然发现没有执行的按钮,如是,我使用鼠标右键,强行执行该测试方法.结果报错. 报错信息: The class com.example.demo.DemoApplicationTests is not public. idea给的建议: Test class should have ex

  • IDEA 单元测试报错:Class not found:xxxx springboot的解决

    目录 IDEA单元测试报错:Class not found:xxxx springboot 报错 解决 让人抓狂的ClassNotFoundException 启动项目时,始终出现如下错误提示 在项目的pom文件,添加了以下代码 IDEA单元测试报错:Class not found:xxxx springboot 报错 引入了新依赖,想着在测试模块进行测试. 结果报错说 Class not found:xxxx 说找不到这个类. 解决 把安装了新依赖的父工程 重新 install一下 再进行测试

  • springboot集成junit编写单元测试实战

    目录 一:查看jar包版本号是否为junit4: 二:实战应用: 三:扩展 在做单元测试时,代码覆盖率常常被拿来作为衡量测试好坏的指标,甚至,用代码覆盖率来考核测试任务完成情况,比如,代码覆盖率必须达到80%或 90%.于是乎,测试人员费尽心思设计案例覆盖代码.用代码覆盖率来衡量,有利也有弊. 首先,让我们先来了解一下所谓的“代码覆盖率”.我找来了所谓的定义:代码覆盖率 = 代码的覆盖程度,一种度量方式. 一:查看jar包版本号是否为junit4: junit自身注解: @BeforeClass

  • springboot打包如何忽略Test单元测试

    springboot打包忽略Test单元测试 在maven pom.xml中加入配置: <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.20.1</version> <configuration> <skipTests>true

  • SpringBoot单元测试使用@Test没有run方法的解决方案

    目录 SpringBoot单元测试使用@Test没有run方法 原因找到了 SpringBoot写单元测试遇到的坑 SpringBoot怎么写单元测试 SpringBoot使用Mockito进行单元测试 通过真实测试用例测试代码 SpringBoot单元测试使用@Test没有run方法 吐了!一个关键字,纠错两小时,看了十几篇博客....最后重新建测试类发现@Test又有用,结果发现是因为默认的Tests测试类没有public关键字! 这个破错改了两小时... ==后续来了:== 原因找到了 建

  • 使用Spring Boot进行单元测试详情

    目录 前言 使用 Spring Boot 进行测试系列文章 依赖项 不要在单元测试中使用Spring 创建一个可测试的类实例 属性注入是不好的 提供一个构造函数 减少模板代码 使用Mockito来模拟依赖项 使用普通Mockito来模拟依赖 通过Mockito的@Mock注解模拟对象 使用AssertJ创建可读断言 结论 前言 本文给你提供在Spring Boot 应用程序中编写好的单元测试的机制,并且深入技术细节. 我们将带你学习如何以可测试的方式创建Spring Bean实例,然后讨论如何使

  • Spring boot 集成 MQTT详情

    目录 一.简介 二.主要特性 三.集成步骤 1.引入相关jar包 2.核心配置类 3.网关配置 4.编写测试类 5.yml配置信息 一.简介 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的"轻量级"通讯协议,可以以极少的代码和有限的带宽为连接远程设备提供实时可靠的消息服务.目前在物联网.小型设备.移动应用等方面有较广泛的应用. 二.主要特性 (1)使用发布/订阅消

  • Spring Boot 条件注解详情

    目录 一 @Conditional扩展注解 1.1 Bean作为条件 1.1.1 @ConditionalOnBean 1.1.2 @ConditionalOnMissingBean 1.1.3 @ConditionalOnSingleCandidate 1.2 类作为条件 1.2.1 @ConditionalOnClass 1.2.2 @ConditionalOnMissingClass 1.3 SpEL表达式作为条件 1.4 JAVA版本作为判断条件 1.5 配置属性作为判断条件 1.6 资

  • 详解Spring Boot Junit单元测试

    Junit这种老技术,现在又拿出来说,不为别的,某种程度上来说,更是为了要说明它在项目中的重要性. 凭本人的感觉和经验来说,在项目中完全按标准都写Junit用例覆盖大部分业务代码的,应该不会超过一半. 刚好前段时间写了一些关于SpringBoot的帖子,正好现在把Junit再拿出来从几个方面再说一下,也算是给一些新手参考了. 那么先简单说一下为什么要写测试用例 1. 可以避免测试点的遗漏,为了更好的进行测试,可以提高测试效率 2. 可以自动测试,可以在项目打包前进行测试校验 3. 可以及时发现因

  • Spring Boot从Controller层进行单元测试的实现

    单元测试是程序员对代码的自测,一般公司都会严格要求单元测试,这是对自己代码的负责,也是对代码的敬畏. 一般单元测试都是测试Service层,下面我将演示从Controller层进行单元测试. 无参Controller单元测试示例: package com.pingan.bloan.genesis.controller.base; import org.junit.After; import org.junit.Before; import org.junit.runner.RunWith; im

  • 玩转spring boot 快速开始(1)

    开发环境: IED环境:Eclipse JDK版本:1.8 maven版本:3.3.9 一.创建一个spring boot的mcv web应用程序 打开Eclipse,新建Maven项目 选择quickstart模板 完成Maven项目的创建 参照spring的官方例子:http://spring.io/guides/gs/testing-web/ 在pom.xml增加maven依赖 <project xmlns="http://maven.apache.org/POM/4.0.0&quo

  • Spring Boot 整合 Thymeleaf 实例分享

    目录 一.什么是 Thymeleaf 二.整合过程 准备过程 添加 Thymeleaf 依赖 编写实体类和 Controller 创建Thymeleaf 模板 三.测试 一.什么是 Thymeleaf Thymeleaf 是新一代的 Java 模板引擎,类似于 Velocity.FreeMarker 等传统引擎,其语言和 HTML 很接近,而且扩展性更高: Thymeleaf 的主要目的是将优雅的模板引入开发工作流程中,并将 HTML 在浏览器中正确显示.同时能够作为静态引擎,让开发成员之间更方

  • Spring Boot MQTT Too many publishes in progress错误的解决方案

    目录 前言 原因分析 源码分析 MQTT的Push消息到缓存中时序图 MqttPahoMessageHandler的publish方法 MqttAsyncClient的publish方法 ClientComms的internalSend方法 ClientState的send方法 异步发送消息时序图 ClientComms的conncect方法 ConnectBG的run方法 CommsSender的run方法 CommsSender的notifySent方法 小结 解决方案 方案1:发送消息时设

  • 详解Spring Boot实战之单元测试

    本文介绍使用Spring测试框架提供的MockMvc对象,对Restful API进行单元测试 Spring测试框架提供MockMvc对象,可以在不需要客户端-服务端请求的情况下进行MVC测试,完全在服务端这边就可以执行Controller的请求,跟启动了测试服务器一样. 测试开始之前需要建立测试环境,setup方法被@Before修饰.通过MockMvcBuilders工具,使用WebApplicationContext对象作为参数,创建一个MockMvc对象. MockMvc对象提供一组工具

  • Spring Boot 单元测试JUnit的实践

    一.介绍 JUnit是一款优秀的开源Java单元测试框架,也是目前使用率最高最流行的测试框架,开发工具Eclipse和IDEA对JUnit都有很好的支持,JUnit主要用于白盒测试和回归测试. <!--more--> 白盒测试:把测试对象看作一个打开的盒子,程序内部的逻辑结构和其他信息对测试人 员是公开的: 回归测试:软件或环境修复或更正后的再测试: 单元测试:最小粒度的测试,以测试某个功能或代码块.一般由程序员来做,因为它需要知道内部程序设计和编码的细节: JUnit GitHub地址:ht

随机推荐