mirror of
https://github.com/alibaba/testable-mock.git
synced 2025-02-04 00:30:52 +08:00
unify usage of TestableMock, no more targetClass
This commit is contained in:
parent
9981aafa93
commit
a0509a889f
@ -13,33 +13,33 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||||||
@EnablePrivateAccess
|
@EnablePrivateAccess
|
||||||
class DemoServiceTest {
|
class DemoServiceTest {
|
||||||
|
|
||||||
@TestableMock
|
@TestableMock(targetMethod = CONSTRUCTOR)
|
||||||
private BlackBox createBlackBox(String text) {
|
private BlackBox createBlackBox(String text) {
|
||||||
return new BlackBox("mock_" + text);
|
return new BlackBox("mock_" + text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestableMock
|
@TestableMock
|
||||||
private String innerFunc(String text) {
|
private String innerFunc(DemoService self, String text) {
|
||||||
return "mock_" + text;
|
return "mock_" + text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestableMock(targetClass = String.class)
|
@TestableMock
|
||||||
private String trim(String self) {
|
private String trim(String self) {
|
||||||
return "trim_string";
|
return "trim_string";
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestableMock(targetClass = String.class, targetMethod = "substring")
|
@TestableMock(targetMethod = "substring")
|
||||||
private String sub(String self, int i, int j) {
|
private String sub(String self, int i, int j) {
|
||||||
return "sub_string";
|
return "sub_string";
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestableMock(targetClass = String.class)
|
@TestableMock
|
||||||
private boolean startsWith(String self, String s) {
|
private boolean startsWith(String self, String s) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestableMock
|
@TestableMock
|
||||||
private String callFromDifferentMethod() {
|
private String callFromDifferentMethod(DemoService self) {
|
||||||
if (TEST_CASE.equals("should_able_to_get_test_case_name")) {
|
if (TEST_CASE.equals("should_able_to_get_test_case_name")) {
|
||||||
return "mock_special";
|
return "mock_special";
|
||||||
}
|
}
|
||||||
|
@ -12,23 +12,23 @@ import java.util.concurrent.Executors
|
|||||||
@EnablePrivateAccess
|
@EnablePrivateAccess
|
||||||
internal class DemoServiceTest {
|
internal class DemoServiceTest {
|
||||||
|
|
||||||
@TestableMock
|
@TestableMock(targetMethod = CONSTRUCTOR)
|
||||||
private fun createBlackBox(text: String) = BlackBox("mock_$text")
|
private fun createBlackBox(text: String) = BlackBox("mock_$text")
|
||||||
|
|
||||||
@TestableMock
|
@TestableMock
|
||||||
private fun innerFunc(text: String) = "mock_$text"
|
private fun innerFunc(self: DemoService, text: String) = "mock_$text"
|
||||||
|
|
||||||
@TestableMock(targetClass = BlackBox::class)
|
@TestableMock
|
||||||
private fun trim(self: BlackBox) = "trim_string"
|
private fun trim(self: BlackBox) = "trim_string"
|
||||||
|
|
||||||
@TestableMock(targetClass = BlackBox::class, targetMethod = "substring")
|
@TestableMock(targetMethod = "substring")
|
||||||
private fun sub(self: BlackBox, i: Int, j: Int) = "sub_string"
|
private fun sub(self: BlackBox, i: Int, j: Int) = "sub_string"
|
||||||
|
|
||||||
@TestableMock(targetClass = BlackBox::class)
|
@TestableMock
|
||||||
private fun startsWith(self: BlackBox, s: String) = false
|
private fun startsWith(self: BlackBox, s: String) = false
|
||||||
|
|
||||||
@TestableMock
|
@TestableMock
|
||||||
private fun callFromDifferentMethod(): String {
|
private fun callFromDifferentMethod(self: DemoService): String {
|
||||||
return if (TEST_CASE == "should_able_to_get_test_case_name") {
|
return if (TEST_CASE == "should_able_to_get_test_case_name") {
|
||||||
"mock_special"
|
"mock_special"
|
||||||
} else {
|
} else {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
- remove dependence on EnableTestable annotation in `testable-agent`
|
- remove dependence on EnableTestable annotation in `testable-agent`
|
||||||
- rename annotations to reflect the actual use
|
- rename annotations to reflect the actual use
|
||||||
|
|
||||||
- ## v0.1.0
|
## v0.1.0
|
||||||
- move generated agent jar to class folder
|
- move generated agent jar to class folder
|
||||||
- support mock method of any object
|
- support mock method of any object
|
||||||
|
|
||||||
|
@ -57,30 +57,30 @@
|
|||||||
|
|
||||||
**【1】覆写任意类的方法调用**
|
**【1】覆写任意类的方法调用**
|
||||||
|
|
||||||
定义一个普通方法,使它与需覆写的方法名称和返回值类型完全一致,且比原方法的参数列表在首位多一个与该方法所属对象类型一致的参数。然后为这个方法加上`@TestableMock`注解,并设置`targetClass`属性值也为该方法所属对象的类型。
|
在测试类里定义一个有`@TestableMock`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
|
||||||
|
|
||||||
此时被测类中所有对该类指定方法的调用,将在单元测试运行时,自动被替换为对上述自定义Mock方法的调用。
|
此时被测类中所有对该需覆写方法的调用,将在单元测试运行时,将自动被替换为对上述自定义Mock方法的调用。
|
||||||
|
|
||||||
`@TestableMock`注解还有一个很少需要用到的`targetMethod`属性,用于指定Mock的目标方法名称。使用此参数后被注释修饰的方法名称就可以随意命名了,通常仅在遇到极其罕见的Mock方法签名重名情况时才需要使用。
|
**注意**:当遇到有两个需覆写的方法重名时,可将需覆写的方法名写到`@TestableMock`注解的`targetMethod`参数里,此时Mock方法自身就可以随意命名了。
|
||||||
|
|
||||||
示例项目文件`DemoServiceTest.java`中的`should_able_to_test_common_method()`用例详细展示了这几种用法。
|
示例项目文件`DemoServiceTest.java`中的`should_able_to_test_common_method()`用例详细展示了这种用法。
|
||||||
|
|
||||||
**【2】覆写任意类的new操作**
|
**【2】覆写被测类自身的成员方法**
|
||||||
|
|
||||||
同样还是定义一个普通方法,然后加上`@TestableMock`注解。方法名称随意,只需让方法的返回值为要覆写new操作的目标类型,且参数与指定类构造方法完全一致。
|
有时候,在对某些方法进行测试时,希望将被测类自身的另外一些成员方法Mock掉。
|
||||||
|
|
||||||
此时被测类中所有用`new`创建指定类的操作将被替换为对该自定义方法的调用。
|
操作方法与前一种情况相同,Mock方法的第一个参数类型需与被测类相同,即可实现对被测类自身(不论是公有或私有)成员方法的覆写。
|
||||||
|
|
||||||
详见示例项目文件`DemoServiceTest.java`中的`should_able_to_test_new_object()`用例。
|
|
||||||
|
|
||||||
**【3】覆写被测类自身的私有成员方法**
|
|
||||||
|
|
||||||
有时候,被测类自身的某个成员方法访问了外部系统,在进行单元测试的时候就需要将这个备查样自己的成员方法Mock掉。
|
|
||||||
|
|
||||||
在测试类中声明一个名称、参数和返回值类型都与要覆写的目标方法完全一致的普通方法,同样加上`@TestableMock`注解,不配置`targetClass`属性,即可实现对被测类私有成员方法的覆写。
|
|
||||||
|
|
||||||
详见示例项目文件`DemoServiceTest.java`中的`should_able_to_test_member_method()`用例。
|
详见示例项目文件`DemoServiceTest.java`中的`should_able_to_test_member_method()`用例。
|
||||||
|
|
||||||
|
**【3】覆写任意类的new操作**
|
||||||
|
|
||||||
|
在测试类里定义一个有`@TestableMock`注解的普通方法,将注解的`targetMethod`参数写为"<init>",然后使该方法与要被创建类型的构造函数参数、返回值类型完全一致,方法名称随意。
|
||||||
|
|
||||||
|
此时被测类中所有用`new`创建指定类的操作(并使用了与Mock方法参数一致的构造函数)将被替换为对该自定义方法的调用。
|
||||||
|
|
||||||
|
详见示例项目文件`DemoServiceTest.java`中的`should_able_to_test_new_object()`用例。
|
||||||
|
|
||||||
**【4】识别当前测试用例和调用来源**
|
**【4】识别当前测试用例和调用来源**
|
||||||
|
|
||||||
在Mock方法中可以通过`TestableTool.TEST_CASE`和`TestableTool.SOURCE_METHOD`来识别**当前运行的测试用例名称**和**进入该Mock方法前的被测类方法名称**,从而区分处理不同的调用场景。
|
在Mock方法中可以通过`TestableTool.TEST_CASE`和`TestableTool.SOURCE_METHOD`来识别**当前运行的测试用例名称**和**进入该Mock方法前的被测类方法名称**,从而区分处理不同的调用场景。
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
<project.compiler.level>1.6</project.compiler.level>
|
<project.compiler.level>1.6</project.compiler.level>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<asm.lib.version>8.0.1</asm.lib.version>
|
<asm.lib.version>8.0.1</asm.lib.version>
|
||||||
|
<testable.version>0.2.0-SNAPSHOT</testable.version>
|
||||||
<plugin.compiler.version>3.8.1</plugin.compiler.version>
|
<plugin.compiler.version>3.8.1</plugin.compiler.version>
|
||||||
<plugin.jar.version>3.2.0</plugin.jar.version>
|
<plugin.jar.version>3.2.0</plugin.jar.version>
|
||||||
<plugin.shade.version>3.2.4</plugin.shade.version>
|
<plugin.shade.version>3.2.4</plugin.shade.version>
|
||||||
@ -31,6 +32,12 @@
|
|||||||
<version>5.6.2</version>
|
<version>5.6.2</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.testable</groupId>
|
||||||
|
<artifactId>testable-core</artifactId>
|
||||||
|
<version>${testable.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@ -4,8 +4,6 @@ import com.alibaba.testable.agent.constant.ConstPool;
|
|||||||
import com.alibaba.testable.agent.model.MethodInfo;
|
import com.alibaba.testable.agent.model.MethodInfo;
|
||||||
import com.alibaba.testable.agent.util.BytecodeUtil;
|
import com.alibaba.testable.agent.util.BytecodeUtil;
|
||||||
import com.alibaba.testable.agent.util.ClassUtil;
|
import com.alibaba.testable.agent.util.ClassUtil;
|
||||||
import com.alibaba.testable.agent.util.CollectionUtil;
|
|
||||||
import com.alibaba.testable.agent.util.StringUtil;
|
|
||||||
import org.objectweb.asm.Opcodes;
|
import org.objectweb.asm.Opcodes;
|
||||||
import org.objectweb.asm.tree.*;
|
import org.objectweb.asm.tree.*;
|
||||||
|
|
||||||
@ -35,22 +33,15 @@ public class SourceClassHandler extends BaseClassHandler {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void transform(ClassNode cn) {
|
protected void transform(ClassNode cn) {
|
||||||
List<MethodInfo> methods = new ArrayList<MethodInfo>();
|
Set<MethodInfo> memberInjectMethods = new HashSet<MethodInfo>();
|
||||||
for (MethodNode m : cn.methods) {
|
Set<MethodInfo> newOperatorInjectMethods = new HashSet<MethodInfo>();
|
||||||
// record all member method names
|
|
||||||
if (!ConstPool.CONSTRUCTOR.equals(m.name)) {
|
|
||||||
methods.add(new MethodInfo(cn.name, m.name, null, m.desc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// member methods which has injection stub
|
|
||||||
Set<MethodInfo> memberInjectMethods = CollectionUtil.getCrossSet(methods, injectMethods);
|
|
||||||
for (MethodInfo mi : injectMethods) {
|
for (MethodInfo mi : injectMethods) {
|
||||||
if (!mi.getClazz().equals(cn.name)) {
|
if (mi.getName().equals(ConstPool.CONSTRUCTOR)) {
|
||||||
|
newOperatorInjectMethods.add(mi);
|
||||||
|
} else {
|
||||||
memberInjectMethods.add(mi);
|
memberInjectMethods.add(mi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// new operations which has injection stub
|
|
||||||
Set<MethodInfo> newOperatorInjectMethods = CollectionUtil.getMinusSet(injectMethods, memberInjectMethods);
|
|
||||||
for (MethodNode m : cn.methods) {
|
for (MethodNode m : cn.methods) {
|
||||||
transformMethod(cn, m, memberInjectMethods, newOperatorInjectMethods);
|
transformMethod(cn, m, memberInjectMethods, newOperatorInjectMethods);
|
||||||
}
|
}
|
||||||
@ -64,25 +55,19 @@ public class SourceClassHandler extends BaseClassHandler {
|
|||||||
do {
|
do {
|
||||||
if (invokeOps.contains(instructions[i].getOpcode())) {
|
if (invokeOps.contains(instructions[i].getOpcode())) {
|
||||||
MethodInsnNode node = (MethodInsnNode)instructions[i];
|
MethodInsnNode node = (MethodInsnNode)instructions[i];
|
||||||
int index = memberInjectMethodList.indexOf(new MethodInfo(node.owner, node.name, null, node.desc));
|
String memberInjectMethodName = getMemberInjectMethodName(memberInjectMethodList, node);
|
||||||
if (index >= 0) {
|
if (memberInjectMethodName != null) {
|
||||||
// it's a member method and an inject method for it exist
|
// it's a member method and an inject method for it exist
|
||||||
int rangeStart = getMemberMethodStart(instructions, i);
|
int rangeStart = getMemberMethodStart(instructions, i);
|
||||||
if (rangeStart >= 0) {
|
if (rangeStart >= 0) {
|
||||||
if (cn.name.equals(node.owner)) {
|
instructions = replaceMemberCallOps(cn, mn, instructions, node.owner, memberInjectMethodName, rangeStart, i);
|
||||||
// member method of current class
|
|
||||||
instructions = replaceMemberCallOps(cn, mn, instructions, rangeStart, i);
|
|
||||||
} else {
|
|
||||||
// member method of other class
|
|
||||||
String method = memberInjectMethodList.get(index).getSubstitutionMethod();
|
|
||||||
instructions = replaceCommonCallOps(cn, mn, instructions, node.owner, method, rangeStart, i);
|
|
||||||
}
|
|
||||||
i = rangeStart;
|
i = rangeStart;
|
||||||
}
|
}
|
||||||
} else if (ConstPool.CONSTRUCTOR.equals(node.name)) {
|
} else if (ConstPool.CONSTRUCTOR.equals(node.name)) {
|
||||||
// it's a new operation
|
// it's a new operation
|
||||||
String newOperatorInjectMethodName = getNewOperatorInjectMethodName(newOperatorInjectMethods, node);
|
String newOperatorInjectMethodName = getNewOperatorInjectMethodName(newOperatorInjectMethods, node);
|
||||||
if (newOperatorInjectMethodName.length() > 0) {
|
if (newOperatorInjectMethodName != null) {
|
||||||
|
// and an inject method for it exist
|
||||||
int rangeStart = getConstructorStart(instructions, node.owner, i);
|
int rangeStart = getConstructorStart(instructions, node.owner, i);
|
||||||
if (rangeStart >= 0) {
|
if (rangeStart >= 0) {
|
||||||
instructions = replaceNewOps(cn, mn, newOperatorInjectMethodName, instructions, rangeStart, i);
|
instructions = replaceNewOps(cn, mn, newOperatorInjectMethodName, instructions, rangeStart, i);
|
||||||
@ -95,13 +80,22 @@ public class SourceClassHandler extends BaseClassHandler {
|
|||||||
} while (i < instructions.length);
|
} while (i < instructions.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getMemberInjectMethodName(List<MethodInfo> memberInjectMethodList, MethodInsnNode node) {
|
||||||
|
for (MethodInfo m : memberInjectMethodList) {
|
||||||
|
if (m.getClazz().equals(node.owner) && m.getName().equals(node.name) && m.getDesc().equals(node.desc)) {
|
||||||
|
return m.getMockName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private String getNewOperatorInjectMethodName(Set<MethodInfo> newOperatorInjectMethods, MethodInsnNode node) {
|
private String getNewOperatorInjectMethodName(Set<MethodInfo> newOperatorInjectMethods, MethodInsnNode node) {
|
||||||
for (MethodInfo m : newOperatorInjectMethods) {
|
for (MethodInfo m : newOperatorInjectMethods) {
|
||||||
if (m.getDesc().equals(getConstructorInjectDesc(node))) {
|
if (m.getDesc().equals(getConstructorInjectDesc(node))) {
|
||||||
return m.getName();
|
return m.getMockName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getConstructorInjectDesc(MethodInsnNode constructorNode) {
|
private String getConstructorInjectDesc(MethodInsnNode constructorNode) {
|
||||||
@ -160,19 +154,6 @@ public class SourceClassHandler extends BaseClassHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions,
|
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions,
|
||||||
int start, int end) {
|
|
||||||
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)));
|
|
||||||
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,
|
|
||||||
method.name, method.desc, false));
|
|
||||||
mn.instructions.remove(instructions[start]);
|
|
||||||
mn.instructions.remove(instructions[end]);
|
|
||||||
return mn.instructions.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private AbstractInsnNode[] replaceCommonCallOps(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions,
|
|
||||||
String ownerClass, String substitutionMethod, int start, int end) {
|
String ownerClass, String substitutionMethod, int start, int end) {
|
||||||
mn.maxStack++;
|
mn.maxStack++;
|
||||||
MethodInsnNode method = (MethodInsnNode)instructions[end];
|
MethodInsnNode method = (MethodInsnNode)instructions[end];
|
||||||
|
@ -6,27 +6,26 @@ package com.alibaba.testable.agent.model;
|
|||||||
public class MethodInfo {
|
public class MethodInfo {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* name of the class this method belongs to
|
* name of the class this method belongs to (in slash-separate format)
|
||||||
*/
|
*/
|
||||||
private final String clazz;
|
private final String clazz;
|
||||||
/**
|
/**
|
||||||
* name of the method
|
* name of the source method
|
||||||
*/
|
*/
|
||||||
private final String name;
|
private final String name;
|
||||||
/**
|
/**
|
||||||
* name of the substitution method
|
* name of the mock method
|
||||||
* Note: this field do NOT join the `equals()` or `hashCode()` calculation
|
|
||||||
*/
|
*/
|
||||||
private final String substitutionMethod;
|
private final String mockName;
|
||||||
/**
|
/**
|
||||||
* parameter and return value of the method
|
* parameter and return value of the source method
|
||||||
*/
|
*/
|
||||||
private final String desc;
|
private final String desc;
|
||||||
|
|
||||||
public MethodInfo(String clazz, String name, String substitutionMethod, String desc) {
|
public MethodInfo(String clazz, String name, String mockName, String desc) {
|
||||||
this.clazz = clazz;
|
this.clazz = clazz;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.substitutionMethod = substitutionMethod;
|
this.mockName = mockName;
|
||||||
this.desc = desc;
|
this.desc = desc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,8 +37,8 @@ public class MethodInfo {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSubstitutionMethod() {
|
public String getMockName() {
|
||||||
return substitutionMethod;
|
return mockName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDesc() {
|
public String getDesc() {
|
||||||
@ -55,6 +54,7 @@ public class MethodInfo {
|
|||||||
|
|
||||||
if (!clazz.equals(that.clazz)) { return false; }
|
if (!clazz.equals(that.clazz)) { return false; }
|
||||||
if (!name.equals(that.name)) { return false; }
|
if (!name.equals(that.name)) { return false; }
|
||||||
|
if (!mockName.equals(that.mockName)) { return false; }
|
||||||
return desc.equals(that.desc);
|
return desc.equals(that.desc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,6 +62,7 @@ public class MethodInfo {
|
|||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int result = clazz.hashCode();
|
int result = clazz.hashCode();
|
||||||
result = 31 * result + name.hashCode();
|
result = 31 * result + name.hashCode();
|
||||||
|
result = 31 * result + mockName.hashCode();
|
||||||
result = 31 * result + desc.hashCode();
|
result = 31 * result + desc.hashCode();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import com.alibaba.testable.agent.model.MethodInfo;
|
|||||||
import com.alibaba.testable.agent.tool.ComparableWeakRef;
|
import com.alibaba.testable.agent.tool.ComparableWeakRef;
|
||||||
import com.alibaba.testable.agent.util.ClassUtil;
|
import com.alibaba.testable.agent.util.ClassUtil;
|
||||||
import org.objectweb.asm.ClassReader;
|
import org.objectweb.asm.ClassReader;
|
||||||
import org.objectweb.asm.Type;
|
|
||||||
import org.objectweb.asm.tree.AnnotationNode;
|
import org.objectweb.asm.tree.AnnotationNode;
|
||||||
import org.objectweb.asm.tree.ClassNode;
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
import org.objectweb.asm.tree.MethodNode;
|
import org.objectweb.asm.tree.MethodNode;
|
||||||
@ -22,7 +21,6 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
|
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
|
||||||
import static com.alibaba.testable.agent.util.ClassUtil.toSlashSeparatedName;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author flin
|
* @author flin
|
||||||
@ -30,7 +28,6 @@ import static com.alibaba.testable.agent.util.ClassUtil.toSlashSeparatedName;
|
|||||||
public class TestableClassTransformer implements ClassFileTransformer {
|
public class TestableClassTransformer implements ClassFileTransformer {
|
||||||
|
|
||||||
private final Set<ComparableWeakRef<String>> loadedClassNames = ComparableWeakRef.getWeekHashSet();
|
private final Set<ComparableWeakRef<String>> loadedClassNames = ComparableWeakRef.getWeekHashSet();
|
||||||
private static final String TARGET_CLASS = "targetClass";
|
|
||||||
private static final String TARGET_METHOD = "targetMethod";
|
private static final String TARGET_METHOD = "targetMethod";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -85,47 +82,44 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkMethodAnnotation(ClassNode cn, List<MethodInfo> methodInfos, MethodNode mn) {
|
private void checkMethodAnnotation(ClassNode cn, List<MethodInfo> methodInfos, MethodNode mn) {
|
||||||
if (mn.visibleAnnotations == null) {
|
ImmutablePair<String, String> methodDescPair = extractFirstParameter(mn.desc);
|
||||||
|
if (methodDescPair == null || mn.visibleAnnotations == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||||
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_MOCK)) {
|
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_MOCK)) {
|
||||||
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
|
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
|
||||||
String targetClass = getAnnotationParameter(an, TARGET_CLASS, sourceClassName);
|
|
||||||
String targetMethod = getAnnotationParameter(an, TARGET_METHOD, mn.name);
|
String targetMethod = getAnnotationParameter(an, TARGET_METHOD, mn.name);
|
||||||
if (sourceClassName.equals(targetClass)) {
|
if (targetMethod.equals(ConstPool.CONSTRUCTOR)) {
|
||||||
// member method of the source class
|
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
|
||||||
methodInfos.add(new MethodInfo(
|
methodInfos.add(new MethodInfo(sourceClassName, targetMethod, mn.name, mn.desc));
|
||||||
toSlashSeparatedName(targetClass), targetMethod, null, mn.desc));
|
|
||||||
} else {
|
} else {
|
||||||
// member method of a common class
|
methodInfos.add(new MethodInfo(targetClass, targetMethod, mn.name, methodDescPair.right));
|
||||||
ImmutablePair<String, String> methodDescPair = extractFirstParameter(mn.desc);
|
|
||||||
if (methodDescPair != null && methodDescPair.left.equals(ClassUtil.toByteCodeClassName(targetClass))) {
|
|
||||||
methodInfos.add(new MethodInfo(
|
|
||||||
toSlashSeparatedName(targetClass), targetMethod, mn.name, methodDescPair.right));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split desc to "first parameter" and "desc of rest parameters"
|
||||||
|
* @param desc method desc
|
||||||
|
*/
|
||||||
private ImmutablePair<String, String> extractFirstParameter(String desc) {
|
private ImmutablePair<String, String> extractFirstParameter(String desc) {
|
||||||
// assume first parameter is a class
|
// assume first parameter is a class
|
||||||
int pos = desc.indexOf(";");
|
int pos = desc.indexOf(";");
|
||||||
return pos < 0 ? null : ImmutablePair.of(desc.substring(1, pos + 1), "(" + desc.substring(pos + 1));
|
return pos < 0 ? null : ImmutablePair.of(desc.substring(1, pos + 1), "(" + desc.substring(pos + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getAnnotationParameter(AnnotationNode an, String key, String defaultValue) {
|
/**
|
||||||
|
* Read value of annotation parameter
|
||||||
|
*/
|
||||||
|
private <T> T getAnnotationParameter(AnnotationNode an, String key, T defaultValue) {
|
||||||
if (an.values != null) {
|
if (an.values != null) {
|
||||||
int i = an.values.indexOf(key);
|
for (int i = 0; i < an.values.size(); i += 2) {
|
||||||
if (i % 2 == 0) {
|
if (an.values.get(i).equals(key)) {
|
||||||
Object value = an.values.get(i + 1);
|
return (T)(an.values.get(i + 1));
|
||||||
if (value instanceof Type) {
|
|
||||||
// fit for `targetClass` parameter
|
|
||||||
return ClassUtil.toSlashSeparateFullClassName(value.toString());
|
|
||||||
}
|
}
|
||||||
return value.toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
|
@ -33,34 +33,4 @@ public class CollectionUtil {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cross set of two collections
|
|
||||||
* @param collectionLeft the first collection
|
|
||||||
* @param collectionRight the second collection
|
|
||||||
*/
|
|
||||||
public static <T> Set<T> getCrossSet(Collection<T> collectionLeft, Collection<T> collectionRight) {
|
|
||||||
Set<T> crossSet = new HashSet<T>();
|
|
||||||
for (T i : collectionLeft) {
|
|
||||||
if (collectionRight.contains(i)) {
|
|
||||||
crossSet.add(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return crossSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get minus set of two collections
|
|
||||||
* @param collectionRaw original collection
|
|
||||||
* @param collectionMinus items to remove
|
|
||||||
*/
|
|
||||||
public static <T> Set<T> getMinusSet(Collection<T> collectionRaw, Collection<T> collectionMinus) {
|
|
||||||
Set<T> crossSet = new HashSet<T>();
|
|
||||||
for (T i : collectionRaw) {
|
|
||||||
if (!collectionMinus.contains(i)) {
|
|
||||||
crossSet.add(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return crossSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
package com.alibaba.testable.agent.handler;
|
||||||
|
|
||||||
|
import com.alibaba.testable.agent.model.MethodInfo;
|
||||||
|
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||||
|
import org.objectweb.asm.tree.InsnNode;
|
||||||
|
import org.objectweb.asm.tree.MethodInsnNode;
|
||||||
|
import org.objectweb.asm.tree.VarInsnNode;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.objectweb.asm.Opcodes.*;
|
||||||
|
|
||||||
|
class SourceClassHandlerTest {
|
||||||
|
|
||||||
|
private final SourceClassHandler handler = new SourceClassHandler(new ArrayList<MethodInfo>());
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_get_member_method_start() {
|
||||||
|
AbstractInsnNode[] instructions = new AbstractInsnNode[]{
|
||||||
|
new VarInsnNode(ALOAD, 2),
|
||||||
|
new VarInsnNode(ALOAD, 0),
|
||||||
|
new VarInsnNode(ALOAD, 2),
|
||||||
|
new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "trim", "()Ljava/lang/String;", false),
|
||||||
|
new InsnNode(ICONST_1),
|
||||||
|
new InsnNode(ICONST_2),
|
||||||
|
new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "substring", "(II)Ljava/lang/String;", false),
|
||||||
|
new MethodInsnNode(INVOKESPECIAL, "com/alibaba/testable/demo/DemoServiceTest", "blackBox", "(Ljava/lang/String;)Lcom/alibaba/testable/demo/BlackBox;", false),
|
||||||
|
new MethodInsnNode(INVOKEVIRTUAL, "com/alibaba/testable/demo/BlackBox", "callMe", "()Ljava/lang/String;", false),
|
||||||
|
new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "startsWith", "(Ljava/lang/String;)Z", false)
|
||||||
|
};
|
||||||
|
assertEquals(2, PrivateAccessor.invoke(handler, "getMemberMethodStart", instructions, 3));
|
||||||
|
assertEquals(2, PrivateAccessor.invoke(handler, "getMemberMethodStart", instructions, 6));
|
||||||
|
assertEquals(0, PrivateAccessor.invoke(handler, "getMemberMethodStart", instructions, 9));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -12,11 +12,6 @@ import java.lang.annotation.*;
|
|||||||
@Documented
|
@Documented
|
||||||
public @interface TestableMock {
|
public @interface TestableMock {
|
||||||
|
|
||||||
/**
|
|
||||||
* mock method of specified class instead of the class under test
|
|
||||||
*/
|
|
||||||
Class targetClass() default Object.class;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mock specified method instead of method with same name
|
* mock specified method instead of method with same name
|
||||||
*/
|
*/
|
||||||
|
@ -7,6 +7,11 @@ import com.alibaba.testable.core.util.TestableUtil;
|
|||||||
*/
|
*/
|
||||||
public class TestableTool {
|
public class TestableTool {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the constructor method
|
||||||
|
*/
|
||||||
|
public static final String CONSTRUCTOR = "<init>";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of current test case method
|
* Name of current test case method
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user