返回
通用仓库调用伪造对象: Map & Lambda实现
java
2025-01-17 00:45:45
构建通用的仓库调用伪造对象
在单元测试中,避免真实数据库交互并模拟仓库层行为是一个常见需求。 本文探讨如何手动创建一个通用伪造对象(Fake Object),以支持测试中对仓库(Repository)的调用,同时避免使用动态代理。 该对象需能记录调用方法、参数和调用次数,并且能根据不同调用配置返回值。
问题剖析
通常在集成测试中,依赖真实数据库会使测试过程变得缓慢,且不易控制。为解决这些问题, 采用伪造对象成为首选方案。传统的做法往往为每个仓库接口实现特定的伪造对象,导致代码冗余,维护困难。 需要一种通用的方式来动态模拟任何仓库的调用,无需预先编写大量特定实现。动态代理能很好地解决这个问题,但某些情况下我们可能希望通过手动的方式实现,以更好理解其运作机制。
解决方案一:基于Map的伪造对象
一种简单的方案是利用Map存储调用信息和配置返回值。Map 的 Key 是调用的方法名称和参数组合, Value 则是对应的返回值以及其他配置。
代码示例
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
public class GenericFakeRepository {
private final Map<String, Object> returnValues = new HashMap<>();
private final Map<String, List<Object[]>> methodInvocations = new HashMap<>();
public Object invoke(String methodName, Object[] args) {
String key = methodName + serializeArguments(args);
//记录方法调用和参数
methodInvocations.computeIfAbsent(methodName, k -> new ArrayList<>()).add(args);
return returnValues.get(key);
}
public void setReturnValue(String methodName, Object[] args, Object returnValue) {
String key = methodName + serializeArguments(args);
this.returnValues.put(key, returnValue);
}
public List<Object[]> getInvocations(String methodName) {
return methodInvocations.getOrDefault(methodName, new ArrayList<>());
}
public void clearInvocations() {
methodInvocations.clear();
}
private String serializeArguments(Object[] args) {
if (args == null || args.length == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (Object arg : args) {
sb.append("_").append(arg == null ? "null":arg.toString());
}
return sb.toString();
}
}
操作步骤
- 创建一个
GenericFakeRepository
类, 包含returnValues
和methodInvocations
两个 Map。 invoke
方法接受方法名和参数,使用方法名与参数组合生成 key ,查找返回值并记录调用信息。setReturnValue
方法根据 key 设置预期返回值。getInvocations
方法用于获取特定方法的调用列表。clearInvocations
方法清空所有的调用信息记录。
使用方法示例
public interface UserRepository {
User findById(int id);
User findByName(String name);
void save(User user);
void delete(int id);
List<User> findAll();
boolean userExists(String name);
}
class User {
public String name;
public int id;
public User(String name, int id) {
this.name = name;
this.id = id;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(int id){
return userRepository.findById(id);
}
public boolean deleteUser(int id){
userRepository.delete(id);
return !userRepository.userExists("temp_user"); // some logic in method needs this to assert something.
}
}
class UserRepositoryTest {
public static void main(String[] args) {
GenericFakeRepository fakeRepository = new GenericFakeRepository();
UserRepository mockUserRepository = new UserRepository(){
@Override
public User findById(int id){
return (User) fakeRepository.invoke("findById", new Object[] {id});
}
@Override
public User findByName(String name) {
return (User) fakeRepository.invoke("findByName",new Object[]{name});
}
@Override
public void save(User user){
fakeRepository.invoke("save",new Object[] {user});
}
@Override
public void delete(int id) {
fakeRepository.invoke("delete",new Object[] {id});
}
@Override
public List<User> findAll(){
return (List<User>)fakeRepository.invoke("findAll", new Object[]{});
}
@Override
public boolean userExists(String name) {
return (boolean)fakeRepository.invoke("userExists", new Object[] {name});
}
};
// setup mock behavior
fakeRepository.setReturnValue("findById", new Object[] {1}, new User("test1",1));
fakeRepository.setReturnValue("userExists", new Object[] {"temp_user"}, true);
UserService userService = new UserService(mockUserRepository);
User user = userService.getUserById(1);
//test deleteUser which indirectly depends on other methods call within the logic
boolean deleteResult = userService.deleteUser(1);
// get method invocation information.
List<Object[]> findByIdInvocations = fakeRepository.getInvocations("findById");
List<Object[]> userExistsInvocations = fakeRepository.getInvocations("userExists");
List<Object[]> deleteInvocations = fakeRepository.getInvocations("delete");
System.out.println( "Result findById: " + user );
System.out.println( "delete operation: " + deleteResult );
System.out.println( "findById Invocations" + findByIdInvocations);
System.out.println("userExists Invocations: " + userExistsInvocations);
System.out.println("delete invocations:" + deleteInvocations );
}
}
方案二:基于Lambda表达式的配置
在更复杂情况下,我们可以用 Lambda 表达式定义返回值策略, 可以处理动态参数或有复杂逻辑的返回值需求。
这能有效解决在方案一中当参数组合较多或逻辑复杂时 setReturnValue
代码难以维护的情况。
代码示例
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class GenericFakeRepositoryWithLambda {
private final Map<String, Function<Object[], Object>> returnFunctions = new HashMap<>();
private final Map<String, List<Object[]>> methodInvocations = new HashMap<>();
public Object invoke(String methodName, Object[] args) {
methodInvocations.computeIfAbsent(methodName, k -> new ArrayList<>()).add(args);
Function<Object[],Object> function = returnFunctions.get(methodName);
return function == null ? null : function.apply(args);
}
public <T> void setReturnValue(String methodName, Function<Object[], T> function){
returnFunctions.put(methodName, function);
}
public List<Object[]> getInvocations(String methodName) {
return methodInvocations.getOrDefault(methodName, new ArrayList<>());
}
public void clearInvocations() {
methodInvocations.clear();
}
}
操作步骤
GenericFakeRepositoryWithLambda
类包含一个returnFunctions
Map, Value类型是Function<Object[],Object>
。invoke
方法, 除了记录调用, 还使用returnFunctions
映射表中的 Function 获取返回值setReturnValue
方法使用 lambda表达式设置函数getInvocations
方法用于获取特定方法的调用列表。clearInvocations
方法清空所有的调用信息记录。
使用方法示例
public class GenericFakeRepositoryWithLambdaTest {
public static void main(String[] args) {
GenericFakeRepositoryWithLambda fakeRepository = new GenericFakeRepositoryWithLambda();
UserRepository mockUserRepository = new UserRepository() {
@Override
public User findById(int id){
return (User) fakeRepository.invoke("findById", new Object[]{id});
}
@Override
public User findByName(String name){
return (User) fakeRepository.invoke("findByName",new Object[] {name});
}
@Override
public void save(User user){
fakeRepository.invoke("save",new Object[] {user});
}
@Override
public void delete(int id){
fakeRepository.invoke("delete",new Object[] {id});
}
@Override
public List<User> findAll(){
return (List<User>) fakeRepository.invoke("findAll",new Object[] {});
}
@Override
public boolean userExists(String name) {
return (boolean)fakeRepository.invoke("userExists", new Object[]{name});
}
};
// config mock return via lamda exprssions.
fakeRepository.setReturnValue("findById", (params) -> new User("test1", (int)params[0]));
fakeRepository.setReturnValue("findByName", (params) -> new User((String) params[0] + "-name",1));
fakeRepository.setReturnValue("userExists", (params) -> ( (String)params[0]).equals("temp_user"));
UserService userService = new UserService(mockUserRepository);
User user = userService.getUserById(2);
boolean deleteResult = userService.deleteUser(1);
List<Object[]> findByIdInvocations = fakeRepository.getInvocations("findById");
List<Object[]> userExistsInvocations = fakeRepository.getInvocations("userExists");
List<Object[]> deleteInvocations = fakeRepository.getInvocations("delete");
System.out.println( "Result: " + user);
System.out.println( "Delete Result " + deleteResult );
System.out.println( "find Invocations " + findByIdInvocations);
System.out.println( "userExists Invocations " + userExistsInvocations );
System.out.println( "delete Invocations" + deleteInvocations );
}
}
额外安全建议
在实际使用中,考虑到代码健壮性,可以添加额外的处理:
- 参数检查: 确保方法调用的参数不为空,并且类型匹配。
- 类型安全: 可以结合 Java 的泛型,增加类型的安全性。
- 更灵活的key匹配: 目前的方式是通过序列化参数为字符串方式形成Key,可以增加额外的自定义Key生成方法,支持根据不同的应用场景来定义不同的匹配规则, 甚至结合方法签名和类签名来完成调用模拟。
总而言之,使用手动实现的伪造对象(fake object)进行测试可以在无需依赖动态代理的同时,提供足够的灵活性与控制权,有效地保障单元测试的质量。通过选用基于 Map
或者 Lambda
的方式,可适应不同场景下的测试需求。