创建模拟库

lea*_*ner 2 java reflection interface mocking java-8

我想创建一个模拟库类来实现InvocationHandlerJava Reflection 的接口。

这是我创建的模板:

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
        }
}
Run Code Online (Sandbox Code Playgroud)

when 和 thenReturn 方法是链式方法。

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

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

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

这是一个示例界面:

interface CalcInterface {
    int add(int a, int b);
    String add(String a, String b);
    String getValue();
}
Run Code Online (Sandbox Code Playgroud)

这里我们有两个重载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"
        }
}
Run Code Online (Sandbox Code Playgroud)

这是我迄今为止实现的用于检查 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) {
        
        }
}
Run Code Online (Sandbox Code Playgroud)

在这里,我仅检查具有名称add和 2 个参数、类型为String或的方法Integer

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

Hol*_*ger 5

您必须将构建器逻辑与要构建的对象分开。该方法when必须返回记住参数的东西,以便调用thenReturn仍然知道上下文。

\n

例如

\n
public class MyMock implements InvocationHandler {\n    record Key(String name, List<?> arguments) {\n        Key { // stream().toList() creates an immutable list allowing null\n            arguments = arguments.stream().toList();\n        }\n        Key(String name, Object... arg) {\n            this(name, arg == null? List.of(): Arrays.stream(arg).toList());\n        }\n    }\n    final Map<Key, Function<Object[], Object>> rules = new HashMap<>();\n\n    @Override\n    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n        var rule = rules.get(new Key(method.getName(), args));\n        if(rule == null) throw new IllegalStateException("No matching rule");\n        return rule.apply(args);\n    }\n    public record Rule(MyMock mock, Key key) {\n        public void thenReturn(Object val) {\n            var existing = mock.rules.putIfAbsent(key, arg -> val);\n            if(existing != null) throw new IllegalStateException("Rule already exist");\n        }\n        public void then(Function<Object[], Object> f) {\n            var existing = mock.rules.putIfAbsent(key, Objects.requireNonNull(f));\n            if(existing != null) throw new IllegalStateException("Rule already exist");\n        }\n    }\n    public Rule when(String method, Object... args) {\n        Key key = new Key(method, args);\n        if(rules.containsKey(key)) throw new IllegalStateException("Rule already exist");\n        return new Rule(this, key);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
MyMock m = new MyMock();\nCalcInterface ref = (CalcInterface) Proxy.newProxyInstance(\n        CalcInterface.class.getClassLoader(), new Class[]{CalcInterface.class}, m);\n\nm.when("add", 1,2).thenReturn(3);\nm.when("add", "x","y").thenReturn("xy");\nAtomicInteger count = new AtomicInteger();\nm.when("getValue").then(arg -> "getValue invoked " + count.incrementAndGet() + " times");\n\nSystem.out.println(ref.add(1,2)); // prints 3\nSystem.out.println(ref.add("x","y")); // prints "xy"\nSystem.out.println(ref.getValue()); // prints getValue invoked 1 times\nSystem.out.println(ref.getValue()); // prints getValue invoked 2 times\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n
\n

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

\n
public class MyMock2 {\n    public static Builder builder() {\n        return new Builder();\n    }\n    public interface Rule {\n        Builder thenReturn(Object val);\n        Builder then(Function<Object[], Object> f);\n    }\n    public static class Builder {\n        final Map<Key, Function<Object[], Object>> rules = new HashMap<>();\n\n        public Rule when(String method, Object... args) {\n            Key key = new Key(method, args);\n            if(rules.containsKey(key))\n                throw new IllegalStateException("Rule already exist");\n            return new RuleImpl(this, key);\n        }\n        public <T> T build(Class<T> type) {\n            Map<Key, Function<Object[], Object>> rules = Map.copyOf(this.rules);\n            return type.cast(Proxy.newProxyInstance(type.getClassLoader(),\n                new Class[]{ type }, (proxy, method, args) -> {\n                   var rule = rules.get(new Key(method.getName(), args));\n                   if(rule == null) throw new IllegalStateException("No matching rule");\n                   return rule.apply(args);\n                }));\n\n        }\n    }\n    record RuleImpl(MyMock2.Builder builder, Key key) implements Rule {\n        public Builder thenReturn(Object val) {\n            var existing = builder.rules.putIfAbsent(key, arg -> val);\n            if(existing != null) throw new IllegalStateException("Rule already exist");\n            return builder;\n        }\n        public Builder then(Function<Object[], Object> f) {\n            var existing = builder.rules.putIfAbsent(key, Objects.requireNonNull(f));\n            if(existing != null) throw new IllegalStateException("Rule already exist");\n            return builder;\n        }\n    }\n    record Key(String name, List<?> arguments) {\n        Key { // stream().toList() createns an immutable list allowing null\n            arguments = arguments.stream().toList();\n        }\n        Key(String name, Object... arg) {\n            this(name, arg == null? List.of(): Arrays.stream(arg).toList());\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

可以像这样使用

\n
AtomicInteger count = new AtomicInteger();\nCalcInterface ref = MyMock2.builder()\n        .when("add", 1,2).thenReturn(3)\n        .when("add", "x","y").thenReturn("xy")\n        .when("getValue")\n            .then(arg -> "getValue invoked " + count.incrementAndGet() + " times")\n        .build(CalcInterface.class);\n\nSystem.out.println(ref.add(1,2)); // prints 3\nSystem.out.println(ref.add("x","y")); // prints "xy"\nSystem.out.println(ref.getValue()); // prints getValue invoked 1 times\nSystem.out.println(ref.getValue()); // prints getValue invoked 2 times\n
Run Code Online (Sandbox Code Playgroud)\n

  • 在“Key”的构造函数中,有循环“for (Object e :arguments) { this.arguments.add(e); }` 没有任何作用,因为 `arguments` 是字段的名称,而您想要迭代的参数是 `arg` (请注意,像 Eclipse 这样的优秀 IDE 会警告您这一点)。但是你可以将整个构造函数简化为 `Key(String name, Object... arg) { this.arguments = arg == null? Collections.emptyList(): Arrays.asList(arg); this.name = 名称;}` (2认同)