java - 创建模拟库

标签 java reflection java-8 interface mocking

我想创建一个实现 InvocationHandler 的模拟库类来自 Java 反射的接口(interface)。

这是我创建的模板:

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方法为给定的模拟参数注册预期的返回值。

此外,如果代理接口(interface)调用方法或使用未注册的参数,我想抛出 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以一般方式上课,支持不同的接口(interface),而不仅仅是CalcInterface ,并且还支持不同的方法,而不仅仅是 add我在这里实现的方法。

最佳答案

您必须将构建器 逻辑与要构建的对象分开。 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 - 创建模拟库,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73999625/

相关文章:

java - SpringAMQP 中的 Reply-To 是否已预先设置?

java - 从另一个非静态方法调用非静态方法

使用第三方 .jar 文件时出现 java.lang.NoClassDefFoundError

javascript - 基本 JS 对象 {a :1, b :2} not supported in Nashorn?

java-8 - Java 流的两个 List<int[]> 类型的交集

java - 使用 Apache Spark 的 Hibernate 持久化导致进程阻塞

c# - Expression.Call "Contains"方法抛出 "Ambiguous match found exception"

c# - 转换为具有动态类型参数的通用接口(interface)

java - 从 getDeclaredMethods 中查找具有实际类型的泛型方法

java - 使用 java 8 Streams 对项目列表进行分组,并使用第一个项目而不是列表填充生成的 map