【问题标题】:Creating a mock library创建模拟库
【发布时间】:2022-11-10 16:46:39
【问题描述】:

我想创建一个从 Java 反射实现 InvocationHandler 接口的模拟库类。

这是我创建的模板:

import java.lang.reflect.*;
import java.util.*;

class MyMock implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // todo
        }
        
        public MyMock when(String method, Object[] args) {
            // todo
        }
        
        public void thenReturn(Object val) {
            // todo
        }
}

when 和 thenReturn 方法是链式方法。

然后when 方法注册给定的模拟参数。

thenReturn 方法注册给定模拟参数的预期返回值。

另外,如果代理接口调用方法或使用未注册的参数,我想抛出 java.lang.IllegalArgumentException。

这是一个示例界面:

interface CalcInterface {
    int add(int a, int b);
    String add(String a, String b);
    String getValue();
}

这里我们有两个重载的add 方法。

这是一个测试我想要实现的模拟类的程序。

class TestApplication {     
        public static void main(String[] args) {
            MyMock m = new MyMock();
            CalcInterface ref = (CalcInterface) Proxy.newProxyInstance(MyMock.class.getClassLoader(), new Class[]{CalcInterface.class}, m);
            
            m.when("add", new Object[]{1,2}).thenReturn(3);
            m.when("add", new Object[]{"x","y"}).thenReturn("xy");
            
            System.out.println(ref.add(1,2)); // prints 3
            System.out.println(ref.add("x","y")); // prints "xy"
        }
}

这是我迄今为止实现的用于检查 CalcInterface 中的方法的代码:

class MyMock implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            int n = args.length;
            if(n == 2 && method.getName().equals("add")) {
                Object o1 = args[0], o2 = args[1];
                if((o1 instanceof String) && (o2 instanceof String)) {
                    String s1 = (String) o1, s2 = (String) o2;
                    return s1+ s2;
                } else if((o1 instanceof Integer) && (o2 instanceof Integer)) {
                    int s1 = (Integer) o1, s2 = (Integer) o2;
                    return s1+ s2;
                }
            }
            throw new IllegalArgumentException();
        }
        
        public MyMock when(String method, Object[] args) {
            return this;
        }
        
        public void thenReturn(Object val) {
        
        }
}

在这里,我只检查名称为 add 并具有 2 个参数的方法,它们的类型为 StringInteger

但是我想以一种通用的方式创建这个MyMock 类,支持不同的接口,而不仅仅是CalcInterface,还支持不同的方法,而不仅仅是我在这里实现的add 方法。

【问题讨论】:

  • 为什么thenReturn 不返回任何东西?

标签: java reflection java-8 interface mocking


【解决方案1】:

你必须分开建设者从对象构建的逻辑。方法when 必须返回一些记住参数的东西,这样thenReturn 的调用仍然知道上下文。

例如

public class MyMock implements InvocationHandler {
    record Key(String name, List<?> arguments) {
        Key { // stream().toList() creates an immutable list allowing null
            arguments = arguments.stream().toList();
        }
        Key(String name, Object... arg) {
            this(name, arg == null? List.of(): Arrays.stream(arg).toList());
        }
    }
    final Map<Key, Function<Object[], Object>> rules = new HashMap<>();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        var rule = rules.get(new Key(method.getName(), args));
        if(rule == null) throw new IllegalStateException("No matching rule");
        return rule.apply(args);
    }
    public record Rule(MyMock mock, Key key) {
        public void thenReturn(Object val) {
            var existing = mock.rules.putIfAbsent(key, arg -> val);
            if(existing != null) throw new IllegalStateException("Rule already exist");
        }
        public void then(Function<Object[], Object> f) {
            var existing = mock.rules.putIfAbsent(key, Objects.requireNonNull(f));
            if(existing != null) throw new IllegalStateException("Rule already exist");
        }
    }
    public Rule when(String method, Object... args) {
        Key key = new Key(method, args);
        if(rules.containsKey(key)) throw new IllegalStateException("Rule already exist");
        return new Rule(this, key);
    }
}

这已经能够从字面上执行您的示例,但也支持类似的东西

MyMock m = new MyMock();
CalcInterface ref = (CalcInterface) Proxy.newProxyInstance(
        CalcInterface.class.getClassLoader(), new Class[]{CalcInterface.class}, m);

m.when("add", 1,2).thenReturn(3);
m.when("add", "x","y").thenReturn("xy");
AtomicInteger count = new AtomicInteger();
m.when("getValue").then(arg -> "getValue invoked " + count.incrementAndGet() + " times");

System.out.println(ref.add(1,2)); // prints 3
System.out.println(ref.add("x","y")); // prints "xy"
System.out.println(ref.getValue()); // prints getValue invoked 1 times
System.out.println(ref.getValue()); // prints getValue invoked 2 times

请注意,当您想要添加对简单值匹配之外的规则的支持时,哈希查找将不再起作用。在这种情况下,您必须求助于必须线性搜索匹配的数据结构。

上面的示例使用了较新的 Java 功能,例如 record 类,但如果需要,为以前的 Java 版本重写它应该不会太难。


