大型程序的构建与测试#
现实中的程序,往往依赖关系错综复杂。
public class UserService {
@Autowired
private UserRepository userRepository;
public User findUser(String userId) {
var user = userRepository.findById(userId)
if (this.isUserValid(user)) {
return user;
}
return null;
}
private boolean isUserValid(User user) {
return user.isValid != null && user.isValid);
}
}
上面的代码是一个简单的模块依赖的案例。要对这份代码进行单元测试,存在两个难点:
UserService依赖UserRepository。为了实例化 UserService, 我们必须先实例化 UserRepository。 更进一步讲,UserRepository 可能也有各种依赖,这种连锁依赖,最终可能导致我们不得不把所有对象都实例化出来。UserService使用了@Autowired注入。即便我们已经有了UserRepository也无法通过常规手段 new 一个UserService.
解决 Autowired 注入问题#
@Autowired 注入问题比较好解决,我们只需要更改成 spring 推荐的构造函数注入方式即可:
public class UserService {
- @Autowired
private final UserRepository userRepository;
+ public UserService(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
... ...
}
这种方式不需要显式添加 @Autowired 注解,并且不影响我们手动实例化。
解决依赖链问题#
模块化做得好的代码,一定是对单元测试友好的。依赖链的问题需要通过接口抽象来解决。
在上个案例中,UserService 依赖 UserRepository。为了避免依赖链,我们可以将 UserRepository 抽象为接口 IUserRepository:
在正式环境中,照常使用 MongoUserRepository.
在单元测试的时候,就使用模拟出来的 TestUserRepository:
public class TestUserRepository implements IUserRepository {
@Override
public User findById(String userId) {
return new User(userId);
}
}
class UserTest {
UserService userService =
new UserService(new TestUserRepository);
@Test
public void testUserService() {
// .... ...
}
}
UserService 也可能被其他 class 依赖(可能是 UserController), 这种情况下,为了测试 UserController, 可以按照同样的方法将 UserService 抽象为接口。
当然这也不是绝对的,在 UserController/UserService/UserRepository 这条依赖链中,也可以选择将他们当做一组模块,只抽象 UserRepository,给 UserController 提供真实的 UserService, 这取决于业务中如何划分模块的边界:
var repository = new TestUserRepository();
var userService = new UserService(repository);
var userController = new UserController(userService);
通过模块划分与接口抽象,就可以将一些很难构造的对象隔离出来,伪造一个假的对象进行替换,这种方法称为 Mock。