原文地址:https://reflectoring.io/unit-...html
编写好的单元测试能够被当作一个很难掌握的艺术。但好消息是支持单元测试的机制很容易学习。java
本文给你提供在Spring Boot 应用程序中编写好的单元测试的机制,而且深刻技术细节。git
咱们将带你学习如何以可测试的方式建立Spring Bean实例,而后讨论如何使用Mockito
和AssertJ
,这两个包在Spring Boot中都为了测试默认引用了。github
本文只讨论单元测试。至于集成测试,测试web层和测试持久层将会在接下来的系列文章中进行讨论。web
本文附带的代码示例地址:spring-boot-testing算法
这个教程是一个系列:spring
若是你喜欢看视频教程,能够看看Philip
的课程:测试Spring Boot应用程序课程数据库
本文中,为了进行单元测试,咱们会使用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-engine:5.2.0' testCompile('org.mockito:mockito-junit-jupiter:2.23.0') }
Mockito
和AssertJ
会在spring-boot-test
依赖中自动引用,可是咱们须要本身引用Lombok
。框架
若是你之前使用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
。它提供至少两种方式来建立一个模拟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() { // ... } }
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
附带Mockito
和AssertJ
做为测试库。让咱们利用这些测试库来建立富有表现力的单元测试!