diff --git a/demo/java-demo/src/test/java/com/alibaba/testable/demo/service/DemoMockServiceTest.java b/demo/java-demo/src/test/java/com/alibaba/testable/demo/service/DemoMockServiceTest.java index f752b9f..9179c3b 100644 --- a/demo/java-demo/src/test/java/com/alibaba/testable/demo/service/DemoMockServiceTest.java +++ b/demo/java-demo/src/test/java/com/alibaba/testable/demo/service/DemoMockServiceTest.java @@ -63,13 +63,13 @@ class DemoMockServiceTest { @Test void should_able_to_mock_new_object() throws Exception { assertEquals("mock_something", demoService.newFunc()); - verify("createBlackBox").times(1); + verify("createBlackBox").with("something"); } @Test void should_able_to_mock_member_method() throws Exception { assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello")); - verify("innerFunc").times(1); + verify("innerFunc").with("hello"); } @Test diff --git a/demo/kotlin-demo/src/test/kotlin/com/alibaba/testable/demo/service/DemoMockServiceTest.kt b/demo/kotlin-demo/src/test/kotlin/com/alibaba/testable/demo/service/DemoMockServiceTest.kt index f6c10c9..a3ffa7e 100644 --- a/demo/kotlin-demo/src/test/kotlin/com/alibaba/testable/demo/service/DemoMockServiceTest.kt +++ b/demo/kotlin-demo/src/test/kotlin/com/alibaba/testable/demo/service/DemoMockServiceTest.kt @@ -59,13 +59,13 @@ internal class DemoMockServiceTest { @Test fun should_able_to_mock_new_object() { assertEquals("mock_something", demoService.newFunc()) - verify("createBlackBox").times(1) + verify("createBlackBox").with("something") } @Test fun should_able_to_mock_member_method() { assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello")) - verify("innerFunc").times(1) + verify("innerFunc").with("hello") } @Test diff --git a/demo/kotlin-demo/src/test/kotlin/com/alibaba/testable/demo/util/PathUtilTest.kt b/demo/kotlin-demo/src/test/kotlin/com/alibaba/testable/demo/util/PathUtilTest.kt index fddd26b..ae60573 100644 --- a/demo/kotlin-demo/src/test/kotlin/com/alibaba/testable/demo/util/PathUtilTest.kt +++ b/demo/kotlin-demo/src/test/kotlin/com/alibaba/testable/demo/util/PathUtilTest.kt @@ -3,7 +3,6 @@ package com.alibaba.testable.demo.util import org.junit.jupiter.api.Test import com.alibaba.testable.core.annotation.TestableMock import com.alibaba.testable.core.tool.TestableTool.verify -import com.alibaba.testable.demo.util.PathUtil import java.io.File class PathUtilTest { diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/constant/ConstPool.java b/testable-agent/src/main/java/com/alibaba/testable/agent/constant/ConstPool.java index b72b227..9f11820 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/constant/ConstPool.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/constant/ConstPool.java @@ -12,5 +12,7 @@ public class ConstPool { public static final String TEST_POSTFIX = "Test"; public static final String TESTABLE_INJECT_REF = "_testableInternalRef"; + public static final String FIELD_TARGET_METHOD = "targetMethod"; + public static final String TESTABLE_MOCK = "com.alibaba.testable.core.annotation.TestableMock"; } diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/TestClassHandler.java b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/TestClassHandler.java index a79d2f0..9fbbf06 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/TestClassHandler.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/TestClassHandler.java @@ -2,6 +2,7 @@ package com.alibaba.testable.agent.handler; import com.alibaba.testable.agent.constant.ConstPool; import com.alibaba.testable.agent.tool.ImmutablePair; +import com.alibaba.testable.agent.util.AnnotationUtil; import com.alibaba.testable.agent.util.ClassUtil; import org.objectweb.asm.tree.*; @@ -24,7 +25,7 @@ public class TestClassHandler extends BaseClassHandler { private static final String METHOD_CURRENT_SOURCE_METHOD_NAME = "currentSourceMethodName"; private static final String METHOD_RECORD_MOCK_INVOKE = "recordMockInvoke"; private static final String SIGNATURE_TESTABLE_UTIL_METHOD = "(Ljava/lang/Object;)Ljava/lang/String;"; - private static final String SIGNATURE_INVOKE_COUNTER_METHOD = "()V"; + private static final String SIGNATURE_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;Z)V"; private static final Map FIELD_TO_METHOD_MAPPING = new HashMap() {{ put(FIELD_TEST_CASE, METHOD_CURRENT_TEST_CASE_NAME); put(FIELD_SOURCE_METHOD, METHOD_CURRENT_SOURCE_METHOD_NAME); @@ -104,6 +105,7 @@ public class TestClassHandler extends BaseClassHandler { il.add(getIntInsn(size)); il.add(new TypeInsnNode(ANEWARRAY, ClassUtil.CLASS_OBJECT)); for (int i = 0; i < size; i++) { + mn.maxStack += 2; il.add(new InsnNode(DUP)); il.add(getIntInsn(i)); ImmutablePair code = getLoadParameterByteCode(types.get(i)); @@ -115,11 +117,26 @@ public class TestClassHandler extends BaseClassHandler { } il.add(new InsnNode(AASTORE)); } + if (isMockForConstructor(mn)) { + il.add(new InsnNode(ICONST_1)); + } else { + il.add(new InsnNode(ICONST_0)); + } il.add(new MethodInsnNode(INVOKESTATIC, CLASS_INVOKE_RECORD_UTIL, METHOD_RECORD_MOCK_INVOKE, - SIGNATURE_INVOKE_COUNTER_METHOD, false)); + SIGNATURE_INVOKE_RECORDER_METHOD, false)); mn.instructions.insertBefore(mn.instructions.get(0), il); } + private boolean isMockForConstructor(MethodNode mn) { + for (AnnotationNode an : mn.visibleAnnotations) { + String method = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_METHOD, null); + if (ConstPool.CONSTRUCTOR.equals(method)) { + return true; + } + } + return false; + } + private static ImmutablePair getLoadParameterByteCode(Byte type) { switch (type) { case ClassUtil.TYPE_BYTE: diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/transformer/TestableClassTransformer.java b/testable-agent/src/main/java/com/alibaba/testable/agent/transformer/TestableClassTransformer.java index cb46281..77c927e 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/transformer/TestableClassTransformer.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/transformer/TestableClassTransformer.java @@ -6,6 +6,7 @@ import com.alibaba.testable.agent.handler.TestClassHandler; import com.alibaba.testable.agent.tool.ImmutablePair; import com.alibaba.testable.agent.model.MethodInfo; import com.alibaba.testable.agent.tool.ComparableWeakRef; +import com.alibaba.testable.agent.util.AnnotationUtil; import com.alibaba.testable.agent.util.ClassUtil; import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.AnnotationNode; @@ -28,7 +29,6 @@ import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassNa public class TestableClassTransformer implements ClassFileTransformer { private final Set> loadedClassNames = ComparableWeakRef.getWeekHashSet(); - private static final String TARGET_METHOD = "targetMethod"; @Override public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, @@ -90,7 +90,7 @@ public class TestableClassTransformer implements ClassFileTransformer { for (AnnotationNode an : mn.visibleAnnotations) { if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_MOCK)) { String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left); - String targetMethod = getAnnotationParameter(an, TARGET_METHOD, mn.name); + String targetMethod = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_METHOD, mn.name); if (targetMethod.equals(ConstPool.CONSTRUCTOR)) { String sourceClassName = ClassUtil.getSourceClassName(cn.name); methodInfos.add(new MethodInfo(sourceClassName, targetMethod, mn.name, mn.desc)); @@ -112,18 +112,4 @@ public class TestableClassTransformer implements ClassFileTransformer { return pos < 0 ? null : ImmutablePair.of(desc.substring(1, pos + 1), "(" + desc.substring(pos + 1)); } - /** - * Read value of annotation parameter - */ - private T getAnnotationParameter(AnnotationNode an, String key, T defaultValue) { - if (an.values != null) { - for (int i = 0; i < an.values.size(); i += 2) { - if (an.values.get(i).equals(key)) { - return (T)(an.values.get(i + 1)); - } - } - } - return defaultValue; - } - } diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/util/AnnotationUtil.java b/testable-agent/src/main/java/com/alibaba/testable/agent/util/AnnotationUtil.java new file mode 100644 index 0000000..06935ce --- /dev/null +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/util/AnnotationUtil.java @@ -0,0 +1,24 @@ +package com.alibaba.testable.agent.util; + +import org.objectweb.asm.tree.AnnotationNode; + +/** + * @author flin + */ +public class AnnotationUtil { + + /** + * Read value of annotation parameter + */ + public static T getAnnotationParameter(AnnotationNode an, String key, T defaultValue) { + if (an.values != null) { + for (int i = 0; i < an.values.size(); i += 2) { + if (an.values.get(i).equals(key)) { + return (T)(an.values.get(i + 1)); + } + } + } + return defaultValue; + } + +} diff --git a/testable-agent/src/test/java/com/alibaba/testable/agent/transformer/TestableClassTransformerTest.java b/testable-agent/src/test/java/com/alibaba/testable/agent/transformer/TestableClassTransformerTest.java deleted file mode 100644 index d1c29e0..0000000 --- a/testable-agent/src/test/java/com/alibaba/testable/agent/transformer/TestableClassTransformerTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.alibaba.testable.agent.transformer; - -import com.alibaba.testable.core.accessor.PrivateAccessor; -import org.junit.jupiter.api.Test; -import org.objectweb.asm.tree.AnnotationNode; - -import static com.alibaba.testable.agent.util.CollectionUtil.listOf; -import static org.junit.jupiter.api.Assertions.*; - -class TestableClassTransformerTest { - - private TestableClassTransformer transformer = new TestableClassTransformer(); - - @Test - void should_get_annotation_parameter() { - AnnotationNode an = new AnnotationNode(""); - an.values = listOf((Object)"testKey", "testValue", "demoKey", "demoValue"); - assertEquals("testValue", PrivateAccessor.invoke(transformer, "getAnnotationParameter", an, "testKey", "none")); - assertEquals("demoValue", PrivateAccessor.invoke(transformer, "getAnnotationParameter", an, "demoKey", "none")); - assertEquals("none", PrivateAccessor.invoke(transformer, "getAnnotationParameter", an, "testValue", "none")); - } - -} diff --git a/testable-agent/src/test/java/com/alibaba/testable/agent/util/AnnotationUtilTest.java b/testable-agent/src/test/java/com/alibaba/testable/agent/util/AnnotationUtilTest.java new file mode 100644 index 0000000..da2c4bb --- /dev/null +++ b/testable-agent/src/test/java/com/alibaba/testable/agent/util/AnnotationUtilTest.java @@ -0,0 +1,20 @@ +package com.alibaba.testable.agent.util; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.tree.AnnotationNode; + +import static com.alibaba.testable.agent.util.CollectionUtil.listOf; +import static org.junit.jupiter.api.Assertions.*; + +class AnnotationUtilTest { + + @Test + void should_get_annotation_parameter() { + AnnotationNode an = new AnnotationNode(""); + an.values = listOf((Object)"testKey", "testValue", "demoKey", "demoValue"); + assertEquals("testValue", AnnotationUtil.getAnnotationParameter(an, "testKey", "none")); + assertEquals("demoValue", AnnotationUtil.getAnnotationParameter(an, "demoKey", "none")); + assertEquals("none", AnnotationUtil.getAnnotationParameter(an, "testValue", "none")); + } + +} diff --git a/testable-core/src/main/java/com/alibaba/testable/core/accessor/PrivateAccessor.java b/testable-core/src/main/java/com/alibaba/testable/core/accessor/PrivateAccessor.java index 19f9959..63b2b7f 100644 --- a/testable-core/src/main/java/com/alibaba/testable/core/accessor/PrivateAccessor.java +++ b/testable-core/src/main/java/com/alibaba/testable/core/accessor/PrivateAccessor.java @@ -1,5 +1,6 @@ package com.alibaba.testable.core.accessor; +import com.alibaba.testable.core.util.InvokeRecordUtil; import com.alibaba.testable.core.util.TypeUtil; import java.lang.reflect.Field; @@ -46,4 +47,18 @@ public class PrivateAccessor { return null; } + public static T invokeStatic(Class clazz, String method, Object... args) { + try { + Class[] cls = TypeUtil.getClassesFromObjects(args); + Method declaredMethod = TypeUtil.getMethodByNameAndParameterTypes(clazz.getDeclaredMethods(), method, cls); + if (declaredMethod != null) { + declaredMethod.setAccessible(true); + return (T)declaredMethod.invoke(null, args); + } + } catch (Exception e) { + e.printStackTrace(); + return null; + } + return null; + } } diff --git a/testable-core/src/main/java/com/alibaba/testable/core/error/VerifyFailedError.java b/testable-core/src/main/java/com/alibaba/testable/core/error/VerifyFailedError.java index 742a94e..aa9d2fe 100644 --- a/testable-core/src/main/java/com/alibaba/testable/core/error/VerifyFailedError.java +++ b/testable-core/src/main/java/com/alibaba/testable/core/error/VerifyFailedError.java @@ -5,12 +5,24 @@ package com.alibaba.testable.core.error; */ public class VerifyFailedError extends AssertionError { - public VerifyFailedError(int actualCount, int expectedCount) { - super(getErrorMessage(actualCount, expectedCount)); + public VerifyFailedError(String message) { + super(getErrorMessage(message)); } - private static String getErrorMessage(int actualCount, int expectedCount) { - return "\nExpected times : " + expectedCount + "\nActual times : " + actualCount; + public VerifyFailedError(String expected, String actual) { + super(getErrorMessage(expected, actual)); + } + + public VerifyFailedError(String message, String expected, String actual) { + super(getErrorMessage(message) + getErrorMessage(expected, actual)); + } + + private static String getErrorMessage(String message) { + return "\n" + message.substring(0, 1).toUpperCase() + message.substring(1); + } + + private static String getErrorMessage(String expected, String actual) { + return "\nExpected " + expected + "\n Actual " + actual; } } diff --git a/testable-core/src/main/java/com/alibaba/testable/core/tool/InvokeCounter.java b/testable-core/src/main/java/com/alibaba/testable/core/tool/InvokeCounter.java deleted file mode 100644 index 7f5b470..0000000 --- a/testable-core/src/main/java/com/alibaba/testable/core/tool/InvokeCounter.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.alibaba.testable.core.tool; - -import com.alibaba.testable.core.error.VerifyFailedError; - -/** - * @author flin - */ -public class InvokeCounter { - - private final int actualCount; - - public InvokeCounter(int actualCount) { - this.actualCount = actualCount; - } - - public void times(int expectedCount) { - if (expectedCount != actualCount) { - throw new VerifyFailedError(actualCount, expectedCount); - } - } - -} diff --git a/testable-core/src/main/java/com/alibaba/testable/core/tool/InvokeVerifier.java b/testable-core/src/main/java/com/alibaba/testable/core/tool/InvokeVerifier.java new file mode 100644 index 0000000..6acc0d6 --- /dev/null +++ b/testable-core/src/main/java/com/alibaba/testable/core/tool/InvokeVerifier.java @@ -0,0 +1,82 @@ +package com.alibaba.testable.core.tool; + +import com.alibaba.testable.core.error.VerifyFailedError; + +import java.util.List; + +/** + * @author flin + */ +public class InvokeVerifier { + + private final List records; + + public InvokeVerifier(List records) { + this.records = records; + } + + public InvokeVerifier with(Object arg1) { + with(new Object[]{arg1}); + return this; + } + + public InvokeVerifier with(Object arg1, Object arg2) { + with(new Object[]{arg1, arg2}); + return this; + } + + public InvokeVerifier with(Object arg1, Object arg2, Object arg3) { + with(new Object[]{arg1, arg2, arg3}); + return this; + } + + public InvokeVerifier with(Object arg1, Object arg2, Object arg3, Object arg4) { + with(new Object[]{arg1, arg2, arg3, arg4}); + return this; + } + + public InvokeVerifier with(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { + with(new Object[]{arg1, arg2, arg3, arg4, arg5}); + return this; + } + + public InvokeVerifier with(Object[] args) { + if (records.isEmpty()) { + throw new VerifyFailedError("has not more invoke"); + } + Object[] record = records.get(0); + if (record.length != args.length) { + throw new VerifyFailedError(desc(args), desc(record)); + } + for (int i = 0; i < args.length; i++) { + if (!args[i].getClass().equals(record[i].getClass())) { + throw new VerifyFailedError("parameter " + (i + 1) + " type mismatch", + ": " + args[i].getClass(), ": " + record[i].getClass()); + } + if (!args[i].equals(record[i])) { + throw new VerifyFailedError("parameter " + (i + 1) + " mismatched", desc(args), desc(record)); + } + } + records.remove(0); + return this; + } + + private String desc(Object[] args) { + StringBuilder sb = new StringBuilder(": "); + for (int i = 0; i < args.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(args[i]); + } + return sb.toString(); + } + + public InvokeVerifier times(int expectedCount) { + if (expectedCount != records.size()) { + throw new VerifyFailedError("times: " + records.size(), "times: " + expectedCount); + } + return this; + } + +} diff --git a/testable-core/src/main/java/com/alibaba/testable/core/tool/TestableTool.java b/testable-core/src/main/java/com/alibaba/testable/core/tool/TestableTool.java index 8e46bf9..36f6c5d 100644 --- a/testable-core/src/main/java/com/alibaba/testable/core/tool/TestableTool.java +++ b/testable-core/src/main/java/com/alibaba/testable/core/tool/TestableTool.java @@ -27,10 +27,10 @@ public class TestableTool { * Get counter to check whether specified mock method invoked * @param mockMethodName name of a mock method */ - public static InvokeCounter verify(String mockMethodName) { + public static InvokeVerifier verify(String mockMethodName) { String testClass = Thread.currentThread().getStackTrace()[InvokeRecordUtil.INDEX_OF_TEST_CLASS].getClassName(); String testCaseName = TestableUtil.currentTestCaseName(testClass); - return new InvokeCounter(InvokeRecordUtil.getInvokeCount(mockMethodName, testCaseName)); + return new InvokeVerifier(InvokeRecordUtil.getInvokeRecord(mockMethodName, testCaseName)); } } diff --git a/testable-core/src/main/java/com/alibaba/testable/core/util/InvokeRecordUtil.java b/testable-core/src/main/java/com/alibaba/testable/core/util/InvokeRecordUtil.java index 3f017d2..88a4b1c 100644 --- a/testable-core/src/main/java/com/alibaba/testable/core/util/InvokeRecordUtil.java +++ b/testable-core/src/main/java/com/alibaba/testable/core/util/InvokeRecordUtil.java @@ -1,6 +1,8 @@ package com.alibaba.testable.core.util; import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; /** @@ -8,8 +10,11 @@ import java.util.Map; */ public class InvokeRecordUtil { - private static final Map INVOKE_RECORDS = new HashMap(); - private final static String JOINER = "->"; + /** + * Mock method name -> List of invoke parameters + */ + private static final Map> INVOKE_RECORDS = new HashMap>(); + private final static String JOINER = "::"; /** * [0]Thread -> [1]TestableUtil/TestableTool -> [2]TestClass @@ -17,32 +22,40 @@ public class InvokeRecordUtil { public static final int INDEX_OF_TEST_CLASS = 2; /** - * Record mock method invoke count + * Record mock method invoke event */ - public static void countMockInvoke() { + public static void recordMockInvoke(Object[] args, boolean isConstructor) { StackTraceElement mockMethodTraceElement = Thread.currentThread().getStackTrace()[INDEX_OF_TEST_CLASS]; String mockMethodName = mockMethodTraceElement.getMethodName(); String testClass = mockMethodTraceElement.getClassName(); String testCaseName = TestableUtil.currentTestCaseName(testClass); String key = testCaseName + JOINER + mockMethodName; - int count = getInvokeCount(mockMethodName, testCaseName); - INVOKE_RECORDS.put(key, count + 1); - } - - /** - * Record mock method invoke event - */ - public static void recordMockInvoke(Object[] args) { - countMockInvoke(); + List records = getInvokeRecord(mockMethodName, testCaseName); + if (isConstructor) { + records.add(args); + } else { + records.add(slice(args, 1)); + } + INVOKE_RECORDS.put(key, records); } /** * Get mock method invoke count */ - public static int getInvokeCount(String mockMethodName, String testCaseName) { + public static List getInvokeRecord(String mockMethodName, String testCaseName) { String key = testCaseName + JOINER + mockMethodName; - Integer count = INVOKE_RECORDS.get(key); - return (count == null) ? 0 : count; + List records = INVOKE_RECORDS.get(key); + return (records == null) ? new LinkedList() : records; + } + + private static Object[] slice(Object[] args, int firstIndex) { + int size = args.length - firstIndex; + if (size <= 0) { + return new Object[0]; + } + Object[] slicedArgs = new Object[size]; + System.arraycopy(args, firstIndex, slicedArgs, 0, size); + return slicedArgs; } } diff --git a/testable-core/src/test/java/com/alibaba/testable/core/util/InvokeRecordUtilTest.java b/testable-core/src/test/java/com/alibaba/testable/core/util/InvokeRecordUtilTest.java new file mode 100644 index 0000000..5005f09 --- /dev/null +++ b/testable-core/src/test/java/com/alibaba/testable/core/util/InvokeRecordUtilTest.java @@ -0,0 +1,18 @@ +package com.alibaba.testable.core.util; + +import com.alibaba.testable.core.accessor.PrivateAccessor; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class InvokeRecordUtilTest { + + @Test + void should_slice_array() { + Object[] args = new Object[]{"1", "2", "3"}; + Object[] slicedArgs = PrivateAccessor.invokeStatic(InvokeRecordUtil.class, "slice", args, 1); + assertEquals(2, slicedArgs.length); + assertEquals("2", slicedArgs[0]); + assertEquals("3", slicedArgs[1]); + } +}