返回

通用仓库调用伪造对象: Map & Lambda实现

java

构建通用的仓库调用伪造对象

在单元测试中,避免真实数据库交互并模拟仓库层行为是一个常见需求。 本文探讨如何手动创建一个通用伪造对象(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();
    }

}

操作步骤

  1. 创建一个 GenericFakeRepository 类, 包含 returnValuesmethodInvocations 两个 Map。
  2. invoke 方法接受方法名和参数,使用方法名与参数组合生成 key ,查找返回值并记录调用信息。
  3. setReturnValue 方法根据 key 设置预期返回值。
  4. getInvocations 方法用于获取特定方法的调用列表。
  5. 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();
    }
}

操作步骤

  1. GenericFakeRepositoryWithLambda 类包含一个 returnFunctions Map, Value类型是 Function<Object[],Object>
  2. invoke 方法, 除了记录调用, 还使用 returnFunctions 映射表中的 Function 获取返回值
  3. setReturnValue 方法使用 lambda表达式设置函数
  4. getInvocations 方法用于获取特定方法的调用列表。
  5. 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 的方式,可适应不同场景下的测试需求。