JUnit 单元测试:通过 Mockito 打桩
适用场景
对一个类的方法的业务逻辑进行测试,为实现 TDD 的快速测试需求,执行测试用例时不会构造 Spring 应用上下文,因此需要进行以下处理:
- 对于该类所依赖的 Bean 进行 Mock 处理
- 对该方法所调用的经过 Mock 的 Bean 的方法打桩
测试对象
下面以对 AuthenticationServiceImpl
的 authenticate(AuthenticateDTO)
方法进行单元测试为例。
/* 测试对象类 */
@Component
public class AuthenticationServiceImpl implements AuthenticationService {
private final CredentialPasswordRepository passwordRepository;
private final UserQueryApi userQueryApi;
@Autowired
public AuthenticationServiceImpl(CredentialPasswordRepository passwordRepository, UserQueryApi userQueryApi) {
this.passwordRepository = passwordRepository;
this.userQueryApi = userQueryApi;
}
/**
* 根据登录用户名取得登录密码信息,当且仅当提供的密码与用户的密码匹配时返回用户信息,否则抛出登录认证错误。
* @param credentials 登录凭证数据,包含登录用户名及密码
* @return 用户信息
*/
@Override
public UserQueryEntity authenticate(final AuthenticateDTO credentials) {
final String username = credentials.getUsername();
final String password = credentials.getPassword();
// 根据用户名取得登录密码信息
CredentialPasswordEntity passwordEntity = passwordRepository.findByCredential(username).orElse(null);
final String encryptedPassword = passwordEntity == null ? null : passwordEntity.getPassword();
// 若密码不存在(即登录用户名不正确)或密码不匹配则抛出登录认证错误
if (passwordEntity == null || !PasswordUtils.validatePassword(password, encryptedPassword)) {
throw new AuthenticationError();
}
// 否则根据用户 ID 取得用户信息并返回
return userQueryApi.get(passwordEntity.getUserId());
}
}
AuthenticationServiceImpl
的实例依赖 CredentialPasswordRepository
和 UserQueryApi
的实例,其中 CredentialPasswordRepository
为用户的登录密码的数据仓库,UserQueryApi
为用户信息查询的 FeignClient。这两个依赖在 Spring 应用启动时由 Spring 负责实例化。
添加依赖
编辑模块的 pom.xml,添加以下依赖:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Mock:模拟依赖的对象
由于 CredentialPasswordRepository
和 UserQueryApi
不是测试对象,且不便于手动实例化,因此在单元测试时需要使用这两个类的 Mock 对象。Mockito 的 @Mock
注解可以将一个依赖声明为 Mock 对象。
/* JUnit 测试类 */
class TestAuthenticationServiceImpl {
@Mock
private CredentialPasswordRepository credentialPasswordRepository;
@Mock
private UserQueryApi userQueryApi;
}
然后通过 @InjectMocks
注解声明测试对象,从而使测试对象在构造时使用 Mock 对象作为构造方法的参数。
/* JUnit 测试类 */
class TestAuthenticationServiceImpl {
@Mock
private CredentialPasswordRepository credentialPasswordRepository;
@Mock
private UserQueryApi userQueryApi;
@InjectMocks
private AuthenticationServiceImpl authenticationServiceImpl;
}
打桩:模拟依赖的对象的处理逻辑
由于依赖的对象是 Mock 对象,而 Mock 对象的方法可能没有默认实现,因此在测试时需要对 Mock 对象的方法打桩,即模拟:
- 给定特定参数时返回什么结果(通过
org.mockito.Mockito.doReturn
模拟) - 给定特定参数时抛出什么错误(通过
org.mockito.Mockito.doThrow
模拟)
要通过 Mockito 对 Mock 对象的方法打桩,需要为 JUnit 类添加 Mockito 扩展:
/* JUnit 测试类 */
@ExtendWith(MockitoExtension.class)
class TestAuthenticationServiceImpl {
// ...
}
例如,通过以下代码模拟用户名为 user01
时 CredentialPasswordRepository
的 findByCredential(String)
方法的返回结果为空:
// 当 authenticationServiceImpl 调用 passwordRepository 的 findByCredential 方法,
// 并以 user01 作为参数时将返回空的 Optional 实例,即无登录密码数据
doReturn(Optional.empty()).when(passwordRepository).findByCredential("user01");
通过以下代码模拟用户 ID 为 JUNIT00000000003
时 UserQueryApi
的 get(String)
方法将抛出 NotFoundError
:
// 当 authenticationServiceImpl 调用 userQueryApi 的 get 方法,
// 并以 JUNIT00000000003 作为参数时将抛出 NotFoundError,即无此用户数据
doThrow(NotFoundError.class).when(userQueryApi).get("JUNIT00000000003");
断言:判断执行结果
通过断言对测试对象方法的结果进行正确性判断,常用的断言(org.junit.jupiter.api.Assertions
的静态方法)有:
assertEquals
:判断是否相等assertNotEquals
:判断是否不相等assertTrue
:判断是否为真assertFalse
:判断是否为假assertNull
:判断是否为空指针assertNotNull
:判断是否不为空指针assertThrows
:判断是否抛出指定类型的异常assertDoesNotThrows
:判断是否未抛出异常
测试用例示例
@ExtendWith(MockitoExtension.class)
class TestAuthenticationServiceImpl {
@Mock
private CredentialPasswordRepository passwordRepository;
@Mock
private UserQueryApi userQueryApi;
@InjectMocks
private AuthenticationServiceImpl authenticationServiceImpl;
/**
* 用户名不正确时抛出登录认证错误。
*/
@Test
void givenUsernameIsWrong_whenAuthenticate_thenThrowAuthenticationError() {
AuthenticateDTO authenticateDTO = new AuthenticateDTO();
authenticateDTO.setUsername("user01");
authenticateDTO.setPassword("Pa5sW0rD");
doReturn(Optional.empty()).when(passwordRepository).findByCredential(authenticateDTO.getUsername());
assertThrows(AuthenticationError.class, () -> authenticationServiceImpl.authenticate(authenticateDTO));
}
/**
* 密码不正确时抛出登录认证错误。
*/
@Test
void givenUsernameIsCorrectAndPasswordIsWrong_whenAuthenticate_thenThrowAuthenticationError() {
AuthenticateDTO authenticateDTO = new AuthenticateDTO();
authenticateDTO.setUsername("user02");
authenticateDTO.setPassword("Pa5sW0rD");
CredentialPasswordEntity passwordEntity = new CredentialPasswordEntity();
passwordEntity.setUserId("JUNIT00000000002");
passwordEntity.setPassword(PasswordUtils.encryptPassword("pAs5woRd", 10));
doReturn(Optional.of(passwordEntity)).when(passwordRepository).findByCredential(authenticateDTO.getUsername());
assertThrows(AuthenticationError.class, () -> authenticationServiceImpl.authenticate(authenticateDTO));
}
/**
* 用户名及密码均正确,且用户存在时返回用户信息。
*/
@Test
void givenUsernameIsCorrectAndPasswordIsCorrect_whenAuthenticate_thenReturnUserEntity() {
AuthenticateDTO authenticateDTO = new AuthenticateDTO();
authenticateDTO.setUsername("user02");
authenticateDTO.setPassword("pAs5woRd");
CredentialPasswordEntity passwordEntity = new CredentialPasswordEntity();
passwordEntity.setUserId("JUNIT00000000002");
passwordEntity.setPassword(PasswordUtils.encryptPassword(authenticateDTO.getPassword(), 10));
doReturn(Optional.of(passwordEntity)).when(passwordRepository).findByCredential(authenticateDTO.getUsername());
UserQueryEntity userEntity = new UserQueryEntity();
doReturn(userEntity).when(userQueryApi).get(passwordEntity.getUserId());
assertEquals(userEntity, authenticationServiceImpl.authenticate(authenticateDTO));
}
/**
* 用户名及密码均正确,但用户帐号不存在时返回不存在错误。
*/
@Test
void givenUsernameIsCorrectAndPasswordIsCorrect_whenAuthenticate_thenThrowNotFoundError() {
AuthenticateDTO authenticateDTO = new AuthenticateDTO();
authenticateDTO.setUsername("user03");
authenticateDTO.setPassword("pAs5woRd");
CredentialPasswordEntity passwordEntity = new CredentialPasswordEntity();
passwordEntity.setUserId("JUNIT00000000003");
passwordEntity.setPassword(PasswordUtils.encryptPassword(authenticateDTO.getPassword(), 10));
doReturn(Optional.of(passwordEntity)).when(passwordRepository).findByCredential(authenticateDTO.getUsername());
doThrow(NotFoundError.class).when(userQueryApi).get(passwordEntity.getUserId());
assertThrows(NotFoundError.class, () -> authenticationServiceImpl.authenticate(authenticateDTO));
}
}