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 8e863c5..50a8b7f 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 @@ -39,7 +39,7 @@ class DemoServiceTest { } @TestableMock - private BlackBox secretBox() { + private BlackBox secretBox(BlackBox _) { return new BlackBox("not_secret_box"); } @@ -91,6 +91,12 @@ class DemoServiceTest { verify("startsWith").times(1); } + @Test + void should_able_to_mock_static_method() throws Exception { + assertEquals("not_secret_box", demoService.getBox().callMe()); + verify("secretBox").times(1); + } + @Test void should_able_to_get_source_method_name() throws Exception { // synchronous diff --git a/demo/kotlin-demo/src/main/kotlin/com/alibaba/testable/demo/BlackBox.kt b/demo/kotlin-demo/src/main/kotlin/com/alibaba/testable/demo/BlackBox.kt index bcf17ea..a789a3c 100644 --- a/demo/kotlin-demo/src/main/kotlin/com/alibaba/testable/demo/BlackBox.kt +++ b/demo/kotlin-demo/src/main/kotlin/com/alibaba/testable/demo/BlackBox.kt @@ -28,7 +28,7 @@ class BlackBox(private val data: String) { } object ColorBox { - fun createBox(color: String): BlackBox { - return BlackBox("${color}_Box") + fun createBox(color: String, box: BlackBox): BlackBox { + return BlackBox("${color}_${box.callMe()}") } } diff --git a/demo/kotlin-demo/src/main/kotlin/com/alibaba/testable/demo/DemoService.kt b/demo/kotlin-demo/src/main/kotlin/com/alibaba/testable/demo/DemoService.kt index 61125b3..bf39b8d 100644 --- a/demo/kotlin-demo/src/main/kotlin/com/alibaba/testable/demo/DemoService.kt +++ b/demo/kotlin-demo/src/main/kotlin/com/alibaba/testable/demo/DemoService.kt @@ -47,6 +47,13 @@ class DemoService { return box.trim() + "__" + box.substring(1, 2) + "__" + box.startsWith("any") } + /** + * Target 6 - method with static method invoke + */ + fun getBox(): BlackBox { + return ColorBox.createBox("Red", BlackBox.secretBox()) + } + fun callerOne(): String { return callFromDifferentMethod() } 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 f4c29b6..ce01a0f 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 @@ -1,9 +1,9 @@ package com.alibaba.testable.demo import com.alibaba.testable.core.accessor.PrivateAccessor -import com.alibaba.testable.processor.annotation.EnablePrivateAccess import com.alibaba.testable.core.annotation.TestableMock import com.alibaba.testable.core.tool.TestableTool.* +import com.alibaba.testable.processor.annotation.EnablePrivateAccess import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import java.util.concurrent.Executors @@ -27,6 +27,16 @@ internal class DemoServiceTest { @TestableMock private fun startsWith(self: BlackBox, s: String) = false + @TestableMock + private fun secretBox(ignore: BlackBox): BlackBox { + return BlackBox("not_secret_box") + } + + @TestableMock + private fun createBox(ignore: ColorBox, color: String, box: BlackBox): BlackBox { + return BlackBox("White_${box.callMe()}") + } + @TestableMock private fun callFromDifferentMethod(self: DemoService): String { return if (TEST_CASE == "should_able_to_get_test_case_name") { @@ -73,6 +83,13 @@ internal class DemoServiceTest { verify("startsWith").times(1) } + @Test + fun should_able_to_mock_static_method() { + assertEquals("White_not_secret_box", demoService.getBox().callMe()) + verify("secretBox").times(1) + verify("createBox").times(1) + } + @Test fun should_able_to_get_source_method_name() { // synchronous diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/SourceClassHandler.java b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/SourceClassHandler.java index 933ebbb..7873432 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/SourceClassHandler.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/SourceClassHandler.java @@ -19,6 +19,7 @@ public class SourceClassHandler extends BaseClassHandler { private final List injectMethods; private final Set invokeOps = new HashSet() {{ + add(Opcodes.INVOKESTATIC); add(Opcodes.INVOKESPECIAL); add(Opcodes.INVOKEVIRTUAL); }}; @@ -60,7 +61,8 @@ public class SourceClassHandler extends BaseClassHandler { // it's a member method and an inject method for it exist int rangeStart = getMemberMethodStart(instructions, i); if (rangeStart >= 0) { - instructions = replaceMemberCallOps(cn, mn, instructions, node.owner, memberInjectMethodName, rangeStart, i); + instructions = replaceMemberCallOps(cn, mn, memberInjectMethodName, instructions, + node.owner, node.getOpcode(), rangeStart, i); i = rangeStart; } } else if (ConstPool.CONSTRUCTOR.equals(node.name)) { @@ -123,6 +125,9 @@ public class SourceClassHandler extends BaseClassHandler { case Opcodes.INVOKESTATIC: stackLevel += ClassUtil.getParameterTypes(((MethodInsnNode)instructions[i]).desc).size(); break; + case -1: + // reach LineNumberNode or LabelNode + return i + 1; default: stackLevel -= BytecodeUtil.stackEffect(instructions[i].getOpcode()); } @@ -153,13 +158,18 @@ public class SourceClassHandler extends BaseClassHandler { ClassUtil.toByteCodeClassName(classType); } - private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions, - String ownerClass, String substitutionMethod, int start, int end) { + private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, String substitutionMethod, + AbstractInsnNode[] instructions, String ownerClass, + int opcode, int start, int end) { mn.maxStack++; MethodInsnNode method = (MethodInsnNode)instructions[end]; String testClassName = ClassUtil.getTestClassName(cn.name); mn.instructions.insertBefore(instructions[start], new FieldInsnNode(GETSTATIC, testClassName, ConstPool.TESTABLE_INJECT_REF, ClassUtil.toByteCodeClassName(testClassName))); + if (Opcodes.INVOKESTATIC == opcode) { + // append a null value if it was a static invoke + mn.instructions.insertBefore(instructions[start], new InsnNode(ACONST_NULL)); + } mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName, substitutionMethod, addFirstParameter(method.desc, ownerClass), false)); mn.instructions.remove(instructions[end]);