lea*_*ner 2 java reflection interface mocking java-8
我想创建一个模拟库类来实现InvocationHandler
Java 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
我在这里实现的方法。
您必须将构建器逻辑与要构建的对象分开。该方法when
必须返回记住参数的东西,以便调用thenReturn
仍然知道上下文。
例如
\npublic 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这已经能够从字面上执行您的示例,但也支持类似的东西
\nMyMock 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 版本重写它应该不会太困难。
还可以重新设计此代码以使用真正的构建器模式,即在创建实际处理程序/模拟实例之前使用构建器来描述配置。这允许处理程序/模拟使用不可变状态:
\npublic 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可以像这样使用
\nAtomicInteger 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