Skip to main content

JUnit 单元测试:通过 Mockito 打桩

适用场景

对一个类的方法的业务逻辑进行测试,为实现 TDD 的快速测试需求,执行测试用例时不会构造 Spring 应用上下文,因此需要进行以下处理:

  • 对于该类所依赖的 Bean 进行 Mock 处理
  • 对该方法所调用的经过 Mock 的 Bean 的方法打桩

测试对象

下面以对 AuthenticationServiceImplauthenticate(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 的实例依赖 CredentialPasswordRepositoryUserQueryApi 的实例,其中 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:模拟依赖的对象

由于 CredentialPasswordRepositoryUserQueryApi 不是测试对象,且不便于手动实例化,因此在单元测试时需要使用这两个类的 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 {
// ...
}

例如,通过以下代码模拟用户名为 user01CredentialPasswordRepositoryfindByCredential(String) 方法的返回结果为空:

// 当 authenticationServiceImpl 调用 passwordRepository 的 findByCredential 方法,
// 并以 user01 作为参数时将返回空的 Optional 实例,即无登录密码数据
doReturn(Optional.empty()).when(passwordRepository).findByCredential("user01");

通过以下代码模拟用户 ID 为 JUNIT00000000003UserQueryApiget(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));
}
}