使用 Spring Boot 进行单元测试
【注】本文译自: Unit Testing with Spring Boot - Reflectoring
编写好的单元测试可以被认为是一门难以掌握的艺术。但好消息是支持它的机制很容易学习。
本教程为您提供了这些机制,并详细介绍了编写良好的单元测试所必需的技术细节,重点是 Spring Boot 应用程序。
我们将看看如何以可测试的方式创建 Spring bean,然后讨论 Mockito 和 AssertJ 的用法,这两个库默认包含在 Spring Boot 中用于测试。
请注意,本文仅讨论单元测试。集成测试、Web 层测试和持久层测试将在本系列的后续文章中讨论。
代码示例
本文附有 GitHub 上 的工作代码示例。
依赖关系
对于本教程中的单元测试,我们将使用 JUnit Jupiter (JUnit 5)、Mockito 和 AssertJ。我们还将包括 Lombok 以减少一些样板代码:
Mockito 和 AssertJ 是使用 spring-boot-starter-test
依赖项自动导入的,但我们必须自己包含 Lombok。不要在单元测试中使用 Spring 如果你以前用 Spring 或 Spring Boot 写过测试,你可能会说我们不需要 Spring 来写单元测试。这是为什么?考虑以下测试 RegisterUseCase
类的单个方法的“单元”测试:
这个测试在我电脑上的一个空 Spring 项目上运行大约需要 4.5 秒。
但是一个好的单元测试只需要几毫秒。否则它会阻碍由测试驱动开发(TDD)思想推动的“测试/代码/测试”流程。但即使我们不采用 TDD,等待太长时间的测试也会破坏我们的注意力。
执行上面的测试方法实际上只需要几毫秒。 剩下的 4.5 秒是由于 @SpringBootRun
告诉 Spring Boot 设置整个 Spring Boot 应用程序上下文。
所以我们启动了整个应用程序只是为了将 RegisterUseCase
实例自动装配到我们的测试中。一旦应用程序变大并且 Spring 不得不将越来越多的 bean 加载到应用程序上下文中,它将花费更长的时间。那么,为什么我们不应该在单元测试中使用 Spring Boot 呢?老实说,本教程的大部分内容都是关于在没有 Spring Boot 的情况下编写单元测试。
创建可测试的 Spring Bean
然而,我们可以做一些事情来提高 Spring bean 的可测试性。
字段注入是不可取的
让我们从一个不好的例子开始。考虑以下类:
这个类不能在没有 Spring 的情况下进行单元测试,因为它没有提供传递 UserRepository
实例的方法。那么,我们需要按照上一节中讨论的方式编写测试,让 Spring 创建一个 UserRepository
实例并将其注入到用 @Autowired
注解的字段中。这里的教训是不要使用字段注入。
提供构造函数
实际上,我们根本不要使用 @Autowired
注解:
这个版本通过提供允许传入 UserRepository
实例的构造函数来允许构造函数注入。在单元测试中,我们现在可以创建这样一个实例(可能是我们稍后讨论的模拟实例)并将其传递给构造函数。
在创建生产应用程序上下文时,Spring 将自动使用此构造函数来实例化 RegisterUseCase
对象。注意,在 Spring 5 之前,我们需要在构造函数中添加 @Autowired
注解,以便 Spring 找到构造函数。
还要注意 UserRepository
字段现在是 final
。这是有道理的,因为字段内容在应用程序的生命周期内永远不会改变。它还有助于避免编程错误,因为如果我们忘记初始化字段,编译器会报错。
减少样板代码
使用 Lombok 的 @RequiredArgsConstructor 注解,我们可以让构造函数自动生成:
现在,我们有一个非常简洁的类,没有样板代码,可以在普通的 java 测试用例中轻松实例化:
然而,还缺少一点,那就是如何模拟我们被测类所依赖的 UserRepository
实例,因为我们不想依赖真实的东西,它可能需要连接到数据库。使用 Mockito 来模拟依赖现在事实上的标准模拟库是 Mockito。它至少提供了两种方法来创建模拟的 UserRepository 以填补前面代码示例中的空白。
使用普通 Mockito 模拟依赖项
第一种方法是以编程方式使用 Mockito:
这将创建一个从外部看起来像 UserRepository
的对象。默认情况下,当一个方法被调用时它什么都不做,如果该方法有返回值则返回 null
。我们的测试现在将在 assertThat(savedUser.getRegistrationDate()).isNotNull()
处以 NullPointerException
失败,因为 userRepository.save(user)
现在返回 null
。所以,我们必须告诉 Mockito 在调用 userRepository.save()
时返回一些东西。我们使用静态 when 方法来做到这一点:
这将使 userRepository.save()
返回传递给方法的相同用户对象。Mockito 具有更多功能,可以进行模拟、匹配参数和验证方法调用。有关更多信息,请查看参考文档。
使用 Mockito 的 @Mock 注解模拟依赖项
创建模拟对象的另一种方法是 Mockito 的 @Mock
注解与 JUnit Jupiter 的 MockitoExtension
相结合:
@Mock
注解指定了 Mockito 应该注入模拟对象的字段。 @MockitoExtension
告诉 Mockito 评估那些 @Mock
注解,因为 JUnit 不会自动执行此操作。
结果和手动调用 Mockito.mock()
一样,选择使用哪种方式是品味问题。 但是请注意,通过使用 MockitoExtension
将我们的测试绑定到测试框架。
请注意,我们也可以在 registerUseCase
字段上使用 @InjectMocks
注解,而不是手动构造 RegisterUseCase
对象。然后 Mockito 会按照指定的算法为我们创建一个实例:
使用 AssertJ 创建可读断言
Spring Boot 测试支持自动附带的另一个库是 AssertJ。我们已经在上面使用它来实现我们的断言:
然而,让断言更具可读性不是更好吗?例如:
在很多情况下,像这样的小改动会使测试更容易理解。因此,让我们在测试源文件夹中创建我们自己的自定义断言:
现在,如果我们从新的 UserAssert
类而不是从 AssertJ 库导入 assertThat
方法,我们就可以使用新的、更易于阅读的断言。
创建像这样的自定义断言似乎需要很多工作,但实际上只需几分钟即可完成。我坚信投入这些时间来创建可读的测试代码是值得的,即使之后它的可读性只是稍微好一点。毕竟,我们只编写一次测试代码,其他人(包括“未来的我”)必须在软件的生命周期中多次阅读、理解和操作代码。
如果仍然觉得工作量太大,请查看 AssertJ 的断言生成器。
结论
在测试中启动 Spring 应用程序是有原因的,但对于普通的单元测试来说,这是没有必要的。由于更长的周转时间,它甚至是有害的。相反,我们应该以一种易于支持为其编写简单单元测试的方式构建我们的 Spring bean。
Spring Boot Test Starter 附带 Mockito 和 AssertJ 作为测试库。让我们利用这些测试库来创建富有表现力的单元测试!最终形式的代码示例可在 github 上 找到。
版权声明: 本文为 InfoQ 作者【信码由缰】的原创文章。
原文链接:【http://xie.infoq.cn/article/853e5477a3c44c70b0ebc9e92】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论