使用|使用 Spring Boot 进行单元测试
【使用|使用 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 以减少一些样板代码:
dependencies {
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.junit.jupiter:junit-jupiter:5.4.0')
testCompile('org.mockito:mockito-junit-jupiter:2.23.0')
}
Mockito 和 AssertJ 是使用
spring-boot-starter-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();
}}
这个测试在我电脑上的一个空 Spring 项目上运行大约需要 4.5 秒。
但是一个好的单元测试只需要几毫秒。否则它会阻碍由测试驱动开发(TDD)思想推动的“测试/代码/测试”流程。但即使我们不采用 TDD,等待太长时间的测试也会破坏我们的注意力。
执行上面的测试方法实际上只需要几毫秒。 剩下的 4.5 秒是由于
@SpringBootRun
告诉 Spring Boot 设置整个 Spring Boot 应用程序上下文。所以我们启动了整个应用程序只是为了将
RegisterUseCase
实例自动装配到我们的测试中。一旦应用程序变大并且 Spring 不得不将越来越多的 bean 加载到应用程序上下文中,它将花费更长的时间。那么,为什么我们不应该在单元测试中使用 Spring Boot 呢?老实说,本教程的大部分内容都是关于在没有 Spring Boot 的情况下编写单元测试。
创建可测试的 Spring Bean 然而,我们可以做一些事情来提高 Spring bean 的可测试性。
字段注入是不可取的 让我们从一个不好的例子开始。考虑以下类:
@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
实例的构造函数来允许构造函数注入。在单元测试中,我们现在可以创建这样一个实例(可能是我们稍后讨论的模拟实例)并将其传递给构造函数。在创建生产应用程序上下文时,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();
}}
然而,还缺少一点,那就是如何模拟我们被测类所依赖的
UserRepository
实例,因为我们不想依赖真实的东西,它可能需要连接到数据库。使用 Mockito 来模拟依赖 现在事实上的标准模拟库是 Mockito。它至少提供了两种方法来创建模拟的
UserRepository
以填补前面代码示例中的空白。使用普通 Mockito 模拟依赖项 第一种方法是以编程方式使用 Mockito:
private UserRepository userRepository = Mockito.mock(UserRepository.class);
这将创建一个从外部看起来像
UserRepository
的对象。默认情况下,当一个方法被调用时它什么都不做,如果该方法有返回值则返回 null
。我们的测试现在将在
assertThat(savedUser.getRegistrationDate()).isNotNull()
处以 NullPointerException
失败,因为 userRepository.save(user)
现在返回 null
。所以,我们必须告诉 Mockito 在调用
userRepository.save()
时返回一些东西。我们使用静态 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 应该注入模拟对象的字段。 @MockitoExtension
告诉 Mockito 评估那些 @Mock
注解,因为 JUnit 不会自动执行此操作。结果和手动调用
Mockito.mock()
一样,选择使用哪种方式是品味问题。 但是请注意,通过使用 MockitoExtension
将我们的测试绑定到测试框架。请注意,我们也可以在
registerUseCase
字段上使用 @InjectMocks
注解,而不是手动构造 RegisterUseCase
对象。然后 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();
在很多情况下,像这样的小改动会使测试更容易理解。因此,让我们在测试源文件夹中创建我们自己的自定义断言:
class UserAssert extends AbstractAssert {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;
}
}
现在,如果我们从新的
UserAssert
类而不是从 AssertJ 库导入 assertThat
方法,我们就可以使用新的、更易于阅读的断言。创建像这样的自定义断言似乎需要很多工作,但实际上只需几分钟即可完成。我坚信投入这些时间来创建可读的测试代码是值得的,即使之后它的可读性只是稍微好一点。毕竟,我们只编写一次测试代码,其他人(包括“未来的我”)必须在软件的生命周期中多次阅读、理解和操作代码。
如果仍然觉得工作量太大,请查看 AssertJ 的断言生成器。
结论 在测试中启动 Spring 应用程序是有原因的,但对于普通的单元测试来说,这是没有必要的。由于更长的周转时间,它甚至是有害的。相反,我们应该以一种易于支持为其编写简单单元测试的方式构建我们的 Spring bean。
Spring Boot Test Starter 附带 Mockito 和 AssertJ 作为测试库。
让我们利用这些测试库来创建富有表现力的单元测试!
最终形式的代码示例可在 github 上 找到。
推荐阅读
- 由浅入深理解AOP
- 【译】20个更有效地使用谷歌搜索的技巧
- Activiti(一)SpringBoot2集成Activiti6
- mybatisplus如何在xml的连表查询中使用queryWrapper
- MybatisPlus|MybatisPlus LambdaQueryWrapper使用int默认值的坑及解决
- MybatisPlus使用queryWrapper如何实现复杂查询
- SpringBoot调用公共模块的自定义注解失效的解决
- 解决SpringBoot引用别的模块无法注入的问题
- iOS中的Block
- Linux下面如何查看tomcat已经使用多少线程