也可以重新设计此代码以使用真正的构建器模式,即在创建实际的处理程序/模拟实例之前使用构建器来描述配置。这允许处理程序/模拟使用不可变状态:

public class MyMock2 {
    public static Builder builder() {
        return new Builder();
    }
    public interface Rule {
        Builder thenReturn(Object val);
        Builder then(Function<Object[], Object> f);
    }
    public static class Builder {
        final Map<Key, Function<Object[], Object>> rules = new HashMap<>();

        public Rule when(String method, Object... args) {
            Key key = new Key(method, args);
            if(rules.containsKey(key))
                throw new IllegalStateException("Rule already exist");
            return new RuleImpl(this, key);
        }
        public <T> T build(Class<T> type) {
            Map<Key, Function<Object[], Object>> rules = Map.copyOf(this.rules);
            return type.cast(Proxy.newProxyInstance(type.getClassLoader(),
                new Class[]{ type }, (proxy, method, args) -> {
                   var rule = rules.get(new Key(method.getName(), args));
                   if(rule == null) throw new IllegalStateException("No matching rule");
                   return rule.apply(args);
                }));

        }
    }
    record RuleImpl(MyMock2.Builder builder, Key key) implements Rule {
        public Builder thenReturn(Object val) {
            var existing = builder.rules.putIfAbsent(key, arg -> val);
            if(existing != null) throw new IllegalStateException("Rule already exist");
            return builder;
        }
        public Builder then(Function<Object[], Object> f) {
            var existing = builder.rules.putIfAbsent(key, Objects.requireNonNull(f));
            if(existing != null) throw new IllegalStateException("Rule already exist");
            return builder;
        }
    }
    record Key(String name, List<?> arguments) {
        Key { // stream().toList() createns an immutable list allowing null
            arguments = arguments.stream().toList();
        }
        Key(String name, Object... arg) {
            this(name, arg == null? List.of(): Arrays.stream(arg).toList());
        }
    }
}

可以像这样使用

AtomicInteger count = new AtomicInteger();
CalcInterface ref = MyMock2.builder()
        .when("add", 1,2).thenReturn(3)
        .when("add", "x","y").thenReturn("xy")
        .when("getValue")
            .then(arg -> "getValue invoked " + count.incrementAndGet() + " times")
        .build(CalcInterface.class);

System.out.println(ref.add(1,2)); // prints 3
System.out.println(ref.add("x","y")); // prints "xy"
System.out.println(ref.getValue()); // prints getValue invoked 1 times
System.out.println(ref.getValue()); // prints getValue invoked 2 times

【讨论】:

  • 你能帮我解决如何在 Java 8 中执行此操作吗,我无法理解如何在 Java 8 版本中进行转换。
  • 熟悉what records are,你就会明白普通类的较长形式是什么样子的。如果你使用 IntelliJ,你可以let the software do the transformation
【解决方案2】:

InvocationHandler 用于针对在内存中实例化的对象实例(代理)调用方法签名(或类,如果您正在使用静态方法),不支持不在提供的代理类中的方法根本,这就是您在这里实现它的方式。也就是说,您可以通过在私有变量中保存您尝试模拟的方法签名(以及您想要在参数匹配时返回的值)来实现您想要做的事情。

这可能有效,但前提是我已经有几年没有做过 Java了,所以我可能有点生疏:

class MyMock implements InvocationHandler {
    private String methodName = null;
    private Object[] supportedArgs = null;
    private Object returnValue = null;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        // If we don't know when this mock is supposed to be used, it's useless
        assert this.methodName != null: "when(method, args) hasn't been called against the mock yet!";

        // Note that both args and supportedArgs will be null if the method signature has no params
        if (method.getName().equals(this.methodName) && this.supportedArgs == args) {
            return this.returnValue;
        }
        try {
            return method.invoke(proxy, args);
        }
        catch (IllegalAccessException | InvocationTargetException innerException){
            // The proxy didn't support the method either, so let's throw an IllegalArgumentException
            throw new IllegalArgumentException("The supplied method signature isn't implemented in the proxy.");
        }
    }

    public MyMock when(String method, Object[] args) {
        this.methodName = method;
        this.supportedArgs = args;
        return this;
    }

    public void thenReturn(Object val) {
        this.returnValue = val;
    }
}

【讨论】:

  • 如果你想变得更漂亮,你可以为你试图模拟的对象的类添加一个通用字段,然后根据代理的类检查它,并确保一个可以从另一个分配。这可能会防止一些相同的方法名称完全不同的对象类型问题。
  • 使用此代码后,当我在 TestApplication 中运行 main 方法时,出现Exception in thread "main" java.lang.IllegalArgumentException: The supplied method signature isn't implemented in the proxy. 异常。
猜你喜欢
  • 1970-01-01
  • 2018-08-13
  • 2011-02-24
  • 2021-11-04
  • 1970-01-01
  • 1970-01-01
  • 2016-08-03
  • 2013-03-14
  • 2013-09-29
相关资源
最近更新 更多