diff --git a/agent/src/main/java/com/alibaba/testable/agent/handler/TestClassHandler.java b/agent/src/main/java/com/alibaba/testable/agent/handler/TestClassHandler.java index 28b323b..f7fe0f8 100644 --- a/agent/src/main/java/com/alibaba/testable/agent/handler/TestClassHandler.java +++ b/agent/src/main/java/com/alibaba/testable/agent/handler/TestClassHandler.java @@ -5,13 +5,26 @@ import com.alibaba.testable.agent.util.ClassUtil; import org.objectweb.asm.tree.*; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * @author flin */ public class TestClassHandler extends BaseClassHandler { + private static final String CLASS_TESTABLE_UTIL = "com/alibaba/testable/core/util/TestableUtil"; + private static final String FIELD_TEST_CASE = "TEST_CASE"; + private static final String FIELD_SOURCE_METHOD = "SOURCE_METHOD"; + private static final String METHOD_CURRENT_TEST_CASE_NAME = "currentTestCaseName"; + private static final String METHOD_CURRENT_SOURCE_METHOD_NAME = "currentSourceMethodName"; + private static final String SIGNATURE_TESTABLE_UTIL_METHOD = "(Ljava/lang/Object;)Ljava/lang/String;"; + 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); + }}; + /** * Handle bytecode of test class * @param cn original class node @@ -26,6 +39,11 @@ public class TestClassHandler extends BaseClassHandler { } private void transformMethod(ClassNode cn, MethodNode mn) { + handleAnnotation(cn, mn); + handleInstruction(cn, mn); + } + + private void handleAnnotation(ClassNode cn, MethodNode mn) { List visibleAnnotationNames = new ArrayList(); if (mn.visibleAnnotations == null) { return; @@ -42,6 +60,36 @@ public class TestClassHandler extends BaseClassHandler { } } + private void handleInstruction(ClassNode cn, MethodNode mn) { + AbstractInsnNode[] instructions = mn.instructions.toArray(); + int i = 0; + do { + if (instructions[i].getOpcode() == GETSTATIC) { + FieldInsnNode fieldInsnNode = (FieldInsnNode)instructions[i]; + if (isTestableUtilField(fieldInsnNode)) { + instructions = replaceTestableUtilField(mn, instructions, fieldInsnNode.name, i); + } + } + i++; + } while (i < instructions.length); + } + + private boolean isTestableUtilField(FieldInsnNode fieldInsnNode) { + return fieldInsnNode.owner.equals(CLASS_TESTABLE_UTIL) && + (fieldInsnNode.name.equals(FIELD_TEST_CASE) || fieldInsnNode.name.equals(FIELD_SOURCE_METHOD)); + } + + private AbstractInsnNode[] replaceTestableUtilField(MethodNode mn, AbstractInsnNode[] instructions, + String fieldName, int pos) { + InsnList insnNodes = new InsnList(); + insnNodes.insert(new VarInsnNode(ALOAD, 0)); + insnNodes.insert(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, FIELD_TO_METHOD_MAPPING.get(fieldName), + SIGNATURE_TESTABLE_UTIL_METHOD, false)); + mn.instructions.insertBefore(instructions[pos], insnNodes); + mn.instructions.remove(instructions[pos]); + return mn.instructions.toArray(); + } + private void injectTestableRef(ClassNode cn, MethodNode mn) { InsnList il = new InsnList(); il.add(new VarInsnNode(ALOAD, 0)); diff --git a/core/src/main/java/com/alibaba/testable/core/util/TestableUtil.java b/core/src/main/java/com/alibaba/testable/core/util/TestableUtil.java index 91d0001..e96acdd 100644 --- a/core/src/main/java/com/alibaba/testable/core/util/TestableUtil.java +++ b/core/src/main/java/com/alibaba/testable/core/util/TestableUtil.java @@ -7,6 +7,9 @@ import com.alibaba.testable.core.constant.ConstPool; */ public class TestableUtil { + public static String TEST_CASE; + public static String SOURCE_METHOD; + public static String currentSourceMethodName(Object testClassRef) { Class testClass = testClassRef.getClass(); StackTraceElement[] stack = getMainThread().getStackTrace(); diff --git a/demo/java-demo/src/test/java/com/alibaba/testable/demo/DemoServiceTest.java b/demo/java-demo/src/test/java/com/alibaba/testable/demo/DemoServiceTest.java index 204fb6f..1c45581 100644 --- a/demo/java-demo/src/test/java/com/alibaba/testable/demo/DemoServiceTest.java +++ b/demo/java-demo/src/test/java/com/alibaba/testable/demo/DemoServiceTest.java @@ -3,11 +3,12 @@ package com.alibaba.testable.demo; import com.alibaba.testable.core.accessor.PrivateAccessor; import com.alibaba.testable.core.annotation.EnableTestable; import com.alibaba.testable.core.annotation.TestableInject; -import com.alibaba.testable.core.util.TestableUtil; import org.junit.jupiter.api.Test; import java.util.concurrent.Callable; +import static com.alibaba.testable.core.util.TestableUtil.SOURCE_METHOD; +import static com.alibaba.testable.core.util.TestableUtil.TEST_CASE; import static org.junit.jupiter.api.Assertions.assertEquals; @EnableTestable @@ -40,7 +41,7 @@ class DemoServiceTest { @TestableInject private String callFromDifferentMethod() { - switch (TestableUtil.currentSourceMethodName(this)) { + switch (SOURCE_METHOD) { case "callerOne": return "mock_one"; default: return "mock_others"; } @@ -80,25 +81,15 @@ class DemoServiceTest { @Test void should_able_to_get_source_method_name() throws Exception { - assertEquals("mock_one", demoService.callerOne()); - assertEquals("mock_others", demoService.callerTwo()); - assertEquals("mock_one_mock_others", new Callable() { - @Override - public String call() { - return demoService.callerOne() + "_" + demoService.callerTwo(); - } - }.call()); + assertEquals("mock_one_mock_others", demoService.callerTwo() + "_" + demoService.callerOne()); + assertEquals("mock_one_mock_others", ((Callable)() -> + demoService.callerOne() + "_" + demoService.callerTwo()).call()); } @Test void should_able_to_get_test_case_name() throws Exception { - assertEquals("should_able_to_get_test_case_name", TestableUtil.currentTestCaseName(this)); - assertEquals("should_able_to_get_test_case_name", new Callable() { - @Override - public String call() { - return TestableUtil.currentTestCaseName(this); - } - }.call()); + assertEquals("should_able_to_get_test_case_name", TEST_CASE); + assertEquals("should_able_to_get_test_case_name", ((Callable)() -> TEST_CASE).call()); } } diff --git a/demo/kotlin-demo/src/test/kotlin/com/alibaba/testable/demo/DemoServiceTest.kt b/demo/kotlin-demo/src/test/kotlin/com/alibaba/testable/demo/DemoServiceTest.kt index de9bbe8..45ac974 100644 --- a/demo/kotlin-demo/src/test/kotlin/com/alibaba/testable/demo/DemoServiceTest.kt +++ b/demo/kotlin-demo/src/test/kotlin/com/alibaba/testable/demo/DemoServiceTest.kt @@ -3,8 +3,9 @@ package com.alibaba.testable.demo import com.alibaba.testable.core.accessor.PrivateAccessor import com.alibaba.testable.core.annotation.EnableTestable import com.alibaba.testable.core.annotation.TestableInject -import com.alibaba.testable.core.util.TestableUtil -import org.junit.jupiter.api.Assertions +import com.alibaba.testable.core.util.TestableUtil.SOURCE_METHOD +import com.alibaba.testable.core.util.TestableUtil.TEST_CASE +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import java.util.concurrent.Callable @@ -28,7 +29,7 @@ internal class DemoServiceTest { private fun startsWith(self: BlackBox, s: String) = false @TestableInject - private fun callFromDifferentMethod() = when (TestableUtil.currentSourceMethodName(this)) { + private fun callFromDifferentMethod() = when (SOURCE_METHOD) { "callerOne" -> "mock_one" else -> "mock_others" } @@ -37,45 +38,42 @@ internal class DemoServiceTest { @Test fun should_able_to_test_private_method() { - Assertions.assertEquals("hello - 1", PrivateAccessor.invoke(demoService, "privateFunc", "hello", 1)) + assertEquals("hello - 1", PrivateAccessor.invoke(demoService, "privateFunc", "hello", 1)) } @Test fun should_able_to_test_private_field() { PrivateAccessor.set(demoService, "count", 3) - Assertions.assertEquals("5", demoService.privateFieldAccessFunc()) - Assertions.assertEquals(5, PrivateAccessor.get(demoService, "count")) + assertEquals("5", demoService.privateFieldAccessFunc()) + assertEquals(5, PrivateAccessor.get(demoService, "count")) } @Test fun should_able_to_test_new_object() { - Assertions.assertEquals("mock_something", demoService.newFunc()) + assertEquals("mock_something", demoService.newFunc()) } @Test fun should_able_to_test_member_method() { - Assertions.assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello")) + assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello")) } @Test fun should_able_to_test_common_method() { - Assertions.assertEquals("trim_string__sub_string__false", demoService.commonFunc()) + assertEquals("trim_string__sub_string__false", demoService.commonFunc()) } @Test fun should_able_to_get_source_method_name() { - Assertions.assertEquals("mock_one", demoService.callerOne()) - Assertions.assertEquals("mock_others", demoService.callerTwo()) - Assertions.assertEquals("mock_one_mock_others", Callable { + assertEquals("mock_one_mock_others", demoService.callerTwo() + "_" + demoService.callerOne()) + assertEquals("mock_one_mock_others", Callable { demoService.callerOne() + "_" + demoService.callerTwo() }.call()) } @Test fun should_able_to_get_test_case_name() { - Assertions.assertEquals("should_able_to_get_test_case_name", TestableUtil.currentTestCaseName(this)) - Assertions.assertEquals("should_able_to_get_test_case_name", Callable { - TestableUtil.currentTestCaseName(this) - }.call()) + assertEquals("should_able_to_get_test_case_name", TEST_CASE) + assertEquals("should_able_to_get_test_case_name", Callable { TEST_CASE }.call()) } }