【注】本文译自: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> {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 上 找到。