如何在 JUnit 测试 Java 中用 Mockito 代替 class
How to substitute Mockito in place to stub a class in JUnit Test Java
在 JUnit 的测试会话期间,我试图用 Mockito 的正确使用来代替存根 class。
不幸的是,网上有很多关于 Mockito 的教程,但是关于存根方法的教程很少,我想学习这种技术。
本次测试由 Mockito 完成:
@Test
public void addWrongNewUserSpaceInUsername() throws Exception {
when(userValidator.isValidUsername(user.getUsername())).thenReturn(false);
try {
mockMvc.perform(
post("/register")
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(user)
));
} catch (Exception e) {
Assert.assertTrue(e.getCause() instanceof UsernameNotValidException);
}
}
为了澄清这些是涉及的 classes:
1) 控制器
@RestController
public class UserController {
@Autowired
RepositoryUserDB repositoryUserDB;
@Autowired
UserValidator userValidator;
@RequestMapping(value = "/register", method = RequestMethod.POST)
public User createUser(@RequestBody User user) {
if (userValidator.isValidUsername(user.getUsername())) {
if(!userValidator.isValidPassword(user.getPassword())){
throw new PasswordNotValidException();
}
if(userValidator.isValidDateOfBirth(user.getDateOfBirth()) == false){
throw new DOBNotValidException();
}
// se lo user e' gia' presente
if (repositoryUserDB.getUserByUsername(user.getUsername()) == null) {
return repositoryUserDB.createUser(user);
}
throw new UsernameAlreadyExistException();
} else throw new UsernameNotValidException();
}
}
2)回购接口:
public interface RepositoryUserDB {
User getUserByUsername(String username);
User createUser(User user);
}
3)回购:
@Component
public class MemoryUserDB implements RepositoryUserDB{
Map<String, User> repo;
public MemoryUserDB() {
this.repo = new HashMap<>();
}
@Override
public User getUserByUsername(String username) {
return repo.get(username);
}
@Override
public User createUser(User user) {
repo.put(user.getUsername(),user);
return repo.get(user.getUsername());
}
}
4) 验证者:
@Component
public class UserValidator {
public boolean isValidUsername(String username) {
return username.matches("^[a-zA-Z0-9]+$");
}
public boolean isValidPassword(String pwd) {
if (pwd == null)
return false;
return pwd.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\d){4,}.+$");
}
public boolean isValidDateOfBirth(String DOB) {
return DOB.matches("^(?:(?:(?:0?[13578]|1[02])(\/|-|\.)31)\1|(?:(?:0?[1,3-9]|1[0-2])(\/|-|\.)(?:29|30)\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:0?2(\/|-|\.)29\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:(?:0?[1-9])|(?:1[0-2]))(\/|-|\.)(?:0?[1-9]|1\d|2[0-8])\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$");
}
}
5)ResEntityExceptionHandler
@ControllerAdvice
public class RestEntityExceptionHandler {
@ExceptionHandler(UsernameNotValidException.class)
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "username wrong")
public void handleUsernameException() {
}
@ExceptionHandler(UsernameAlreadyExistException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "the username is already presents")
public void handleUsername() {
}
@ExceptionHandler(PasswordNotValidException.class)
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "password wrong")
public void handlePasswordException() {
}
@ExceptionHandler(DOBNotValidException.class)
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "Date Of Birth wrong")
public void handleDOBException(){
}
}
理论上,对于您的用例,存根方法应该相当简单。
但是当你依赖于使用 Spring beans 容器的 Spring 启动测试时,事情真的很难设置,因为你应该找到一种方法将模拟的 bean 注入容器中以替换实际的 bean: UserValidator
。
Spring 中的模拟启动测试通常依赖于 Spring Boot MockBean
。
这不是 mockito 模拟,但不是很远。
要了解与 Mockito 模拟的差异,您可以参考 .
使用框架提供了许多开箱即用的功能,但也会限制您自己,因为您想绕过框架功能。
假设您没有使用 Spring Boot 执行集成测试,而是执行真正的单元测试(因此没有 Spring 引导容器),事情可以通过这种方式执行。
您可以定义 UserValidator
的自定义实现,而不是模拟 UserValidator.isValidUsername()
,它会按照您的测试预期对方法 return 进行存根。
最后,Mockito 或任何模拟框架为您做了什么。
所以这是存根 class :
public class UserValidatorStub extends UserValidator {
private String expectedUsername;
private boolean isValidUsername;
public UserValidatorStub(String expectedUsername, boolean isValidUsername){
this.expectedUsername = expectedUsername;
this.isValidUsername = isValidUsername;
}
public boolean isValidUsername(String username) {
if (username.equals(expectedUsername)){
return isValidUsername;
}
// as fallback, it uses the default implementation but you may return false or null as alternative
return super.isValidUsername(username);
}
}
它接受一个构造函数来存储传递给存根方法的预期参数和存根结果到return。
现在,这里是您的测试的编写方式:
@Test
public void addWrongNewUserSpaceInUsername() throws Exception {
// inject the mock in the class under test
UserController userController = new UserController(new UserValidatorStub(user.getUsername(), false));
try {
userController.createUser(user);
} catch (Exception e) {
Assert.assertTrue(e.getCause() instanceof UsernameNotValidException);
}
}
请注意,该示例依赖于 UserController
中的构造函数注入来设置依赖项。
在 JUnit 的测试会话期间,我试图用 Mockito 的正确使用来代替存根 class。 不幸的是,网上有很多关于 Mockito 的教程,但是关于存根方法的教程很少,我想学习这种技术。
本次测试由 Mockito 完成:
@Test
public void addWrongNewUserSpaceInUsername() throws Exception {
when(userValidator.isValidUsername(user.getUsername())).thenReturn(false);
try {
mockMvc.perform(
post("/register")
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(user)
));
} catch (Exception e) {
Assert.assertTrue(e.getCause() instanceof UsernameNotValidException);
}
}
为了澄清这些是涉及的 classes:
1) 控制器
@RestController
public class UserController {
@Autowired
RepositoryUserDB repositoryUserDB;
@Autowired
UserValidator userValidator;
@RequestMapping(value = "/register", method = RequestMethod.POST)
public User createUser(@RequestBody User user) {
if (userValidator.isValidUsername(user.getUsername())) {
if(!userValidator.isValidPassword(user.getPassword())){
throw new PasswordNotValidException();
}
if(userValidator.isValidDateOfBirth(user.getDateOfBirth()) == false){
throw new DOBNotValidException();
}
// se lo user e' gia' presente
if (repositoryUserDB.getUserByUsername(user.getUsername()) == null) {
return repositoryUserDB.createUser(user);
}
throw new UsernameAlreadyExistException();
} else throw new UsernameNotValidException();
}
}
2)回购接口:
public interface RepositoryUserDB {
User getUserByUsername(String username);
User createUser(User user);
}
3)回购:
@Component
public class MemoryUserDB implements RepositoryUserDB{
Map<String, User> repo;
public MemoryUserDB() {
this.repo = new HashMap<>();
}
@Override
public User getUserByUsername(String username) {
return repo.get(username);
}
@Override
public User createUser(User user) {
repo.put(user.getUsername(),user);
return repo.get(user.getUsername());
}
}
4) 验证者:
@Component
public class UserValidator {
public boolean isValidUsername(String username) {
return username.matches("^[a-zA-Z0-9]+$");
}
public boolean isValidPassword(String pwd) {
if (pwd == null)
return false;
return pwd.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\d){4,}.+$");
}
public boolean isValidDateOfBirth(String DOB) {
return DOB.matches("^(?:(?:(?:0?[13578]|1[02])(\/|-|\.)31)\1|(?:(?:0?[1,3-9]|1[0-2])(\/|-|\.)(?:29|30)\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:0?2(\/|-|\.)29\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:(?:0?[1-9])|(?:1[0-2]))(\/|-|\.)(?:0?[1-9]|1\d|2[0-8])\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$");
}
}
5)ResEntityExceptionHandler
@ControllerAdvice
public class RestEntityExceptionHandler {
@ExceptionHandler(UsernameNotValidException.class)
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "username wrong")
public void handleUsernameException() {
}
@ExceptionHandler(UsernameAlreadyExistException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "the username is already presents")
public void handleUsername() {
}
@ExceptionHandler(PasswordNotValidException.class)
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "password wrong")
public void handlePasswordException() {
}
@ExceptionHandler(DOBNotValidException.class)
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "Date Of Birth wrong")
public void handleDOBException(){
}
}
理论上,对于您的用例,存根方法应该相当简单。
但是当你依赖于使用 Spring beans 容器的 Spring 启动测试时,事情真的很难设置,因为你应该找到一种方法将模拟的 bean 注入容器中以替换实际的 bean: UserValidator
。
Spring 中的模拟启动测试通常依赖于 Spring Boot MockBean
。
这不是 mockito 模拟,但不是很远。
要了解与 Mockito 模拟的差异,您可以参考
使用框架提供了许多开箱即用的功能,但也会限制您自己,因为您想绕过框架功能。
假设您没有使用 Spring Boot 执行集成测试,而是执行真正的单元测试(因此没有 Spring 引导容器),事情可以通过这种方式执行。
您可以定义 UserValidator
的自定义实现,而不是模拟 UserValidator.isValidUsername()
,它会按照您的测试预期对方法 return 进行存根。
最后,Mockito 或任何模拟框架为您做了什么。
所以这是存根 class :
public class UserValidatorStub extends UserValidator {
private String expectedUsername;
private boolean isValidUsername;
public UserValidatorStub(String expectedUsername, boolean isValidUsername){
this.expectedUsername = expectedUsername;
this.isValidUsername = isValidUsername;
}
public boolean isValidUsername(String username) {
if (username.equals(expectedUsername)){
return isValidUsername;
}
// as fallback, it uses the default implementation but you may return false or null as alternative
return super.isValidUsername(username);
}
}
它接受一个构造函数来存储传递给存根方法的预期参数和存根结果到return。
现在,这里是您的测试的编写方式:
@Test
public void addWrongNewUserSpaceInUsername() throws Exception {
// inject the mock in the class under test
UserController userController = new UserController(new UserValidatorStub(user.getUsername(), false));
try {
userController.createUser(user);
} catch (Exception e) {
Assert.assertTrue(e.getCause() instanceof UsernameNotValidException);
}
}
请注意,该示例依赖于 UserController
中的构造函数注入来设置依赖项。