mirror of
https://github.com/alibaba/testable-mock.git
synced 2026-08-19 17:53:29 +08:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02ba7ad83c | ||
|
|
7bbdf82a41 | ||
|
|
126c7257d8 | ||
|
|
adfba4bac0 | ||
|
|
2c18ea12ed | ||
|
|
d4bce13918 | ||
|
|
b23b8b98b3 | ||
|
|
f228ed5002 | ||
|
|
2303ac188e | ||
|
|
290d0db400 | ||
|
|
77b955a5ce | ||
|
|
ab8a0b32e5 | ||
|
|
39c788cd02 | ||
|
|
7029579534 | ||
|
|
ba951ce744 | ||
|
|
2bbe2eba93 | ||
|
|
4c96f4cc0e | ||
|
|
3a2e3032df | ||
|
|
530f3295bd | ||
|
|
99e7c3bf18 | ||
|
|
ef5d21baf6 | ||
|
|
6951b98c7e | ||
|
|
369c2bd192 | ||
|
|
13de5e42ad | ||
|
|
064926e952 | ||
|
|
2f110ae4d6 | ||
|
|
7ba44ae618 | ||
|
|
66cb8f7732 | ||
|
|
1f29aff00f | ||
|
|
8d12bb42ba | ||
|
|
c1744d2254 | ||
|
|
444967f676 | ||
|
|
860fdcf072 | ||
|
|
851ae91406 | ||
|
|
580fed7dac | ||
|
|
f07ac8f21f | ||
|
|
31a0b49b7a | ||
|
|
0a1b46b352 | ||
|
|
55ec1ca5b5 | ||
|
|
2806d38c94 | ||
|
|
070aeb6559 | ||
|
|
0255ffa911 | ||
|
|
f64e0cd959 | ||
|
|
74b68d495a | ||
|
|
6c0bd7de46 | ||
|
|
593d134672 | ||
|
|
c496837861 | ||
|
|
b42ee1c9b9 | ||
|
|
079410ee0f | ||
|
|
acc3f0bdf2 | ||
|
|
6f9d535cee | ||
|
|
631f45f4c5 | ||
|
|
94127064e8 | ||
|
|
36f170bdd3 | ||
|
|
5ab1cf7a40 | ||
|
|
68075a2ead | ||
|
|
783b127bbb | ||
|
|
cd7097ded9 | ||
|
|
0f2c889c46 | ||
|
|
df4ac8e950 | ||
|
|
9e393b5ef5 | ||
|
|
bb47f5d199 | ||
|
|
2efee1feb2 | ||
|
|
3df95db607 | ||
|
|
a6b289e136 | ||
|
|
3c375713aa | ||
|
|
9a7372d9ab |
17
README.md
17
README.md
@@ -5,10 +5,25 @@
|
||||
无需初始化,不挑测试框架,甭管要换的是私有方法、静态方法、构造方法还是其他任何类的任何方法,也甭管要换的对象是怎么创建的。写好Mock定义,加个`@MockMethod`注解,一切统统搞定。
|
||||
|
||||
- 文档:https://alibaba.github.io/testable-mock/
|
||||
- 国内文档镜像:http://freyrlin.gitee.io/testable-mock/ (速度快,内容稍有延迟)
|
||||
- 国内文档镜像:http://freyrlin.gitee.io/testable-mock/
|
||||
|
||||
阅读[这里](https://mp.weixin.qq.com/s/KyU6Eu7mDkZU8FspfSqfMw)了解更多故事。
|
||||
|
||||
> 特别说明
|
||||
> 1. 如遇到"Attempt to access non-static member in mock method"错误,参见[常见问题](https://alibaba.github.io/testable-mock/#/zh-cn/doc/frequently-asked-questions)第8条
|
||||
> 2. 如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将在24小时内回复并处理
|
||||
|
||||
-----
|
||||
|
||||
## 版本计划
|
||||
|
||||
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
|
||||
|
||||
- `0.4` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
|
||||
- `0.5` 实现以"独立Mock类"为单元的Mock方法复用机制,让测试类之间可以方便复用相同的Mock方法
|
||||
- `0.6` 实现将Mock方法的默认生效范围缩小至当前被测类,避免Mock方法在测试类之间相互影响
|
||||
- `1.0` 功能稳定,一个崭新的开始
|
||||
|
||||
## 目录结构
|
||||
|
||||
```bash
|
||||
|
||||
@@ -7,6 +7,15 @@ Write a mock method, add an `@MockMethod` annotation, everything is done.
|
||||
|
||||
Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
|
||||
|
||||
## Loadmap
|
||||
|
||||
`TestableMock` is still under heavy development, the following version plans may be adjusted during the iteration
|
||||
|
||||
- `v0.4` it's the current version, refer to the [issue](https://github.com/alibaba/testable-mock/issues) list for the work in progress
|
||||
- `v0.5` implementation mock class inherit mechanism, so that the same mock method can be reused between test classes conveniently
|
||||
- `v0.6` narrows the default effective scope of mock methods to the class under test, to avoid interaction of mocking cross classes
|
||||
- `v1.0` all functions are stable, a brand-new start
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```bash
|
||||
|
||||
@@ -7,16 +7,21 @@ version = '1.0.0-SNAPSHOT'
|
||||
sourceCompatibility = '8'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.5')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.5')
|
||||
testImplementation('junit:junit:4.13.1')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.11')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.11')
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
test {
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
useJUnitPlatform()
|
||||
useJUnit()
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
<properties>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.4.5</testable.version>
|
||||
<junit.version>4.13.1</junit.version>
|
||||
<testable.version>0.4.11</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -23,8 +23,8 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
@@ -33,14 +33,14 @@
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<configuration>
|
||||
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<configuration>
|
||||
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- <plugin>-->
|
||||
<!-- <groupId>com.alibaba.testable</groupId>-->
|
||||
<!-- <artifactId>testable-maven-plugin</artifactId>-->
|
||||
|
||||
@@ -22,16 +22,30 @@ public class DemoPrivateAccess {
|
||||
public final Double pi = 3.14;
|
||||
|
||||
/**
|
||||
* private static method
|
||||
* private static method without arguments
|
||||
*/
|
||||
private static String privateStaticFunc(String str, int i) {
|
||||
return str + " + " + i;
|
||||
private static String privateStaticFunc() {
|
||||
return "static";
|
||||
}
|
||||
|
||||
/**
|
||||
* private member method
|
||||
* private static method with arguments
|
||||
*/
|
||||
private String privateFunc(List<String> list, String str, int i) {
|
||||
private static String privateStaticFuncWithArgs(String str, int i) {
|
||||
return (str == null ? "null" : str) + " + " + i;
|
||||
}
|
||||
|
||||
/**
|
||||
* private member method without arguments
|
||||
*/
|
||||
private String privateFunc() {
|
||||
return "member";
|
||||
}
|
||||
|
||||
/**
|
||||
* private member method with arguments
|
||||
*/
|
||||
private String privateFuncWithArgs(List<String> list, String str, int i) {
|
||||
return list.stream().reduce((a, s) -> a + s).orElse("") + " + " + str + " + " + i;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@ import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.model.Box;
|
||||
import com.alibaba.testable.demo.model.Color;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.Test;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
|
||||
*/
|
||||
class DemoInheritTest {
|
||||
public class DemoInheritTest {
|
||||
|
||||
private DemoInherit demoInherit = new DemoInherit();
|
||||
|
||||
@@ -49,42 +49,42 @@ class DemoInheritTest {
|
||||
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_sub_object_method_by_parent_object() {
|
||||
public void should_able_to_mock_call_sub_object_method_by_parent_object() {
|
||||
BlackBox box = (BlackBox)demoInherit.putIntoBox();
|
||||
verify("put_into_box").withTimes(1);
|
||||
assertEquals("put_data_into_box", box.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_sub_object_method_by_sub_object() {
|
||||
public void should_able_to_mock_call_sub_object_method_by_sub_object() {
|
||||
BlackBox box = demoInherit.putIntoBlackBox();
|
||||
verify("put_into_blackbox").withTimes(1);
|
||||
assertEquals("put_data_into_blackbox", box.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_parent_object_method_by_parent_object() {
|
||||
public void should_able_to_mock_call_parent_object_method_by_parent_object() {
|
||||
String content = demoInherit.getFromBox();
|
||||
verify("get_from_box").withTimes(1);
|
||||
assertEquals("get_from_box", content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_parent_object_method_by_sub_object() {
|
||||
public void should_able_to_mock_call_parent_object_method_by_sub_object() {
|
||||
String content = demoInherit.getFromBlackBox();
|
||||
verify("get_from_blackbox").withTimes(1);
|
||||
assertEquals("get_from_blackbox", content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_interface_method_by_interface_object() {
|
||||
public void should_able_to_mock_call_interface_method_by_interface_object() {
|
||||
String color = demoInherit.getColorViaColor();
|
||||
verify("get_color_from_color").withTimes(1);
|
||||
assertEquals("color_from_color", color);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_interface_method_by_sub_class_object() {
|
||||
public void should_able_to_mock_call_interface_method_by_sub_class_object() {
|
||||
String color = demoInherit.getColorViaBox();
|
||||
verify("get_color_from_blackbox").withTimes(1);
|
||||
assertEquals("color_from_blackbox", color);
|
||||
|
||||
@@ -3,17 +3,17 @@ package com.alibaba.testable.demo;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.core.error.VerifyFailedError;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.Test;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeMatcher.*;
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* 演示Mock方法调用校验器
|
||||
* Demonstrate mock method invocation verifier
|
||||
*/
|
||||
class DemoMatcherTest {
|
||||
public class DemoMatcherTest {
|
||||
|
||||
private DemoMatcher demoMatcher = new DemoMatcher();
|
||||
|
||||
@@ -28,7 +28,7 @@ class DemoMatcherTest {
|
||||
|
||||
|
||||
@Test
|
||||
void should_match_no_argument() {
|
||||
public void should_match_no_argument() {
|
||||
demoMatcher.callMethodWithoutArgument();
|
||||
verify("methodWithoutArgument").withTimes(1);
|
||||
demoMatcher.callMethodWithoutArgument();
|
||||
@@ -36,7 +36,7 @@ class DemoMatcherTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_match_number_arguments() {
|
||||
public void should_match_number_arguments() {
|
||||
demoMatcher.callMethodWithNumberArguments();
|
||||
verify("methodWithArguments").without(anyString(), 2);
|
||||
verify("methodWithArguments").withInOrder(anyInt(), 2);
|
||||
@@ -49,7 +49,7 @@ class DemoMatcherTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_match_string_arguments() {
|
||||
public void should_match_string_arguments() {
|
||||
demoMatcher.callMethodWithStringArgument();
|
||||
verify("methodWithArguments").with(startsWith("he"), endsWith("ld"));
|
||||
verify("methodWithArguments").with(contains("stab"), matches("m.[cd]k"));
|
||||
@@ -57,7 +57,7 @@ class DemoMatcherTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_match_object_arguments() {
|
||||
public void should_match_object_arguments() {
|
||||
demoMatcher.callMethodWithObjectArgument();
|
||||
verify("methodWithArguments").withInOrder(any(BlackBox.class), any(BlackBox.class));
|
||||
verify("methodWithArguments").withInOrder(nullable(BlackBox.class), nullable(BlackBox.class));
|
||||
@@ -65,7 +65,7 @@ class DemoMatcherTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_match_with_times() {
|
||||
public void should_match_with_times() {
|
||||
demoMatcher.callMethodWithNumberArguments();
|
||||
verify("methodWithArguments").with(anyNumber(), any()).times(3);
|
||||
|
||||
|
||||
@@ -3,20 +3,20 @@ package com.alibaba.testable.demo;
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.TEST_CASE;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示基本的Mock功能
|
||||
* Demonstrate basic mock functionality
|
||||
*/
|
||||
class DemoMockTest {
|
||||
public class DemoMockTest {
|
||||
|
||||
private DemoMock demoMock = new DemoMock();
|
||||
|
||||
@@ -25,39 +25,39 @@ class DemoMockTest {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private String innerFunc(DemoMock self, String text) {
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private String staticFunc(DemoMock self) {
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String staticFunc() {
|
||||
return "_MOCK_TAIL";
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private String trim(String self) {
|
||||
@MockMethod(targetClass = String.class)
|
||||
private String trim() {
|
||||
return "trim_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "substring")
|
||||
private String sub(String self, int i, int j) {
|
||||
@MockMethod(targetClass = String.class, targetMethod = "substring")
|
||||
private String sub(int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private boolean startsWith(String self, String s) {
|
||||
@MockMethod(targetClass = String.class)
|
||||
private boolean startsWith(String s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private BlackBox secretBox(BlackBox ignore) {
|
||||
@MockMethod(targetClass = BlackBox.class)
|
||||
private BlackBox secretBox() {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private String callFromDifferentMethod(DemoMock self) {
|
||||
if (TEST_CASE.equals("should_able_to_get_test_case_name")) {
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String callFromDifferentMethod() {
|
||||
if ("special_case".equals(MOCK_CONTEXT.get("case"))) {
|
||||
return "mock_special";
|
||||
}
|
||||
switch (SOURCE_METHOD) {
|
||||
@@ -68,19 +68,20 @@ class DemoMockTest {
|
||||
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_new_object() {
|
||||
public void should_able_to_mock_new_object() {
|
||||
assertEquals("mock_something", demoMock.newFunc());
|
||||
verify("createBlackBox").with("something");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
public void should_able_to_mock_member_method() throws Exception {
|
||||
assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoMock.outerFunc("hello"));
|
||||
verify("innerFunc").with("hello");
|
||||
verify("staticFunc").with();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_common_method() {
|
||||
public void should_able_to_mock_common_method() {
|
||||
assertEquals("trim_string__sub_string__false", demoMock.commonFunc());
|
||||
verify("trim").withTimes(1);
|
||||
verify("sub").withTimes(1);
|
||||
@@ -88,13 +89,13 @@ class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_static_method() {
|
||||
public void should_able_to_mock_static_method() {
|
||||
assertEquals("not_secret_box", demoMock.getBox().get());
|
||||
verify("secretBox").withTimes(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_source_method_name() throws Exception {
|
||||
public void should_able_to_get_source_method_name() throws Exception {
|
||||
// synchronous
|
||||
assertEquals("mock_one_mock_others", demoMock.callerOne() + "_" + demoMock.callerTwo());
|
||||
// asynchronous
|
||||
@@ -104,12 +105,14 @@ class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_test_case_name() throws Exception {
|
||||
public void should_able_to_get_test_case_name() throws Exception {
|
||||
MOCK_CONTEXT.put("case", "special_case");
|
||||
// synchronous
|
||||
assertEquals("mock_special", demoMock.callerOne());
|
||||
// asynchronous
|
||||
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoMock.callerOne()).get());
|
||||
verify("callFromDifferentMethod").withTimes(2);
|
||||
MOCK_CONTEXT.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,57 +2,76 @@ package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
*/
|
||||
@EnablePrivateAccess
|
||||
class DemoPrivateAccessTest {
|
||||
public class DemoPrivateAccessTest {
|
||||
|
||||
private DemoPrivateAccess demoPrivateAccess = new DemoPrivateAccess();
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_method() {
|
||||
public void should_able_to_access_private_method() {
|
||||
List<String> list = new ArrayList<String>() {{ add("a"); add("b"); add("c"); }};
|
||||
assertEquals("abc + hello + 1", demoPrivateAccess.privateFunc(list, "hello", 1));
|
||||
assertEquals("abc + hello + 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", list, "hello", 1));
|
||||
assertEquals("member", demoPrivateAccess.privateFunc());
|
||||
assertEquals("member", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc"));
|
||||
assertEquals("abc + hello + 1", demoPrivateAccess.privateFuncWithArgs(list, "hello", 1));
|
||||
assertEquals("abc + hello + 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFuncWithArgs", list, "hello", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_field() {
|
||||
public void should_able_to_access_private_field() {
|
||||
demoPrivateAccess.count = 2;
|
||||
assertEquals(new Integer(2), demoPrivateAccess.count);
|
||||
assertEquals(Integer.valueOf(2), demoPrivateAccess.count);
|
||||
|
||||
PrivateAccessor.set(demoPrivateAccess, "count", 3);
|
||||
assertEquals(new Integer(3), PrivateAccessor.get(demoPrivateAccess, "count"));
|
||||
assertEquals(Integer.valueOf(3), PrivateAccessor.get(demoPrivateAccess, "count"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_static_method() {
|
||||
assertEquals("hello + 1", DemoPrivateAccess.privateStaticFunc("hello", 1));
|
||||
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFunc", "hello", 1));
|
||||
public void should_able_to_access_private_static_method() {
|
||||
assertEquals("static", DemoPrivateAccess.privateStaticFunc());
|
||||
assertEquals("static", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFunc"));
|
||||
assertEquals("hello + 1", DemoPrivateAccess.privateStaticFuncWithArgs("hello", 1));
|
||||
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", "hello", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_static_field() {
|
||||
public void should_able_to_access_private_static_field() {
|
||||
DemoPrivateAccess.staticCount = 2;
|
||||
assertEquals(new Integer(2), DemoPrivateAccess.staticCount);
|
||||
assertEquals(Integer.valueOf(2), DemoPrivateAccess.staticCount);
|
||||
|
||||
PrivateAccessor.setStatic(DemoPrivateAccess.class, "staticCount", 3);
|
||||
assertEquals(new Integer(3), PrivateAccessor.getStatic(DemoPrivateAccess.class, "staticCount"));
|
||||
assertEquals(Integer.valueOf(3), PrivateAccessor.getStatic(DemoPrivateAccess.class, "staticCount"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_update_final_field() {
|
||||
public void should_able_to_update_final_field() {
|
||||
demoPrivateAccess.pi = 4.13;
|
||||
assertEquals(4.13, demoPrivateAccess.pi);
|
||||
assertEquals(Double.valueOf(4.13), demoPrivateAccess.pi);
|
||||
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", 3.14);
|
||||
assertEquals(Double.valueOf(3.14), PrivateAccessor.get(demoPrivateAccess, "pi"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_able_to_use_null_parameter() {
|
||||
demoPrivateAccess.pi = null;
|
||||
assertNull(demoPrivateAccess.pi);
|
||||
assertEquals("null + 1", DemoPrivateAccess.privateStaticFuncWithArgs(null, 1));
|
||||
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", null);
|
||||
assertNull(PrivateAccessor.get(demoPrivateAccess, "pi"));
|
||||
assertEquals("null + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", null, 1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,17 +2,17 @@ package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示模板方法的Mock场景
|
||||
* Demonstrate scenario of mocking template method
|
||||
*/
|
||||
class DemoTemplateTest {
|
||||
public class DemoTemplateTest {
|
||||
|
||||
private DemoTemplate demoTemplate = new DemoTemplate();
|
||||
|
||||
@@ -70,19 +70,19 @@ class DemoTemplateTest {
|
||||
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_single_template_method() {
|
||||
public void should_able_to_mock_single_template_method() {
|
||||
String res = demoTemplate.singleTemplateMethod();
|
||||
assertEquals("demo_mock_list", res);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_double_template_method() {
|
||||
public void should_able_to_mock_double_template_method() {
|
||||
String res = demoTemplate.doubleTemplateMethod();
|
||||
assertEquals("testable_mock_map", res);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_new_template_method() {
|
||||
public void should_able_to_mock_new_template_method() {
|
||||
Set<?> res = demoTemplate.newTemplateMethod();
|
||||
assertEquals(2, res.size());
|
||||
Iterator<?> iterator = res.stream().iterator();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "1.4.10"
|
||||
kotlin("jvm") version "1.4.10"
|
||||
}
|
||||
|
||||
group = "com.alibaba.testable"
|
||||
@@ -9,25 +9,25 @@ version = "1.0.0-SNAPSHOT"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
|
||||
testImplementation("com.alibaba.testable:testable-all:0.4.5")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.5")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
|
||||
testImplementation("com.alibaba.testable:testable-all:0.4.11")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.11")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
jvmArgs("-javaagent:${classpath.find { it.name.contains("testable-agent") }!!.absolutePath}")
|
||||
useJUnitPlatform()
|
||||
jvmArgs("-javaagent:${classpath.find { it.name.contains("testable-agent") }!!.absolutePath}")
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
@@ -1,119 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>kotlin-demo</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>kotlin-demo</name>
|
||||
<description>Demo project for TestableMock</description>
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>kotlin-demo</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>kotlin-demo</name>
|
||||
<description>Demo project for TestableMock</description>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<kotlin.version>1.3.72</kotlin.version>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.4.5</testable.version>
|
||||
</properties>
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<kotlin.version>1.3.72</kotlin.version>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.4.11</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<!-- <configuration>-->
|
||||
<!-- <argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>-->
|
||||
<!-- </configuration>-->
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare</id>
|
||||
<goals>
|
||||
<goal>prepare</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<dataFile>target/jacoco.exec</dataFile>
|
||||
<outputDirectory>target/jacoco-ut</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<build>
|
||||
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<!-- <configuration>-->
|
||||
<!-- <argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>-->
|
||||
<!-- </configuration>-->
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare</id>
|
||||
<goals>
|
||||
<goal>prepare</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<dataFile>target/jacoco.exec</dataFile>
|
||||
<outputDirectory>target/jacoco-ut</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -4,6 +4,10 @@ import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.Box
|
||||
import com.alibaba.testable.demo.model.Color
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
|
||||
*/
|
||||
class DemoInherit {
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,8 @@ import com.alibaba.testable.demo.model.BlackBox
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
* 演示Mock方法调用校验器
|
||||
* Demonstrate mock method invocation verifier
|
||||
*/
|
||||
class DemoMatcher {
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,10 @@ import com.alibaba.testable.demo.model.ColorBox
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
/**
|
||||
* 演示基本的Mock功能
|
||||
* Demonstrate basic mock functionality
|
||||
*/
|
||||
class DemoMock {
|
||||
|
||||
/**
|
||||
@@ -18,7 +22,7 @@ class DemoMock {
|
||||
* method with member method invoke
|
||||
*/
|
||||
fun outerFunc(s: String): String {
|
||||
return "{ \"res\": \"" + innerFunc(s) + "\"}"
|
||||
return "{ \"res\": \"" + innerFunc(s) + staticFunc() + "\"}"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,4 +56,14 @@ class DemoMock {
|
||||
}
|
||||
|
||||
private fun callFromDifferentMethod() = "realOne"
|
||||
|
||||
companion object {
|
||||
private fun staticFunc(): String {
|
||||
return "_STATIC_TAIL"
|
||||
}
|
||||
|
||||
// fun callStaticFunc(): String {
|
||||
// return "CALL${staticFunc()}"
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
*/
|
||||
class DemoPrivateAccess {
|
||||
|
||||
/**
|
||||
@@ -15,8 +19,8 @@ class DemoPrivateAccess {
|
||||
/**
|
||||
* private member method
|
||||
*/
|
||||
private fun privateFunc(s: String, i: Int): String {
|
||||
return "$s - $i"
|
||||
private fun privateFunc(list: List<String>, str: String, i: Int): String {
|
||||
return list.reduce { a: String, s: String -> a + s } + " + " + "$str + $i"
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -29,15 +33,15 @@ class DemoPrivateAccess {
|
||||
/**
|
||||
* private static method
|
||||
*/
|
||||
private fun privateStaticFunc(s: String, i: Int): String {
|
||||
return "$s + $i"
|
||||
private fun privateStaticFunc(str: String, i: Int): String {
|
||||
return "$str + $i"
|
||||
}
|
||||
|
||||
/**
|
||||
* private jvm static method
|
||||
*/
|
||||
@JvmStatic private fun privateJvmStaticFunc(s: String, i: Int): String {
|
||||
return "$s * $i"
|
||||
@JvmStatic private fun privateJvmStaticFunc(list: List<String>, str: String, i: Int): String {
|
||||
return list.reduce { a: String, s: String -> a + s } + " * " + "$str * $i"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import java.util.ArrayList
|
||||
import java.util.HashMap
|
||||
import java.util.HashSet
|
||||
|
||||
/**
|
||||
* 演示模板方法的Mock场景
|
||||
* Demonstrate scenario of mocking template method
|
||||
*/
|
||||
class DemoTemplate {
|
||||
private fun <T> getList(value: T): List<T> {
|
||||
val l: MutableList<T> = ArrayList()
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.alibaba.testable.core.annotation.MockConstructor
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD
|
||||
import com.alibaba.testable.core.tool.TestableTool.TEST_CASE
|
||||
import com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.ColorBox
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -22,31 +22,36 @@ internal class DemoMockTest {
|
||||
@MockConstructor
|
||||
private fun createBlackBox(text: String) = BlackBox("mock_$text")
|
||||
|
||||
@MockMethod
|
||||
private fun innerFunc(self: DemoMock, text: String) = "mock_$text"
|
||||
@MockMethod(targetClass = DemoMock::class)
|
||||
private fun innerFunc(text: String) = "mock_$text"
|
||||
|
||||
@MockMethod
|
||||
private fun trim(self: BlackBox) = "trim_string"
|
||||
@MockMethod(targetClass = DemoMock::class)
|
||||
private fun staticFunc(): String {
|
||||
return "_MOCK_TAIL";
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "substring")
|
||||
private fun sub(self: BlackBox, i: Int, j: Int) = "sub_string"
|
||||
@MockMethod(targetClass = BlackBox::class)
|
||||
private fun trim() = "trim_string"
|
||||
|
||||
@MockMethod
|
||||
private fun startsWith(self: BlackBox, s: String) = false
|
||||
@MockMethod(targetClass = BlackBox::class, targetMethod = "substring")
|
||||
private fun sub(i: Int, j: Int) = "sub_string"
|
||||
|
||||
@MockMethod
|
||||
private fun secretBox(ignore: BlackBox): BlackBox {
|
||||
@MockMethod(targetClass = BlackBox::class)
|
||||
private fun startsWith(s: String) = false
|
||||
|
||||
@MockMethod(targetClass = BlackBox::class)
|
||||
private fun secretBox(): BlackBox {
|
||||
return BlackBox("not_secret_box")
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private fun createBox(ignore: ColorBox, color: String, box: BlackBox): BlackBox {
|
||||
@MockMethod(targetClass = ColorBox::class)
|
||||
private fun createBox(color: String, box: BlackBox): BlackBox {
|
||||
return BlackBox("White_${box.get()}")
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private fun callFromDifferentMethod(self: DemoMock): String {
|
||||
return if (TEST_CASE == "should_able_to_get_test_case_name") {
|
||||
@MockMethod(targetClass = DemoMock::class)
|
||||
private fun callFromDifferentMethod(): String {
|
||||
return if (MOCK_CONTEXT["case"] == "special_case") {
|
||||
"mock_special"
|
||||
} else {
|
||||
when (SOURCE_METHOD) {
|
||||
@@ -65,10 +70,17 @@ internal class DemoMockTest {
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_member_method() {
|
||||
assertEquals("{ \"res\": \"mock_hello\"}", demoMock.outerFunc("hello"))
|
||||
assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoMock.outerFunc("hello"))
|
||||
verify("innerFunc").with("hello")
|
||||
verify("staticFunc").with()
|
||||
}
|
||||
|
||||
// @Test
|
||||
// fun should_able_to_mock_method_in_companion_object() {
|
||||
// assertEquals("CALL_MOCK_TAIL", DemoMock.callStaticFunc())
|
||||
// verify("staticFunc").with()
|
||||
// }
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_common_method() {
|
||||
assertEquals("trim_string__sub_string__false", demoMock.commonFunc())
|
||||
@@ -97,6 +109,7 @@ internal class DemoMockTest {
|
||||
|
||||
@Test
|
||||
fun should_able_to_get_test_case_name() {
|
||||
MOCK_CONTEXT["case"] = "special_case"
|
||||
// synchronous
|
||||
assertEquals("mock_special", demoMock.callerOne())
|
||||
// asynchronous
|
||||
@@ -104,5 +117,6 @@ internal class DemoMockTest {
|
||||
demoMock.callerOne()
|
||||
}.get())
|
||||
verify("callFromDifferentMethod").withTimes(2)
|
||||
MOCK_CONTEXT.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ internal class DemoPrivateAccessTest {
|
||||
|
||||
@Test
|
||||
fun should_able_to_access_private_method() {
|
||||
assertEquals("hello - 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", "hello", 1))
|
||||
val list = listOf("a", "b", "c");
|
||||
assertEquals("abc + hello + 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", list, "hello", 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -25,8 +26,9 @@ internal class DemoPrivateAccessTest {
|
||||
|
||||
@Test
|
||||
fun should_able_to_access_private_static_method() {
|
||||
val list = listOf("a", "b", "c");
|
||||
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateStaticFunc", "hello", 1))
|
||||
assertEquals("hello * 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateJvmStaticFunc", "hello", 1))
|
||||
assertEquals("abc * hello * 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateJvmStaticFunc", list, "hello", 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -7,8 +7,8 @@ import org.junit.jupiter.api.Test
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 演示模板方法可以被Mock
|
||||
* Demonstrate template method can be mocked
|
||||
* 演示模板方法的Mock场景
|
||||
* Demonstrate scenario of mocking template method
|
||||
*/
|
||||
internal class DemoTemplateTest {
|
||||
|
||||
|
||||
9
docs/en-us/doc/about-us.md
Normal file
9
docs/en-us/doc/about-us.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## About Us
|
||||
|
||||
The core development team of `TestableMock` comes from **Alibaba Cloud · Cloud R&D Department** (formerly known as **Alibaba Group R&D Efficiency Department**). We not only internally support Alibaba Group's end-to-end project code, testing, and release process of thousands of BU-level product lines, but also the product R&D team of Alibaba Cloud enterprise-level R&D collaboration platform [云效](https://www.aliyun.com/product/yunxiao). As has been deeply involved in the field of developer tools, we are committed to improving the production experience of developers and building a digital R&D ecosystem for enterprises.
|
||||
|
||||
The function of this project is inspired by our summary of the pain points that Java developers often encounter in daily unit testing ([external dependence on Mock is cumbersome](en-us/doc/use-mock.md), [private method is difficult to test](en-us/doc/private-accessor.md), [void type method is difficult to test](en-us/doc/test-void-method.md), and [complex parameters are difficult to construct](en-us/doc/parameter-constructor.md)). Its internal name is `Testable`, and was renamed `TestableMock` for SEO reason when it was open sourced, thus the lightweight mock function is highlighted. Don't be fooled by its name, `TestableMock` is more than just a mocking tool.
|
||||
|
||||
From the incubation of hackathon, to internal open source, and then to external open source, `TestableMock` has accumulated a group of developer users in the internal and external communities. At the same time, we are constantly improving the functional richness and stability of `TestableMock` itself. According to the current version release process, we usually update the internal beta version containing the `SNAPSHOT` mark internally, and release the official version to the Maven central repository (except for the severe bug fix version) after at least one day of stable used to ensure the update will bring more convenience and less burden to everyone.
|
||||
|
||||
Finally, we are relatively optimistic about the discussion in the community about "whether too powerful test assistance is equivalent to condoning code corruption". Just as the birth of `PowerMock` did not really spawn more bad projects, but actually solved many test problems left over by the Java language. Rather than use "code tricks" to test functions indirectly, it could be worth to just remove the constraints, and let the unit test break in all its fury! 🤠
|
||||
@@ -35,14 +35,40 @@ The `String` type in Kotlin language is actually `kotlin.String` instead of `jav
|
||||
|
||||
In actual scenarios, there are very few scenarios where methods in the `String` class need to be mocked, so `TestableMock` has not dealt with this situation specifically.
|
||||
|
||||
#### 6. When trigger a single test case in IntelliJ IDE 2020.3, why class with `@EnablePrivateAccess` annotation report still private member access errors?
|
||||
#### 6. Will the mock definition still be valid when it is **indirectly called** from other test classes?
|
||||
|
||||
From version `2020.2.2`, the compiler provided by IntelliJ handle the annotation processor of the `JSR-269` specification in an incompatible way of maven. You can turn on the "Delegate IDE build/run actions to maven" option in "Build Tools > Maven > Runner" of IntelliJ system configuration:
|
||||
Equally effective, the scope of mock is the entire test runtime process.
|
||||
|
||||

|
||||
For example, some private methods and external invocation in the `Aaa` class are mocked, mock method are defined in the test class `AaaTest`. When testing the `Bbb` class in another test class `BbbTest`, some mocked methods are invoked indirectly in the `Aaa` class, the actual call will also be routed to the mock method defined in the `AaaTest` class.
|
||||
|
||||
#### 7. Can `TestableMock` be used for testing Android projects?
|
||||
|
||||
It can be used in combination with [Roboelectric](https://github.com/robolectric/robolectric) testing framework.
|
||||
|
||||
The `Dalvik` and `ART` virtual machines of the Android system use a bytecode system different from the standard JVM, which will affect the normal functionality of `TestableMock`. The `Roboelectric` framework can run Android unit tests on a standard JVM virtual machine, which is much faster than running unit tests through the Android virtual machine. Recently, most Android App unit tests are written with the `Roboelectric` framework.
|
||||
|
||||
#### 8. Meet "Attempt to access non-static member in mock method" error during mocking?
|
||||
|
||||
The current design of `TestableMock` does not allow access to the non-`static` members of the test class in the mock method (because the mock method itself will be dynamically modified to the `static` type during runtime). However, some Java statements include building blocks (like `new ArrayList<String>() {{ append("data"); }}`), lambda expression (like `list.stream().map(i -> i. get)`) and so on, will generate additional member method invocations during compilation, causing mock method execution report above error.
|
||||
|
||||
The simplest solution is to declare the mock method itself as a `static` type (so that dynamically generated invocation will also be `static` to avoid the errors), for example, the original method is defined as:
|
||||
|
||||
```java
|
||||
@MockMethod
|
||||
private int getXxx(Demo self) {}
|
||||
```
|
||||
|
||||
Modify it to:
|
||||
|
||||
```java
|
||||
@MockMethod
|
||||
private static int getXxx(Demo self) {}
|
||||
```
|
||||
|
||||
In the next major iteration (**i.e. `v0.5`**), the mock implementation mechanism will be modified while maintaining the current mock experience. Then, it will be no longer necessary to modify the mock method to a static method, and completely solving this problem.
|
||||
|
||||
#### 9. Meet "Command Line is too Long. Shorten command line for ..." error when triggering test in IntelliJ IDE?
|
||||
|
||||
This problem is caused by the system `Class Path` content is too long, and has nothing to do with `TestableMock`. However, it should be noted that IntelliJ provides two auxiliary solutions: `JAR manifest` and `classpath file`. If `TestableMock` is used in the test, please select `JAR manifest`.
|
||||
|
||||

|
||||
|
||||
6
docs/en-us/doc/parameter-constructor.md
Normal file
6
docs/en-us/doc/parameter-constructor.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Parameter constructor
|
||||
---
|
||||
|
||||
No matter how intricate the parameter structure required by the method under test is, even there is no suitable construction method, or even there are private internal class objects... Call `TestableMock`, the parameter object will be handed to you immediately~
|
||||
|
||||
This feature is planned to be released in the `0.5.x` version.
|
||||
@@ -1,5 +1,33 @@
|
||||
# Release Note
|
||||
|
||||
## 0.4.11
|
||||
- support accessing private members of class under test in different package path
|
||||
- validate the number of private method parameters accessed by `PrivateAccessor`
|
||||
- fix a bug which may cause errors when the mock method contains array parameters
|
||||
- fix an issue which cause some private members not be found in the IntelliJ build
|
||||
|
||||
## 0.4.10
|
||||
- fix an issue of using mock in lambda expression
|
||||
- fix the NullPointerException when invoke private method with parameter value `null`
|
||||
|
||||
## 0.4.9
|
||||
- fix an issue cause by improperly bytecode processing while using `targetClass` parameter
|
||||
- auto validate access target of `PrivateAccessor`, improve resistance to code refactoring
|
||||
|
||||
## 0.4.8
|
||||
- fix an issue of private method invocation failed in assignment statements
|
||||
- support using mock for testing with `SpringRunner`
|
||||
- support `targetClass` parameter in `@MockMethod` annotation
|
||||
|
||||
## 0.4.7
|
||||
- fix incorrect stack size caused by `MOCK_CONTEXT` variable initialization
|
||||
- fix incorrect gradle build path of under Windows operating system
|
||||
|
||||
## 0.4.6
|
||||
- fix an issue of `IINC` bytecode processing
|
||||
- support `TestableTool.MOCK_CONTEXT` variable to inject extra parameters to mock context
|
||||
- use `TestableTool.TEST_CASE` variable to distinguish test case is no longer recommended
|
||||
|
||||
## 0.4.5
|
||||
- fix private access compile error in intelliJ 2020.3+ environment
|
||||
- change javaagent initialization logic to avoid NPE in cross-layer test scenario
|
||||
@@ -44,7 +72,7 @@
|
||||
## v0.2.1
|
||||
- support mock static method
|
||||
- support mock kotlin companion object method
|
||||
- support mock invoke by interface / base class object
|
||||
- support mock invoke by an interface / base class object
|
||||
|
||||
## v0.2.0
|
||||
- use `TestableTool` class to expose test context and verify mock invoke
|
||||
|
||||
@@ -3,9 +3,10 @@ Use TestableMock
|
||||
|
||||
`TestableMock` is an assist tool for Java unit testing based on source code and bytecode enhancement, including the following functions:
|
||||
|
||||
- [Quickly mock arbitrary call](en-us/doc/use-mock.md): quickly replace any method invocation in the class under test with a mock method, solve the cumbersome use of traditional mock tools problem
|
||||
- [Access private members of the class under test](en-us/doc/private-accessor.md): enable unit tests directly invoke or access private members of the class under test, solve the problems of private member initialization and private method testing
|
||||
- [Quick mock arbitrary call](en-us/doc/use-mock.md): quickly replace any method invocation in the class under test with a mock method, solve the cumbersome use of traditional mock tools problem
|
||||
- [Auxiliary test void method](en-us/doc/test-void-method.md): use the mock validator to check the internal logic of method, solve the problem that unit testing is difficult to implement to the method with no return value
|
||||
- [Quickly construct complicated parameter object](en-us/doc/parameter-constructor.md):generate arbitrarily nested object instances, simplify their internal member assignment methods, solve the problem of long initialization codes for method parameters
|
||||
|
||||
## Use in Maven project
|
||||
|
||||
@@ -15,7 +16,7 @@ It is recommended to add a `property` field that identifies the TestableMock ver
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.4.5</testable.version>
|
||||
<testable.version>0.4.11</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -62,12 +63,12 @@ Add dependence of `TestableMock` in `build.gradle` file:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.5')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.5')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.11')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.11')
|
||||
}
|
||||
```
|
||||
|
||||
Then add javaagent to `test` configuration:
|
||||
Then add `javaagent` to "test" configuration:
|
||||
|
||||
```groovy
|
||||
test {
|
||||
@@ -76,3 +77,19 @@ test {
|
||||
```
|
||||
|
||||
See the [build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/build.gradle) file of project `java-demo` and the [build.gradle.kts](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/build.gradle.kts) file of project `kotlin-demo`.
|
||||
|
||||
> For Android project tested with `Robolectric` framework, please use the same method to add `TestableMock` dependency as above, and add `javaagent` configuration as follows:
|
||||
>
|
||||
> ```groovy
|
||||
> android {
|
||||
> testOptions {
|
||||
> unitTests {
|
||||
> all {
|
||||
> jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> See [issue-43](https://github.com/alibaba/testable-mock/issues/43) for a complete example.
|
||||
|
||||
@@ -38,3 +38,9 @@ The output log example is as follows:
|
||||
```
|
||||
|
||||
The log shows all the mocked invocation and corresponding code line numbers in the class under test.
|
||||
|
||||
- Self troubleshooting:
|
||||
|
||||
- If there is no output, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies
|
||||
- If only the first line of `Handling test class` is output, please check whether the test class is in the same package of the class under test, and the name is "<ClassUnderTest>+Test" (required for `0.4.x` version)
|
||||
- If `Handling source class` and `Handling method xxx` are output, but there is no mock replacement happen at the expected code line, please check whether the mock method definition matches the target method
|
||||
|
||||
18
docs/en-us/doc/use-in-ide.md
Normal file
18
docs/en-us/doc/use-in-ide.md
Normal file
@@ -0,0 +1,18 @@
|
||||
Use TestableMock In IDE
|
||||
---
|
||||
|
||||
## Use IntelliJ IDE
|
||||
|
||||
IntelliJ IDE supports the `JSR-269` annotation processor and the `maven-surefire-plugin` arguments very well (both are techniques back the `TestableMock`). Usually you don't need any special configuration to make everything work, it's all out of the box.
|
||||
|
||||
## Use Eclipse IDE
|
||||
|
||||
Since the built-in compilation feature of `Eclipse` is based on a self-made compiler, it is not compatible with the standard `javac` compilation process, which will cause the `@EnablePrivateAccess` annotation to be invalid when running test cases in the IDE. However, the function of accessing the private members of the class under test through the `PrivateAccessor` tool class will not be affected by differences in the compiler.
|
||||
|
||||
If the `@EnablePrivateAccess` annotation is used in the project, you can use `mvn test -Dtest=<TestClassName>` and `mvn test -Dtest=<TestClassName>#<TestCaseName>` in the command line of `Eclipse` to run a single test class or test case.
|
||||
|
||||
At the same time, because the built-in unit test executor of `Eclipse` completely ignores the configuration of the `pom.xml` file, additional configuration is required to use the Mock function.
|
||||
|
||||
Take the use of `JUnit` as an example. You need to pull down from the small triangle next to the run button on the IDE toolbar, select "Run Configurations...", select the task to run the unit test on the left side, and switch to "arguments" Tab on the right side, append a `-javaagent:` parameter in the "VM Options", the following figure is an example, note that the `testable-agent` package should be modified to match the actual situation of the local Maven repository path.
|
||||
|
||||

|
||||
@@ -4,78 +4,86 @@ Fast Mocking
|
||||
Compared with the class-granularity mocking practices of existing mock tools, `TestableMock` allows developers to directly define a single method and use it for mocking. With the principle of convention over configuration, mock method replacement will automatically happen when the specified method in the test class match an invocation in the class under test.
|
||||
|
||||
> In summary, there are two simple rules:
|
||||
> - Mock non-constructive method, copy the original method definition to the test class, add a parameter of the same type as the caller, and add a `@MockMethod` annotation
|
||||
> - Mock non-constructive method, copy the original method definition to the test class, add a `@MockMethod` annotation
|
||||
> - Mock construction method, copy the original method definition to the test class, replace the return value with the constructed type, the method name is arbitrary, and add a `@MockContructor` annotation
|
||||
|
||||
> **Note**: There is also a convention in the current version that the name of the test class should be `<NameOfClassUnderTest> + Test` (and in the same package path), which is usually the by-default naming convention of Java project managed by `Maven` or `Gradle`. This constraint may be relaxed or removed in future versions of `TestableMock`.
|
||||
|
||||
The detail mock method definition convention is as follows:
|
||||
|
||||
#### 1. Mock method calls of any class
|
||||
|
||||
Define an ordinary method annotated with `@MockMethod` in the test class with exactly the same signature (name, parameter, and return value type) as the method to be mocked, and then add an extra parameter as the first parameter of method, with the same type as the object that the method originally belongs to.
|
||||
Define an ordinary method annotated with `@MockMethod` in the test class with exactly the same signature (name, parameter, and return value type) as the method to be mocked, and then add the type of target object (which the method originally belongs to) as `targetMethod` parameter of `@MockMethod` annotation.
|
||||
|
||||
At this time, all invocations to that original method in the class under test will be automatically replaced with invocations to the above-mentioned mock method when the unit test is running.
|
||||
|
||||
**Note**: When several methods to be mocked have the same name, you can put the name of the method to be mocked in the `targetMethod` parameter of `@MockMethod` annotation, so that the mock method itself can be named at will.
|
||||
|
||||
For example, there is a call to `"anything".substring(1, 2)` in the class under test, and we want to change it to a fixed string when running the test, we only need to define the following method in the test class:
|
||||
|
||||
```java
|
||||
// The original method signature is `String substring(int, int)`
|
||||
// The object `"anything"` that invokes this method is of type `String`
|
||||
// Adds a `String` type parameter to the first position the mock method parameter list (parameter name is arbitrary)
|
||||
// This parameter can be used to get the value and context of the actual invoker at runtime
|
||||
@MockMethod
|
||||
private String substring(String self, int i, int j) {
|
||||
@MockMethod(targetClass = String.class)
|
||||
private String substring(int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
```
|
||||
|
||||
When several methods to be mocked have the same name, you can put the name of the method to be mocked in the `targetMethod` parameter of `@MockMethod` annotation, so that the mock method itself can be named at will.
|
||||
|
||||
The following example shows the usage of the `targetMethod` parameter, and its effect is the same as the above example:
|
||||
|
||||
```java
|
||||
// Use `targetMethod` to specify the name of the method that needs to be mocked
|
||||
// The method itself can now be named arbitrarily, but the method parameters still need to follow the same matching rules
|
||||
@MockMethod(targetMethod = "substring")
|
||||
private String use_any_mock_method_name(String self, int i, int j) {
|
||||
@MockMethod(targetClass = String.class, targetMethod = "substring")
|
||||
private String use_any_mock_method_name(int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
```
|
||||
|
||||
Sometimes, the mock method need to access the member variables in the original object that initiated the invocation, or invoke other methods of the original object. At this point, you can remove the `targetClass` parameter in the `@MockMethod` annotation, and then add a extra parameter whose type is the original object type of the method to the first index of the method parameter list.
|
||||
|
||||
The `TestableMock` convention is that when the `targetClass` parameter value of the `@MockMethod` annotation is empty, the first parameter of the mock method is the type of the target method, and the parameter name is arbitrary. In order to facilitate code reading, it is recommended to name this parameter as `self` or `src`. Example as follows:
|
||||
|
||||
```java
|
||||
// Adds a `String` type parameter to the first position the mock method parameter list (parameter name is arbitrary)
|
||||
// This parameter can be used to get the value and context of the actual invoker at runtime
|
||||
@MockMethod
|
||||
private String substring(String self, int i, int j) {
|
||||
// Call the original method is also allowed
|
||||
return self.substring(i, j);
|
||||
}
|
||||
```
|
||||
|
||||
For complete code examples, see the `should_able_to_mock_common_method()` test cases in the `java-demo` and `kotlin-demo` sample projects. (Because Kotlin has made magical changes to the String type, the method under test in the Kotlin example adds a layer of encapsulation to the `BlackBox` class)
|
||||
|
||||
#### 2. Mock the member method of the class under test itself
|
||||
|
||||
Sometimes, when testing certain methods, it is desirable to mock out some other member methods of the class under test itself.
|
||||
|
||||
The solution is the same as the previous case. The first parameter type of the mock method needs to be the same as that of the class under test.
|
||||
The solution is the same as the previous case. Just set `targetClass` parameter value to the type of class under test.
|
||||
|
||||
For example, there is a private method with the signature `String innerFunc(String)` in the class under test. If we want to replace it during testing, we only need to define the following method in the test class:
|
||||
|
||||
```java
|
||||
// The type to test is `DemoMock`
|
||||
// So when defining the mock method, add a parameter of type `DemoMock` to the first position of parameter list (the name is arbitrary)
|
||||
@MockMethod
|
||||
private String innerFunc(DemoMock self, String text) {
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, if the method in the above example needs to access the original tested object that initiated the call, it may not use the `targetClass` parameter, but when defining the mock method, add a parameter of type `DemoMock` to the first index of the method parameter list.
|
||||
|
||||
For complete code examples, see the `should_able_to_mock_member_method()` test case in the `java-demo` and `kotlin-demo` sample projects.
|
||||
|
||||
#### 3. Mock static methods of any class
|
||||
|
||||
Mock for static methods is the same as for any ordinary methods. But it should be noted that when the mock method of a static method is called, the actual value of the first parameter passed in is always `null`.
|
||||
Mock for static methods is the same as for any ordinary methods.
|
||||
|
||||
For example, if the static method `secretBox()` of the `BlackBox` type is invoked in the class under test, and the method signature is changed to `BlackBox secretBox()`, the mock method is as follows:
|
||||
For example, if the static method `secretBox()` of the `BlackBox` type is invoked in the class under test, and the method signature is `BlackBox secretBox()`, then the mock method is as follows:
|
||||
|
||||
```java
|
||||
// The target static method is defined in the `BlackBox` type
|
||||
// When defining the mock method, add a parameter of type `BlackBox` to the first position parameter list (the name is arbitrary)
|
||||
// This parameter is only used to identify the target type, the actual incoming value will always be `null`
|
||||
@MockMethod
|
||||
private BlackBox secretBox(BlackBox ignore) {
|
||||
@MockMethod(targetClass = BlackBox.class)
|
||||
private BlackBox secretBox() {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
```
|
||||
@@ -105,9 +113,42 @@ For complete code examples, see the `should_able_to_mock_new_object()` test case
|
||||
|
||||
#### 5. Identify the current test case and invoke source
|
||||
|
||||
In the mock method, you can use `TestableTool.TEST_CASE` and `TestableTool.SOURCE_METHOD` to identify **the name of the currently running test case** and **the name of the method under test before entering the mock method**, so as to distinguish different invocation source.
|
||||
In the mock method, you can use the `TestableTool.SOURCE_METHOD` variable to identify **the method name of the class under test before entering the mock method**; in addition, the `TestableTool.MOCK_CONTEXT` variable can **inject additional context parameters into the mock method**, to distinguish and process different calling scenarios.
|
||||
|
||||
> The implementation mechanism of these two fields is based on call stack analysis. Although various special cases have been dealt with, there is still the possibility of misjudgment in complex scenarios involving multiple threads. If you find a relevant reproducible BUG, please submit an issue on Github.
|
||||
For example, to verify the impact on the target method under test when the mock method returns different results in the test case:
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void testDemo() {
|
||||
MOCK_CONTEXT.set("case", "data-ready");
|
||||
assertEquals(true, demo());
|
||||
MOCK_CONTEXT.set("case", "has-error");
|
||||
assertEquals(false, demo());
|
||||
MOCK_CONTEXT.clear();
|
||||
}
|
||||
```
|
||||
|
||||
Take out the injected parameters in the mock method and return different results according to the situation:
|
||||
|
||||
```java
|
||||
@MockMethod
|
||||
private Data mockDemo() {
|
||||
switch((String)MOCK_CONTEXT.get("case")) {
|
||||
case "data-ready":
|
||||
return new Data();
|
||||
case "has-error":
|
||||
throw new NetworkException();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that because `TestableMock` does not (and won't to) rely on any specific test framework, it cannot automatically identify the end position of a single test case, which makes the parameters set to the `TestableTool.MOCK_CONTEXT` variable may exist cross test cases in the same test class. It is recommended to always use `MOCK_CONTEXT.clear()` to clear the context immediately after use. You can also add this statement to the unified position where the test case ends of the specific unit test framework, such as the `@AfterEach` method of JUnit 5.
|
||||
|
||||
In the current version, the effect of this variable at runtime is similar to a normal `Map` type member object in the test class, but please try to use this variable instead of a custom object to pass additional mock parameters in order to get better compatibility in the upcoming`v0.5` version.
|
||||
|
||||
> The `TestableTool.MOCK_CONTEXT` variable is currently shared within the test class. When the unit test runs in parallel, it is recommended to select the `parallel` type as `classes`
|
||||
|
||||
For complete code examples, see the `should_able_to_get_source_method_name()` and `should_able_to_get_test_case_name()` test cases in the `java-demo` and `kotlin-demo` sample projects.
|
||||
|
||||
@@ -116,3 +157,11 @@ For complete code examples, see the `should_able_to_get_source_method_name()` an
|
||||
In test cases, you can use the `TestableTool.verify()` method, and cooperate with `with()`, `withInOrder()`, `without()`, `withTimes()` and other methods to verify the mock call situation.
|
||||
|
||||
For details, please refer to the [Check Mock Call](en-us/doc/matcher.md) document.
|
||||
|
||||
#### Additional note
|
||||
|
||||
> **Mock convention in version 0.4.x**:
|
||||
> - The name of the test class must be `<NameOfClassUnderTest> + Test` (and in the same package path), which is usually the by-default naming convention of Java project managed by `Maven` or `Gradle`.
|
||||
> - Do NOT access any non-`static` members in mock methods. Currently, methods that is decorated by `@MockMethod` or `@MockContructor` annotations will be automatically modified to `static` methods during runtime. (When mock method contains some statement like _lambda function_, _anonymous class_ or _initiation block_, java compiler will generate additional method during compilation, these mock methods also have to be declared as `static` to avoid non-static dynamical method invoked.)
|
||||
>
|
||||
> These constraints will change in `0.5` versions of `TestableMock`.
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
- Quick Start
|
||||
- [Use TestableMock](en-us/doc/setup.md)
|
||||
- [Private Accessor](en-us/doc/private-accessor.md)
|
||||
- [Fast Mocking](en-us/doc/use-mock.md)
|
||||
- [Private Accessor](en-us/doc/private-accessor.md)
|
||||
- [Test Void Method](en-us/doc/test-void-method.md)
|
||||
- [Parameter constructor](en-us/doc/parameter-constructor.md)
|
||||
|
||||
- Usage Reference
|
||||
- Usage Guide
|
||||
- [Verify Mock Invocation](en-us/doc/invoke-matcher.md)
|
||||
- [Frequently Asked Questions](en-us/doc/frequently-asked-questions.md)
|
||||
- [Use TestableMock In IDE](en-us/doc/use-in-ide.md)
|
||||
- [Self-Help Troubleshooting](en-us/doc/troubleshooting.md)
|
||||
- [Testable Maven Plugin](en-us/doc/use-maven-plugin.md)
|
||||
|
||||
- Technical Reference
|
||||
- [Mock Tools Comparison](en-us/doc/comparation.md)
|
||||
- [Release Note](en-us/doc/release-note.md)
|
||||
- [About Us](en-us/doc/about-us.md)
|
||||
- [Feedback Channel](en-us/doc/feedback.md)
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
hook.beforeEach(function (html) {
|
||||
return html
|
||||
+ '\n\n----\n\n'
|
||||
+ '<a href="https://devops.aliyun.com" target="_blank" style="color: inherit; font-weight: normal; text-decoration: none;">Powered by 云效</a>'
|
||||
+ '<a href="https://www.aliyun.com/product/yunxiao" target="_blank" style="color: inherit; font-weight: normal; text-decoration: none;">Powered by 云效</a>'
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
9
docs/zh-cn/doc/about-us.md
Normal file
9
docs/zh-cn/doc/about-us.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## 关于我们
|
||||
|
||||
`TestableMock`的核心开发团队来自**阿里云·云研发部**(前身为**阿里集团研发效能部**)。我们不仅对内支持阿里数万名技术小二每天从任务分配、代码提交、功能测试、到产品发布的全生命周期流程,同时也是阿里云企业级研发协同平台[云效](https://www.aliyun.com/product/yunxiao)的产品研发团队,一直深耕在开发者工具领域,致力于改善开发者生产体验和构筑企业数字化研发生态。
|
||||
|
||||
这个项目的原始灵感来自于我们对Java开发者在日常单元测试中经常遇到的痛点总结([外部依赖Mock繁琐](zh-cn/doc/use-mock.md)、[私有方法难测试](zh-cn/doc/private-accessor.md)、[无返回值方法难测试](zh-cn/doc/test-void-method.md)、[复杂参数难构造](zh-cn/doc/parameter-constructor.md))。它在内部的名字是`Testable`,开源时为了搜索引擎优化,改名为`TestableMock`,并主推轻量级Mock功能。 不要被它的名字所迷惑,`TestableMock`绝不仅仅是Mock。
|
||||
|
||||
从黑客马拉松孵化,到内部开源,再到对外开源,`TestableMock`已经积累了一批阿里集团内部和外部社区的开发者用户。与此同时,我们也在不断完善`TestableMock`自身的功能丰富性和稳定性。按照当前的版本发布计划,每次新版本发布前会首先提交包含`SNAPSHOT`标记的内测版到内部仓库,在至少稳定使用一天以后,才会成为正式版提交到Maven中心仓库(紧急BUG修复版本除外),以确保工具在为大家带来更多便捷的同时具有足够的可靠性。
|
||||
|
||||
最后,关于社区里有对“破坏封装进行单元测试是否等同于纵容代码腐化”的讨论,我们持相对乐观的态度。正如`PowerMock`的诞生并没有真的催生更多烂项目,反而切实解决了许多Java语言遗留的测试难题。与其束手束脚的采用“测试技巧”间接测试功能,不如索性解除限制,就让单元测试来得更猛烈一些吧!🤠
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#### 2. `TestableMock`是否能够与其他Mock工具一起使用?
|
||||
|
||||
`TestableMock`可与其他基于动态代理机制的Mock工具安全的共同使用,譬如`Mockito`、`EasyMock`、`MockRunner`等皆属此范畴。
|
||||
`TestableMock`可与其他基于动态代理机制的Mock工具安全的共同使用,譬如`Mockito`、`Spock`、`EasyMock`等皆属此范畴。
|
||||
|
||||
对于会修改类加载器或被测类字节码的Mock工具,譬如`PowerMock`和`JMockit`,尚无案例证明会与`TestableMock`发生冲突,但从原理来说二者可能存在不兼容风险,请谨慎使用。
|
||||
|
||||
@@ -35,14 +35,40 @@ Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.S
|
||||
|
||||
实际场景中需要对`String`类中的方法进行Mock的场景很少,`TestableMock`暂未对这种情况做特别处理。
|
||||
|
||||
#### 6. 在IntelliJ IDE 2020.3版本中运行单个测试用例时,用了`@EnablePrivateAccess`注解还是报私有成员访问错误?
|
||||
#### 6. 当被Mock的方法被其它测试类**间接调用**时依然有效吗?
|
||||
|
||||
IntelliJ从`2020.2.2`版本以后,IntelliJ对`JSR-269`规范注解处理器的处理机制发生了变化,与Maven标准不再完全兼容。可通过IntelliJ系统配置的"Build Tools > Maven > Runner"中开启"Delegate IDE build/run actions to maven"选项解决:
|
||||
同样有效,Mock的作用范围是整个测试运行过程。
|
||||
|
||||

|
||||
例如测试类`AaaTest`中Mock了`Aaa`类的某些私有方法(或者某些外部方法调用);在另一个测试类`BbbTest`中测试`Bbb`类时,某些方法间接用到了`Aaa`类被Mock过的方法或调用,此时实际调用的同样会是`AaaTest`类中定义的Mock方法。
|
||||
|
||||
#### 7. `TestableMock`能否用于Android项目的测试?
|
||||
|
||||
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。
|
||||
|
||||
Android系统的`Dalvik`和`ART`虚拟机采用了与标准JVM不同的字节码体系,会影响`TestableMock`的正常工作。`Roboelectric`框架能在普通JVM虚拟机上运行Android单元测试,其速度比通过Android虚拟机运行单元测试快非常多,绝大多数Android App的单元测试都在使用`Roboelectric`框架。
|
||||
|
||||
#### 8. 使用Mock时候遇到"Attempt to access non-static member in mock method"错误?
|
||||
|
||||
当前`TestableMock`的设计不允许在Mock方法中访问测试类的非`static`成员(因为Mock方法自身会在运行期被动态修改为`static`类型)。然而有些Java语句,包括构造块(譬如`new ArrayList<String>() {{ append("data"); }}`)、匿名函数(譬如`list.stream().map(i -> i.get)`)等等,会在编译过程中生成额外的成员方法调用,导致Mock方法执行报错。
|
||||
|
||||
最简单的解决办法是将Mock方法本身也声明为`static`类型(这样动态生成的调用也会是`static`的,避免了以上错误),例如原方法定义为:
|
||||
|
||||
```java
|
||||
@MockMethod
|
||||
private int getXxx(Demo self) {}
|
||||
```
|
||||
|
||||
将其修改为:
|
||||
|
||||
```java
|
||||
@MockMethod
|
||||
private static int getXxx(Demo self) {}
|
||||
```
|
||||
|
||||
在下一个大迭代版本(**即`0.5`版本**)中,将会在保持当前Mock体验的前提下,对Mock的实现机制进行修改,不再需要修改Mock方法为静态方法,从而彻底解决此类报错问题。
|
||||
|
||||
#### 9. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
|
||||
|
||||
这个问题是由于系统ClassPath包含太多路径所致,与是否使用`TestableMock`无关。但需要注意的是,IntelliJ提供了两种辅助解决机制:`JAR manifest`和`classpath file`,若测试中使用了`TestableMock`,请选择`JAR manifest`。
|
||||
|
||||

|
||||
|
||||
6
docs/zh-cn/doc/parameter-constructor.md
Normal file
6
docs/zh-cn/doc/parameter-constructor.md
Normal file
@@ -0,0 +1,6 @@
|
||||
快速构造复杂入参
|
||||
---
|
||||
|
||||
不论被测方法所需的参数结构多么错综复杂、甚至没有合适的构造方法、甚至需要私有内部类对象... 呼唤TestableMock,马上递给您~
|
||||
|
||||
计划在`0.5.x`版本中推出。
|
||||
@@ -1,5 +1,33 @@
|
||||
# Release Note
|
||||
|
||||
## 0.4.11
|
||||
- 支持测试类访问与自身包路径不同的被测类的私有成员
|
||||
- 增加`PrivateAccessor`访问的私有方法参数数目检查,提高抗代码重构能力
|
||||
- 修复被Mock方法包含数组参数可能导致出错的BUG (issue-48)
|
||||
- 修复一处会导致在IntelliJ中构建找不到私有成员的问题
|
||||
|
||||
## 0.4.10
|
||||
- 修复在Lambda函数中使用Mock出错的BUG(issue-44)
|
||||
- 修复调用私有方法时参数值不能为null的问题(issue-27)
|
||||
|
||||
## 0.4.9
|
||||
- 修复发起调用的对象不是局部或成员变量时Mock出错的BUG (issue-40)
|
||||
- 增加`PrivateAccessor`访问目标有效性检查,提高抗代码重构能力 (issue-21)
|
||||
|
||||
## 0.4.8
|
||||
- 修复赋值语句中的私有方法调用无法访问的BUG (issue-33)
|
||||
- 支持在包含`SpringRunner`的测试中使用Mock (issue-30)
|
||||
- `@MockMethod`注解支持`targetClass`参数 (issue-24)
|
||||
|
||||
## 0.4.7
|
||||
- 修复由于`MOCK_CONTEXT`引入的堆栈大小错误
|
||||
- 修复Windows下的`Gradle`构建路径错误 (issue-25)
|
||||
|
||||
## 0.4.6
|
||||
- 修复一处`IINC`字节码处理异常
|
||||
- 支持使用`TestableTool.MOCK_CONTEXT`变量为Mock方法注入额外上下文参数 (issue-17)
|
||||
- 不再推荐使用`TestableTool.TEST_CASE`变量来区分测试用例
|
||||
|
||||
## 0.4.5
|
||||
- 修复IntelliJ 2020.3+环境下的私有成员访问编译期错误
|
||||
- 修复潜在的跨用例初始化空指针异常 (issue-20)
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
`TestableMock`是基于源码和字节码增强的Java单元测试辅助工具,包含以下功能:
|
||||
|
||||
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
|
||||
- [快速Mock任意调用](zh-cn/doc/use-mock.md):使被测类的任意方法调用快速替换为Mock方法,实现"指哪换哪",解决传统Mock工具使用繁琐的问题
|
||||
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
|
||||
- [辅助测试void方法](zh-cn/doc/test-void-method.md):利用Mock校验器对方法的内部逻辑进行检查,解决无返回值方法难以实施单元测试的问题
|
||||
- [快速构造参数对象](zh-cn/doc/parameter-constructor.md):生成任意多层嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题
|
||||
|
||||
## 在Maven项目中使用
|
||||
|
||||
@@ -15,7 +16,7 @@
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.4.5</testable.version>
|
||||
<testable.version>0.4.11</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -58,12 +59,12 @@
|
||||
|
||||
## 在Gradle项目中使用
|
||||
|
||||
在`build.gradle`文件中添加TestableMock依赖:
|
||||
在`build.gradle`文件中添加`TestableMock`依赖:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.5')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.5')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.11')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.11')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,3 +77,19 @@ test {
|
||||
```
|
||||
|
||||
参见项目`java-demo`的[build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/build.gradle)和`kotlin-demo`的[build.gradle.kts](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/build.gradle.kts)文件。
|
||||
|
||||
> 若是基于`Robolectric`框架的Android项目,则添加`TestableMock`依赖方法同上,添加javaagent配置方法如下:
|
||||
>
|
||||
> ```groovy
|
||||
> android {
|
||||
> testOptions {
|
||||
> unitTests {
|
||||
> all {
|
||||
> jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> 完整示例参考[issue-43](https://github.com/alibaba/testable-mock/issues/43)
|
||||
|
||||
@@ -38,3 +38,9 @@ class DemoTest {
|
||||
```
|
||||
|
||||
该日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
|
||||
|
||||
简单排查方法:
|
||||
|
||||
- 若没有任何输出,请检查`pom.xml`或`build.gradle`配置是否正确引入了TestableMock依赖
|
||||
- 若只输出了第一行`Handling test class`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test"(`0.4.x`版本要求)
|
||||
- 若输出了`Handling source class`以及`Handling method xxx`,但预期的代码行位置没有发生Mock替换,请检查Mock方法定义是否未与目标方法匹配
|
||||
|
||||
18
docs/zh-cn/doc/use-in-ide.md
Normal file
18
docs/zh-cn/doc/use-in-ide.md
Normal file
@@ -0,0 +1,18 @@
|
||||
在IDE中运行单元测试
|
||||
---
|
||||
|
||||
## 使用IntelliJ IDE
|
||||
|
||||
IntelliJ IDE对`TestableMock`所用到的`JSR-269`注释处理器以及`maven-surefire-plugin`插件的附加参数均支持良好。通常无需特殊配置,可开箱即用。
|
||||
|
||||
## 使用Eclipse IDE
|
||||
|
||||
由于`Eclipse`内置的自动编译功能基于三方编译器实现,与标准`javac`编译过程不兼容,会导致在IDE中运行测试用例时`@EnablePrivateAccess`注解无效。不过,通过`PrivateAccessor`工具类访问被测类私有成员的功能不会受编译器差异影响。
|
||||
|
||||
若项目中使用了`@EnablePrivateAccess`注解,可在`Eclipse`的命令行中使用`mvn test -Dtest=<测试类名>`和`mvn test -Dtest=<测试类名>#<测试用例名>`来运行单个测试类或测试用例。
|
||||
|
||||
同时,由于`Eclipse`内置的单元测试执行器完全忽略`pom.xml`文件的配置,因此若需使用Mock功能,需进行额外配置。
|
||||
|
||||
以使用`JUnit`为例,方法为从IDE工具栏的运行按钮旁边的小三角处下拉,选择"Run Configurations...",左侧选择要运行单元测试的任务,在右侧切换到"arguments"标签页,在"VM Options"里添加`-javaagent:`参数,下图为示例,注意应修改`testable-agent`包为与实际情况匹配的本地Maven仓库路径。
|
||||
|
||||

|
||||
@@ -19,13 +19,15 @@
|
||||
</plugin>
|
||||
```
|
||||
|
||||
> 当使用`testable-maven-plugin`插件时,应该移除`maven-surefire-plugin`插件上的TestableMock相关配置
|
||||
**注意:**当使用`testable-maven-plugin`插件时,应该移除`maven-surefire-plugin`插件上的TestableMock相关配置。
|
||||
|
||||
`testable-maven-plugin`插件能够与Jacoco插件直接同时使用,无需额外适配,因此能使`pom.xml`文件编写起来更简单且美观。
|
||||
|
||||
但需要注意的是,当通过IDE运行单个测试用例时,Mock功能会失效。
|
||||
> 还有一种特殊情况,当`jacoco`插件是通过`maven`命令行参数引入的时候,若要使用`TestableMock`功能,则也必须通过命令行参数引入`testable-maven-plugin`插件。详见[issue-14](https://github.com/alibaba/testable-mock/issues/14)。
|
||||
|
||||
这是由于IDE运行单个测试用例时通常都只会运行`maven-surefire-plugin`插件,跳过了`testable-maven-plugin`插件执行,导致Mock功能所需的JavaAgent没有随测试注入。
|
||||
但需要注意的是,使用`testable-maven-plugin`插件后,通过IntelliJ IDE运行单个测试用例时,Mock功能会失效。
|
||||
|
||||
这是由于IntelliJ IDE运行单个测试用例时只会运行`maven-surefire-plugin`插件,跳过了`testable-maven-plugin`插件执行,导致Mock功能所需的JavaAgent未随测试注入。
|
||||
|
||||
该问题可以通过额外配置IDE的测试参数绕过。以IntelliJ为例,打开运行菜单的"编辑配置..."选型,如图中位置①
|
||||
|
||||
|
||||
@@ -4,87 +4,97 @@
|
||||
相比以往Mock工具以类为粒度的Mock方式,`TestableMock`允许用户直接定义需要Mock的单个方法,并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测方法中的指定方法调用。
|
||||
|
||||
> 归纳起来就两条:
|
||||
> - Mock非构造方法,拷贝原方法定义到测试类,增加一个与调用者类型相同的参数,加`@MockMethod`注解
|
||||
> - Mock非构造方法,拷贝原方法定义到测试类,加`@MockMethod`注解
|
||||
> - Mock构造方法,拷贝原方法定义到测试类,返回值换成构造的类型,方法名随意,加`@MockContructor`注解
|
||||
|
||||
> **注意**:当前版本还有一项约定是,测试类与被测类的包路径应相同,且名称为`被测类名+Test`,通常采用`Maven`或`Gradle`构建的Java项目符合这种惯例。此约定在未来的`TestableMock`版本中可能会被放宽或去除。
|
||||
|
||||
具体的Mock方法定义约定如下:
|
||||
|
||||
#### 1. 覆写任意类的方法调用
|
||||
|
||||
在测试类里定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
|
||||
在测试类里定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,并在注解的`targetClass`参数指定该方法原本所属对象类型。
|
||||
|
||||
此时被测类中所有对该需覆写方法的调用,将在单元测试运行时,将自动被替换为对上述自定义Mock方法的调用。
|
||||
|
||||
**注意**:当遇到待覆写方法有重名时,可以将需覆写的方法名写到`@MockMethod`注解的`targetMethod`参数里,这样Mock方法自身就可以随意命名了。
|
||||
|
||||
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在测试类定义如下方法:
|
||||
|
||||
```java
|
||||
// 原方法签名为`String substring(int, int)`
|
||||
// 调用此方法的对象`"anything"`类型为`String`
|
||||
// 则Mock方法签名在其参数列表首位增加一个类型为`String`的参数(名字随意)
|
||||
// 此参数可用于获得当时的实际调用者的值和上下文
|
||||
@MockMethod
|
||||
private String substring(String self, int i, int j) {
|
||||
@MockMethod(targetClass = String.class)
|
||||
private String substring(int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
```
|
||||
|
||||
当遇到待覆写方法有重名时,可以将需覆写的方法名写到`@MockMethod`注解的`targetMethod`参数里,这样Mock方法自身就可以随意命名了。
|
||||
|
||||
下面这个例子展示了`targetMethod`参数的用法,其效果与上述示例相同:
|
||||
|
||||
```java
|
||||
// 使用`targetMethod`指定需Mock的方法名
|
||||
// 此方法本身现在可以随意命名,但方法参数依然需要遵循相同的匹配规则
|
||||
@MockMethod(targetMethod = "substring")
|
||||
private String use_any_mock_method_name(String self, int i, int j) {
|
||||
@MockMethod(targetClass = String.class, targetMethod = "substring")
|
||||
private String use_any_mock_method_name(int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
```
|
||||
|
||||
有时,在Mock方法里会需要访问发起调用的原始对象中的成员变量,或是调用原始对象的其他方法。此时,可以将`@MockMethod`注解中的`targetClass`参数去除,然后在方法参数列表首位增加一个类型为该方法原本所属对象类型的参数。
|
||||
|
||||
`TestableMock`约定,当`@MockMethod`注解的`targetClass`参数值为空时,Mock方法的首位参数即为目标方法所属类型,参数名称随意。通常为了便于代码阅读,建议将此参数统一命名为`self`或`src`。举例如下:
|
||||
|
||||
```java
|
||||
// Mock方法在参数列表首位增加一个类型为`String`的参数(名字随意)
|
||||
// 此参数可用于获得当时的实际调用者的值和上下文
|
||||
@MockMethod
|
||||
private String substring(String self, int i, int j) {
|
||||
// 可以直接调用原方法,此时Mock方法仅用于记录调用,常见于对void方法的测试
|
||||
return self.substring(i, j);
|
||||
}
|
||||
```
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_common_method()`测试用例。(由于Kotlin对String类型进行了魔改,故Kotlin示例中将被测方法在`BlackBox`类里加了一层封装)
|
||||
|
||||
#### 2. 覆写被测类自身的成员方法
|
||||
|
||||
有时候,在对某些方法进行测试时,希望将被测类自身的另外一些成员方法Mock掉。
|
||||
有时候,在对某些方法进行测试时,希望将被测类自身的另外一些成员方法Mock掉(比如这个方法里有许多外部依赖或耗时操作)。
|
||||
|
||||
操作方法与前一种情况相同,Mock方法的第一个参数类型需与被测类相同,即可实现对被测类自身(不论是公有或私有)成员方法的覆写。
|
||||
做法与前一种情况完全相同,只需将`targetClass`参数赋值为被测类,即可实现对被测类自身(不论是公有或私有)成员方法的覆写。
|
||||
|
||||
例如,被测类中有一个签名为`String innerFunc(String)`的私有方法,我们希望在测试的时候将它替换掉,则只需在测试类定义如下方法:
|
||||
|
||||
```java
|
||||
// 被测类型是`DemoMock`
|
||||
// 因此在定义Mock方法时,在目标方法参数首位加一个类型为`DemoMock`的参数(名字随意)
|
||||
@MockMethod
|
||||
private String innerFunc(DemoMock self, String text) {
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
```
|
||||
|
||||
同样的,上述示例中的方法如需访问发起调用的原始被测对象,也可不使用`targetClass`参数,而是在定义Mock方法时,在方法参数列表首位加一个类型为`DemoMock`的参数(名字随意)。
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_member_method()`测试用例。
|
||||
|
||||
#### 3. 覆写任意类的静态方法
|
||||
|
||||
对于静态方法的Mock与普通方法相同。但需要注意的是,静态方法的Mock方法被调用时,传入的第一个参数实际值始终是`null`。
|
||||
对于静态方法的Mock与普通方法相同。
|
||||
|
||||
例如,在被测类中调用了`BlackBox`类型中的静态方法`secretBox()`,改方法签名为`BlackBox secretBox()`,则Mock方法如下:
|
||||
例如,在被测类中调用了`BlackBox`类型中的静态方法`secretBox()`,该方法签名为`BlackBox secretBox()`,则Mock方法如下:
|
||||
|
||||
```java
|
||||
// 目标静态方法定义在`BlackBox`类型中
|
||||
// 在定义Mock方法时,在目标方法参数首位加一个类型为`BlackBox`的参数(名字随意)
|
||||
// 此参数仅用于标识目标类型,实际传入值将始终为`null`
|
||||
@MockMethod
|
||||
private BlackBox secretBox(BlackBox ignore) {
|
||||
@MockMethod(targetClass = BlackBox.class)
|
||||
private BlackBox secretBox() {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
```
|
||||
|
||||
对于静态方法的Mock,通常不使用方法参数列表的首位加参数来表示目标类型。但这种方法也依然适用,只是实际传入的第一个参数值将始终是`null`。
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_static_method()`测试用例。
|
||||
|
||||
#### 4. 覆写任意类的new操作
|
||||
|
||||
在测试类里定义一个有`@MockContructor`注解的普通方法,使该方法返回值类型为要被创建的对象类型,且方法参数与要Mock的构造函数参数完全一致,方法名称随意。
|
||||
在测试类里定义一个返回值类型为要被创建的对象类型,且方法参数与要Mock的构造函数参数完全一致的方法,名称随意,然后加上`@MockContructor`注解。
|
||||
|
||||
此时被测类中所有用`new`创建指定类的操作(并使用了与Mock方法参数一致的构造函数)将被替换为对该自定义方法的调用。
|
||||
|
||||
@@ -92,22 +102,53 @@ private BlackBox secretBox(BlackBox ignore) {
|
||||
|
||||
```java
|
||||
// 要覆写的构造函数签名为`BlackBox(String)`
|
||||
// 无需在Mock方法参数列表增加额外参数,Mock方法的名称随意起
|
||||
// Mock方法返回`BlackBox`类型对象,方法的名称随意起
|
||||
@MockContructor
|
||||
private BlackBox createBlackBox(String text) {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
```
|
||||
|
||||
> 也可以依然使用`@MockMethod`注解,并配置`targetMethod`参数值为`"<init>"`,其余同上。效果与使用`@MockContructor`注解相同
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_new_object()`测试用例。
|
||||
|
||||
#### 5. 识别当前测试用例和调用来源
|
||||
|
||||
在Mock方法中可以通过`TestableTool.TEST_CASE`和`TestableTool.SOURCE_METHOD`来识别**当前运行的测试用例名称**和**进入该Mock方法前的被测类方法名称**,从而区分处理不同的调用场景。
|
||||
在Mock方法中通过`TestableTool.SOURCE_METHOD`变量可以识别**进入该Mock方法前的被测类方法名称**;此外,还可以借助`TestableTool.MOCK_CONTEXT`变量为Mock方法注入“**额外的上下文参数**”,从而区分处理不同的调用场景。
|
||||
|
||||
> 这两个字段的实现机制基于调用堆栈分析,尽管已经做了各种特殊情况的处理,但在涉及多线程的复杂场景下,依然存在误判的可能。若您发现了相关的可复现BUG,请在Github提交Issue。
|
||||
例如,在测试用例中验证当被Mock方法返回不同结果时,对被测目标方法的影响:
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void testDemo() {
|
||||
MOCK_CONTEXT.put("case", "data-ready");
|
||||
assertEquals(true, demo());
|
||||
MOCK_CONTEXT.put("case", "has-error");
|
||||
assertEquals(false, demo());
|
||||
MOCK_CONTEXT.clear();
|
||||
}
|
||||
```
|
||||
|
||||
在Mock方法中取出注入的参数,根据情况返回不同结果:
|
||||
|
||||
```java
|
||||
@MockMethod
|
||||
private Data mockDemo() {
|
||||
switch((String)MOCK_CONTEXT.get("case")) {
|
||||
case "data-ready":
|
||||
return new Data();
|
||||
case "has-error":
|
||||
throw new NetworkException();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意,由于`TestableMock`并不依赖(也不希望依赖)任何特定测试框架,因而无法自动识别单个测试用例的结束位置,这使得设置到`TestableTool.MOCK_CONTEXT`变量的参数可能会在同测试类中跨测试用例存在。建议总是在使用后及时使用`MOCK_CONTEXT.clear()`清空上下文,也可将这行语句添加到单元测试框架特定的测试用例结束的统一位置,比如JUnit 5的`@AfterEach`方法。
|
||||
|
||||
在当前版本中,此变量在运行期的效果类似于一个在测试类中的普通`Map`类型成员对象,但请尽量使用此变量而非自定义对象传递附加的Mock参数,以便在将来升级至`v0.5`版本时获得更好的兼容性。
|
||||
|
||||
> `TestableTool.MOCK_CONTEXT`变量的值是在测试类内共享的,当单元测试并行运行时,建议请选择`parallel`类型为`classes`
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_get_source_method_name()`和`should_able_to_get_test_case_name()`测试用例。
|
||||
|
||||
@@ -116,3 +157,13 @@ private BlackBox createBlackBox(String text) {
|
||||
在测试用例中可用通过`TestableTool.verify()`方法,配合`with()`、`withInOrder()`、`without()`、`withTimes()`等方法实现对Mock调用情况的验证。
|
||||
|
||||
详见[校验Mock调用](zh-cn/doc/matcher.md)文档。
|
||||
|
||||
#### 特别说明
|
||||
|
||||
> **0.4.x 版本的Mock约定**:
|
||||
> - 测试类与被测类的包路径应相同,且名称为`被测类名+Test`(通常采用`Maven`或`Gradle`构建的Java项目均符合这种惯例)
|
||||
> - Mock方法(即包含`@MockMethod`或`@MockContructor`注解的方法)会在运行期被自动修改为`static`方法,请勿在Mock方法的定义中访问任何非静态成员。
|
||||
>
|
||||
> 这两项约束会在`0.5`版本中去除
|
||||
>
|
||||
> 当Mock方法内容较复杂(包含Lambda语句、构造块、匿名类等)时,编译器会在构建期生成额外的非静态临时方法,导致"Bad type in operand stack"错误。如果有遇到此类错误,请将Mock方法显式加上`static`修饰即可解决。这个问题会在`0.5`版本中彻底解决。
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
- 快速上手
|
||||
- [使用TestableMock](zh-cn/doc/setup.md)
|
||||
- [直接访问私有成员](zh-cn/doc/private-accessor.md)
|
||||
- [快速Mock任意方法](zh-cn/doc/use-mock.md)
|
||||
- [直接访问私有成员](zh-cn/doc/private-accessor.md)
|
||||
- [测试无返回值的方法](zh-cn/doc/test-void-method.md)
|
||||
- [快速构造复杂入参](zh-cn/doc/parameter-constructor.md)
|
||||
|
||||
- 使用参考
|
||||
- 使用指南
|
||||
- [校验Mock调用](zh-cn/doc/invoke-matcher.md)
|
||||
- [常见使用问题](zh-cn/doc/frequently-asked-questions.md)
|
||||
- [在IDE运行单元测试](zh-cn/doc/use-in-ide.md)
|
||||
- [自助问题排查](zh-cn/doc/troubleshooting.md)
|
||||
- [Testable Maven插件](zh-cn/doc/use-maven-plugin.md)
|
||||
- [使用Maven插件](zh-cn/doc/use-maven-plugin.md)
|
||||
|
||||
- 技术参考
|
||||
- [主流Mock工具对比](zh-cn/doc/comparation.md)
|
||||
- [Release Note](zh-cn/doc/release-note.md)
|
||||
- [版本更新](zh-cn/doc/release-note.md)
|
||||
- [关于我们](zh-cn/doc/about-us.md)
|
||||
- [问题反馈](zh-cn/doc/feedback.md)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.5</version>
|
||||
<version>0.4.11</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
|
||||
@@ -14,6 +14,7 @@ public class PreMain {
|
||||
private static final String AND = "&";
|
||||
private static final String LOG_LEVEL = "logLevel";
|
||||
private static final String DUMP_PATH = "dumpPath";
|
||||
private static final String PKG_PREFIX = "pkgPrefix";
|
||||
private static final String EQUAL = "=";
|
||||
|
||||
public static void premain(String agentArgs, Instrumentation inst) {
|
||||
@@ -34,6 +35,8 @@ public class PreMain {
|
||||
GlobalConfig.setLogLevel(v);
|
||||
} else if (k.equals(DUMP_PATH)) {
|
||||
GlobalConfig.setDumpPath(v);
|
||||
} else if (k.equals(PKG_PREFIX)) {
|
||||
GlobalConfig.setPkgPrefix(v);
|
||||
}
|
||||
} else {
|
||||
GlobalConfig.setLogLevel(a);
|
||||
|
||||
@@ -11,12 +11,18 @@ public class ConstPool {
|
||||
public static final String TEST_POSTFIX = "Test";
|
||||
|
||||
public static final String FIELD_TARGET_METHOD = "targetMethod";
|
||||
public static final String FIELD_TARGET_CLASS = "targetClass";
|
||||
|
||||
public static final String MOCK_WITH = "com.alibaba.testable.core.annotation.MockWith";
|
||||
public static final String MOCK_METHOD = "com.alibaba.testable.core.annotation.MockMethod";
|
||||
public static final String MOCK_CONSTRUCTOR = "com.alibaba.testable.core.annotation.MockConstructor";
|
||||
public static final String TESTABLE_MOCK = "com.alibaba.testable.core.annotation.TestableMock";
|
||||
|
||||
public static final String CGLIB_CLASS_INFIX = "$$EnhancerBy";
|
||||
|
||||
public static final String KOTLIN_POSTFIX_COMPANION = "$Companion";
|
||||
public static final String KOTLIN_PREFIX_ACCESS = "access$";
|
||||
|
||||
/**
|
||||
* Name of the constructor method
|
||||
*/
|
||||
|
||||
@@ -15,18 +15,17 @@ import java.util.Iterator;
|
||||
*/
|
||||
abstract public class BaseClassHandler implements Opcodes {
|
||||
|
||||
protected static final String TESTABLE_MARK_FIELD = "__testable";
|
||||
|
||||
protected boolean wasTransformed(ClassNode cn) {
|
||||
protected boolean wasTransformed(ClassNode cn, String refName, String refDescriptor) {
|
||||
Iterator<FieldNode> iterator = cn.fields.iterator();
|
||||
if (iterator.hasNext()) {
|
||||
if (TESTABLE_MARK_FIELD.equals(iterator.next().name)) {
|
||||
if (refName.equals(iterator.next().name)) {
|
||||
// avoid duplicate injection
|
||||
LogUtil.verbose("Duplicate injection found, ignore " + cn.name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
cn.fields.add(new FieldNode(ACC_PRIVATE, TESTABLE_MARK_FIELD, "I", null, null));
|
||||
// TODO: `ACC_STATIC` should be removed in v0.5 to shorten the life cycle of this variable
|
||||
cn.fields.add(new FieldNode(ACC_PRIVATE | ACC_STATIC, refName, refDescriptor, null, null));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@ package com.alibaba.testable.agent.handler;
|
||||
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.model.MethodInfo;
|
||||
import com.alibaba.testable.agent.model.ModifiedInsnNodes;
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.util.BytecodeUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -18,6 +19,8 @@ import java.util.Set;
|
||||
*/
|
||||
public class SourceClassHandler extends BaseClassHandler {
|
||||
|
||||
private static final String TESTABLE_MARK_FIELD = "__testable";
|
||||
|
||||
private final List<MethodInfo> injectMethods;
|
||||
private final Set<Integer> invokeOps = new HashSet<Integer>() {{
|
||||
add(Opcodes.INVOKEVIRTUAL);
|
||||
@@ -36,16 +39,16 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
*/
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
if (wasTransformed(cn)) {
|
||||
if (wasTransformed(cn, TESTABLE_MARK_FIELD, "I")) {
|
||||
return;
|
||||
}
|
||||
Set<MethodInfo> memberInjectMethods = new HashSet<MethodInfo>();
|
||||
Set<MethodInfo> newOperatorInjectMethods = new HashSet<MethodInfo>();
|
||||
for (MethodInfo mi : injectMethods) {
|
||||
if (mi.getName().equals(ConstPool.CONSTRUCTOR)) {
|
||||
newOperatorInjectMethods.add(mi);
|
||||
for (MethodInfo im : injectMethods) {
|
||||
if (im.getName().equals(ConstPool.CONSTRUCTOR)) {
|
||||
newOperatorInjectMethods.add(im);
|
||||
} else {
|
||||
memberInjectMethods.add(mi);
|
||||
memberInjectMethods.add(im);
|
||||
}
|
||||
}
|
||||
for (MethodNode m : cn.methods) {
|
||||
@@ -57,44 +60,68 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
Set<MethodInfo> newOperatorInjectMethods) {
|
||||
LogUtil.diagnose(" Handling method %s", mn.name);
|
||||
AbstractInsnNode[] instructions = mn.instructions.toArray();
|
||||
List<MethodInfo> memberInjectMethodList = new ArrayList<MethodInfo>(memberInjectMethods);
|
||||
if (instructions.length == 0) {
|
||||
// native method (issue-52)
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
int maxStackDiff = 0;
|
||||
do {
|
||||
if (invokeOps.contains(instructions[i].getOpcode())) {
|
||||
MethodInsnNode node = (MethodInsnNode)instructions[i];
|
||||
String memberInjectMethodName = getMemberInjectMethodName(memberInjectMethodList, node);
|
||||
if (memberInjectMethodName != null) {
|
||||
// it's a member or static method and an inject method for it exist
|
||||
int rangeStart = getMemberMethodStart(instructions, i);
|
||||
if (rangeStart >= 0) {
|
||||
instructions = replaceMemberCallOps(cn, mn, memberInjectMethodName, instructions,
|
||||
node.owner, node.getOpcode(), rangeStart, i);
|
||||
i = rangeStart;
|
||||
} else {
|
||||
LogUtil.warn("Potential missed mocking at %s:%s", mn.name, getLineNum(instructions, i));
|
||||
}
|
||||
} else if (ConstPool.CONSTRUCTOR.equals(node.name)) {
|
||||
// it's a new operation
|
||||
if (ConstPool.CONSTRUCTOR.equals(node.name)) {
|
||||
LogUtil.verbose(" Line %d, constructing \"%s\" as \"%s\"", getLineNum(instructions, i),
|
||||
node.owner, node.desc);
|
||||
String newOperatorInjectMethodName = getNewOperatorInjectMethodName(newOperatorInjectMethods, node);
|
||||
if (newOperatorInjectMethodName != null) {
|
||||
// and an inject method for it exist
|
||||
// it's a new operation and an inject method for it exist
|
||||
int rangeStart = getConstructorStart(instructions, node.owner, i);
|
||||
if (rangeStart >= 0) {
|
||||
instructions = replaceNewOps(cn, mn, newOperatorInjectMethodName, instructions, rangeStart, i);
|
||||
ModifiedInsnNodes modifiedInsnNodes = replaceNewOps(cn, mn, newOperatorInjectMethodName,
|
||||
instructions, rangeStart, i);
|
||||
instructions = modifiedInsnNodes.nodes;
|
||||
maxStackDiff = Math.max(maxStackDiff, modifiedInsnNodes.stackDiff);
|
||||
i = rangeStart;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LogUtil.verbose(" Line %d, invoking \"%s\" as \"%s\"", getLineNum(instructions, i),
|
||||
node.name, node.desc);
|
||||
MethodInfo mockMethod = getMemberInjectMethodName(memberInjectMethods, node);
|
||||
if (mockMethod != null) {
|
||||
// it's a member or static method and an inject method for it exist
|
||||
int rangeStart = getMemberMethodStart(instructions, i);
|
||||
if (rangeStart >= 0) {
|
||||
ModifiedInsnNodes modifiedInsnNodes = replaceMemberCallOps(cn, mn, mockMethod,
|
||||
instructions, node.owner, node.getOpcode(), rangeStart, i);
|
||||
instructions = modifiedInsnNodes.nodes;
|
||||
maxStackDiff = Math.max(maxStackDiff, modifiedInsnNodes.stackDiff);
|
||||
i = rangeStart;
|
||||
} else {
|
||||
LogUtil.warn("Potential missed mocking at %s:%s", mn.name, getLineNum(instructions, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i++;
|
||||
} while (i < instructions.length);
|
||||
mn.maxStack += maxStackDiff;
|
||||
}
|
||||
|
||||
private String getMemberInjectMethodName(List<MethodInfo> memberInjectMethodList, MethodInsnNode node) {
|
||||
for (MethodInfo m : memberInjectMethodList) {
|
||||
/**
|
||||
* find the mock method fit for specified method node
|
||||
* @param memberInjectMethods mock methods available
|
||||
* @param node method node to match for
|
||||
* @return mock method info
|
||||
*/
|
||||
private MethodInfo getMemberInjectMethodName(Set<MethodInfo> memberInjectMethods, MethodInsnNode node) {
|
||||
for (MethodInfo m : memberInjectMethods) {
|
||||
String nodeOwner = ClassUtil.fitCompanionClassName(node.owner);
|
||||
if (m.getClazz().equals(nodeOwner) && m.getName().equals(node.name) && m.getDesc().equals(node.desc)) {
|
||||
return m.getMockName();
|
||||
String nodeName = ClassUtil.fitKotlinAccessorName(node.name);
|
||||
// Kotlin accessor method will append a extra type parameter
|
||||
String nodeDesc = nodeName.equals(node.name) ? node.desc : ClassUtil.removeFirstParameter(node.desc);
|
||||
if (m.getClazz().equals(nodeOwner) && m.getName().equals(nodeName) && m.getDesc().equals(nodeDesc)) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -157,10 +184,11 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
case Opcodes.INVOKESPECIAL:
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
case Opcodes.INVOKEINTERFACE:
|
||||
return stackEffectOfInvocation(instruction) + 1;
|
||||
return stackEffectOfInvocation(((MethodInsnNode)instruction).desc) + 1;
|
||||
case Opcodes.INVOKESTATIC:
|
||||
return stackEffectOfInvocation(((MethodInsnNode)instruction).desc);
|
||||
case Opcodes.INVOKEDYNAMIC:
|
||||
return stackEffectOfInvocation(instruction);
|
||||
return stackEffectOfInvocation(((InvokeDynamicInsnNode)instruction).desc);
|
||||
case -1:
|
||||
// either LabelNode or LineNumberNode
|
||||
return 0;
|
||||
@@ -169,14 +197,13 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private int stackEffectOfInvocation(AbstractInsnNode instruction) {
|
||||
String desc = ((MethodInsnNode)instruction).desc;
|
||||
private int stackEffectOfInvocation(String desc) {
|
||||
return ClassUtil.getParameterTypes(desc).size() - (ClassUtil.getReturnType(desc).isEmpty() ? 0 : 1);
|
||||
}
|
||||
|
||||
private AbstractInsnNode[] replaceNewOps(ClassNode cn, MethodNode mn, String newOperatorInjectMethodName,
|
||||
private ModifiedInsnNodes replaceNewOps(ClassNode cn, MethodNode mn, String newOperatorInjectMethodName,
|
||||
AbstractInsnNode[] instructions, int start, int end) {
|
||||
LogUtil.diagnose(" Line %d, mock method %s used", getLineNum(instructions, start),
|
||||
LogUtil.diagnose(" Line %d, mock method \"%s\" used", getLineNum(instructions, start),
|
||||
newOperatorInjectMethodName);
|
||||
String classType = ((TypeInsnNode)instructions[start]).desc;
|
||||
String constructorDesc = ((MethodInsnNode)instructions[end]).desc;
|
||||
@@ -186,7 +213,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
mn.instructions.remove(instructions[start]);
|
||||
mn.instructions.remove(instructions[start + 1]);
|
||||
mn.instructions.remove(instructions[end]);
|
||||
return mn.instructions.toArray();
|
||||
return new ModifiedInsnNodes(mn.instructions.toArray(), 0);
|
||||
}
|
||||
|
||||
private int getLineNum(AbstractInsnNode[] instructions, int start) {
|
||||
@@ -203,34 +230,50 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
ClassUtil.toByteCodeClassName(classType);
|
||||
}
|
||||
|
||||
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, String substitutionMethod,
|
||||
AbstractInsnNode[] instructions, String ownerClass,
|
||||
int opcode, int start, int end) {
|
||||
LogUtil.diagnose(" Line %d, mock method %s used", getLineNum(instructions, start), substitutionMethod);
|
||||
mn.maxStack++;
|
||||
MethodInsnNode method = (MethodInsnNode)instructions[end];
|
||||
private ModifiedInsnNodes replaceMemberCallOps(ClassNode cn, MethodNode mn, MethodInfo mockMethod,
|
||||
AbstractInsnNode[] instructions, String ownerClass,
|
||||
int opcode, int start, int end) {
|
||||
LogUtil.diagnose(" Line %d, mock method \"%s\" used", getLineNum(instructions, start),
|
||||
mockMethod.getMockName());
|
||||
boolean shouldAppendTypeParameter = !mockMethod.getDesc().equals(mockMethod.getMockDesc());
|
||||
String testClassName = ClassUtil.getTestClassName(cn.name);
|
||||
if (Opcodes.INVOKESTATIC == opcode || isCompanionMethod(ownerClass, opcode)) {
|
||||
// append a null value if it was a static invoke or in kotlin companion class
|
||||
mn.instructions.insertBefore(instructions[start], new InsnNode(ACONST_NULL));
|
||||
if (shouldAppendTypeParameter) {
|
||||
// append a null value if it was a static invoke or in kotlin companion class
|
||||
mn.instructions.insertBefore(instructions[start], new InsnNode(ACONST_NULL));
|
||||
}
|
||||
if (ClassUtil.isCompanionClassName(ownerClass)) {
|
||||
// for kotlin companion class, remove the byte code of reference to "companion" static field
|
||||
mn.instructions.remove(instructions[end - 1]);
|
||||
}
|
||||
} else if (!shouldAppendTypeParameter) {
|
||||
// remove extra ops code of the mocked instance, which was used as first parameter of mock method
|
||||
ImmutablePair<Integer, Integer> range = findRangeOfInvokerInstance(instructions, start, end);
|
||||
for (int i = range.left; i <= range.right; i++) {
|
||||
mn.instructions.remove(instructions[i]);
|
||||
}
|
||||
}
|
||||
// method with @MockMethod will be modified as public static access, so INVOKESTATIC is used
|
||||
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKESTATIC, testClassName,
|
||||
substitutionMethod, addFirstParameter(method.desc, ClassUtil.fitCompanionClassName(ownerClass)), false));
|
||||
mockMethod.getMockName(), mockMethod.getMockDesc(), false));
|
||||
mn.instructions.remove(instructions[end]);
|
||||
return mn.instructions.toArray();
|
||||
return new ModifiedInsnNodes(mn.instructions.toArray(), 1);
|
||||
}
|
||||
|
||||
private ImmutablePair<Integer, Integer> findRangeOfInvokerInstance(AbstractInsnNode[] nodes, int start, int end) {
|
||||
int accumulatedLevelChange = 0;
|
||||
int edgeIndex = start;
|
||||
for (int i = start; i < end; i++) {
|
||||
accumulatedLevelChange -= getStackLevelChange(nodes[i]);
|
||||
if (accumulatedLevelChange == 1) {
|
||||
edgeIndex = i;
|
||||
}
|
||||
}
|
||||
return ImmutablePair.of(start, edgeIndex);
|
||||
}
|
||||
|
||||
private boolean isCompanionMethod(String ownerClass, int opcode) {
|
||||
return Opcodes.INVOKEVIRTUAL == opcode && ClassUtil.isCompanionClassName(ownerClass);
|
||||
}
|
||||
|
||||
private String addFirstParameter(String desc, String ownerClass) {
|
||||
return "(" + ClassUtil.toByteCodeClassName(ownerClass) + desc.substring(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.util.AnnotationUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
|
||||
@@ -19,14 +21,19 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
private static final String CLASS_TESTABLE_TOOL = "com/alibaba/testable/core/tool/TestableTool";
|
||||
private static final String CLASS_TESTABLE_UTIL = "com/alibaba/testable/core/util/TestableUtil";
|
||||
private static final String CLASS_INVOKE_RECORD_UTIL = "com/alibaba/testable/core/util/InvokeRecordUtil";
|
||||
private static final String CLASS_MOCK_CONTEXT = "com/alibaba/testable/agent/model/MockContext";
|
||||
private static final String REF_TESTABLE_CONTEXT = "_testableContextReference";
|
||||
private static final String FIELD_TEST_CASE = "TEST_CASE";
|
||||
private static final String FIELD_SOURCE_METHOD = "SOURCE_METHOD";
|
||||
private static final String FIELD_MOCK_CONTEXT = "MOCK_CONTEXT";
|
||||
private static final String FIELD_PARAMETERS = "parameters";
|
||||
private static final String METHOD_CURRENT_TEST_CASE_NAME = "currentTestCaseName";
|
||||
private static final String METHOD_CURRENT_SOURCE_METHOD_NAME = "currentSourceMethodName";
|
||||
private static final String METHOD_RECORD_MOCK_INVOKE = "recordMockInvoke";
|
||||
private static final String SIGNATURE_CURRENT_TEST_CASE_NAME = "(Ljava/lang/String;)Ljava/lang/String;";
|
||||
private static final String SIGNATURE_CURRENT_SOURCE_METHOD_NAME = "()Ljava/lang/String;";
|
||||
private static final String SIGNATURE_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;Z)V";
|
||||
private static final String SIGNATURE_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;ZZ)V";
|
||||
private static final String SIGNATURE_PARAMETERS = "Ljava/util/Map;";
|
||||
|
||||
/**
|
||||
* Handle bytecode of test class
|
||||
@@ -34,15 +41,30 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
*/
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
if (wasTransformed(cn)) {
|
||||
if (wasTransformed(cn, REF_TESTABLE_CONTEXT, ClassUtil.toByteCodeClassName(CLASS_MOCK_CONTEXT))) {
|
||||
return;
|
||||
}
|
||||
for (MethodNode mn : cn.methods) {
|
||||
handleMockMethod(cn, mn);
|
||||
handleInstruction(cn, mn);
|
||||
if (mn.name.equals(ConstPool.CONSTRUCTOR)) {
|
||||
initMockContextReference(cn, mn);
|
||||
} else {
|
||||
handleMockMethod(cn, mn);
|
||||
handleInstruction(cn, mn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initMockContextReference(ClassNode cn, MethodNode mn) {
|
||||
InsnList il = new InsnList();
|
||||
il.add(new TypeInsnNode(NEW, CLASS_MOCK_CONTEXT));
|
||||
il.add(new InsnNode(DUP));
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, CLASS_MOCK_CONTEXT, "<init>", "()V", false));
|
||||
il.add(new FieldInsnNode(PUTSTATIC, cn.name, REF_TESTABLE_CONTEXT,
|
||||
ClassUtil.toByteCodeClassName(CLASS_MOCK_CONTEXT)));
|
||||
mn.instructions.insertBefore(mn.instructions.get(0), il);
|
||||
mn.maxStack++;
|
||||
}
|
||||
|
||||
private void handleMockMethod(ClassNode cn, MethodNode mn) {
|
||||
if (isMockMethod(mn)) {
|
||||
toPublicStatic(cn, mn);
|
||||
@@ -68,15 +90,17 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
if (thisRef != null) {
|
||||
mn.localVariables.remove(thisRef);
|
||||
} else {
|
||||
LogUtil.error("Fail to find `this` reference in none-static method " + getName(cn, mn));
|
||||
LogUtil.error("Fail to find `this` reference in non-static method " + getName(cn, mn));
|
||||
return;
|
||||
}
|
||||
for (AbstractInsnNode in : mn.instructions) {
|
||||
if (in.getOpcode() >= ILOAD && in.getOpcode() <= SASTORE && in instanceof VarInsnNode) {
|
||||
if (in instanceof IincInsnNode) {
|
||||
((IincInsnNode)in).var--;
|
||||
} else if (in instanceof VarInsnNode) {
|
||||
if (((VarInsnNode)in).var > 0) {
|
||||
((VarInsnNode)in).var--;
|
||||
} else if (in.getOpcode() == ALOAD) {
|
||||
LogUtil.error("Attempt to access none-static member in mock method " + getName(cn, mn));
|
||||
LogUtil.error("Attempt to access non-static member in mock method " + getName(cn, mn));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -119,7 +143,8 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
|
||||
private boolean isTestableUtilField(FieldInsnNode fieldInsnNode) {
|
||||
return fieldInsnNode.owner.equals(CLASS_TESTABLE_TOOL) &&
|
||||
(fieldInsnNode.name.equals(FIELD_TEST_CASE) || fieldInsnNode.name.equals(FIELD_SOURCE_METHOD));
|
||||
(fieldInsnNode.name.equals(FIELD_TEST_CASE) || fieldInsnNode.name.equals(FIELD_SOURCE_METHOD) ||
|
||||
fieldInsnNode.name.equals(FIELD_MOCK_CONTEXT));
|
||||
}
|
||||
|
||||
private AbstractInsnNode[] replaceTestableUtilField(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions,
|
||||
@@ -132,6 +157,10 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
} else if (FIELD_SOURCE_METHOD.equals(fieldName)) {
|
||||
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_CURRENT_SOURCE_METHOD_NAME,
|
||||
SIGNATURE_CURRENT_SOURCE_METHOD_NAME, false));
|
||||
} else if (FIELD_MOCK_CONTEXT.equals(fieldName)) {
|
||||
il.add(new FieldInsnNode(GETSTATIC, cn.name, REF_TESTABLE_CONTEXT,
|
||||
ClassUtil.toByteCodeClassName(CLASS_MOCK_CONTEXT)));
|
||||
il.add(new FieldInsnNode(GETFIELD, CLASS_MOCK_CONTEXT, FIELD_PARAMETERS, SIGNATURE_PARAMETERS));
|
||||
}
|
||||
if (il.size() > 0) {
|
||||
mn.instructions.insert(instructions[pos], il);
|
||||
@@ -145,7 +174,7 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
List<Byte> types = ClassUtil.getParameterTypes(mn.desc);
|
||||
int size = types.size();
|
||||
int parameterOffset = 0;
|
||||
mn.maxStack += 1;
|
||||
mn.maxStack += 2;
|
||||
il.add(getIntInsn(size));
|
||||
il.add(new TypeInsnNode(ANEWARRAY, ClassUtil.CLASS_OBJECT));
|
||||
for (int i = 0; i < size; i++) {
|
||||
@@ -166,6 +195,11 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
} else {
|
||||
il.add(new InsnNode(ICONST_0));
|
||||
}
|
||||
if (isTargetClassInParameter(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_RECORDER_METHOD, false));
|
||||
mn.instructions.insertBefore(mn.instructions.get(0), il);
|
||||
@@ -173,18 +207,32 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
|
||||
private boolean isMockForConstructor(MethodNode mn) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.MOCK_CONSTRUCTOR)) {
|
||||
return true;
|
||||
}
|
||||
String method = AnnotationUtil.getAnnotationParameter
|
||||
(an, ConstPool.FIELD_TARGET_METHOD, null, String.class);
|
||||
if (ConstPool.CONSTRUCTOR.equals(method)) {
|
||||
String annotationName = toDotSeparateFullClassName(an.desc);
|
||||
if (ConstPool.MOCK_CONSTRUCTOR.equals(annotationName)) {
|
||||
return true;
|
||||
} else if (ConstPool.MOCK_METHOD.equals(annotationName)) {
|
||||
String method = AnnotationUtil.getAnnotationParameter
|
||||
(an, ConstPool.FIELD_TARGET_METHOD, null, String.class);
|
||||
if (ConstPool.CONSTRUCTOR.equals(method)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isTargetClassInParameter(MethodNode mn) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (ConstPool.MOCK_METHOD.equals(toDotSeparateFullClassName(an.desc))) {
|
||||
Type type = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_CLASS, null, Type.class);
|
||||
if (type != null && !type.getClassName().equals(NullType.class.getName())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ImmutablePair<Integer, Integer> getLoadParameterByteCode(Byte type) {
|
||||
switch (type) {
|
||||
case ClassUtil.TYPE_BYTE:
|
||||
|
||||
@@ -13,20 +13,25 @@ public class MethodInfo {
|
||||
* name of the source method
|
||||
*/
|
||||
private final String name;
|
||||
/**
|
||||
* parameter and return value of the source method
|
||||
*/
|
||||
private final String desc;
|
||||
/**
|
||||
* name of the mock method
|
||||
*/
|
||||
private final String mockName;
|
||||
/**
|
||||
* parameter and return value of the source method
|
||||
* parameter and return value of the mock method
|
||||
*/
|
||||
private final String desc;
|
||||
private final String mockDesc;
|
||||
|
||||
public MethodInfo(String clazz, String name, String mockName, String desc) {
|
||||
public MethodInfo(String clazz, String name, String desc, String mockName, String mockDesc) {
|
||||
this.clazz = clazz;
|
||||
this.name = name;
|
||||
this.mockName = mockName;
|
||||
this.desc = desc;
|
||||
this.mockName = mockName;
|
||||
this.mockDesc = mockDesc;
|
||||
}
|
||||
|
||||
public String getClazz() {
|
||||
@@ -37,12 +42,16 @@ public class MethodInfo {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return desc;
|
||||
}
|
||||
|
||||
public String getMockName() {
|
||||
return mockName;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return desc;
|
||||
public String getMockDesc() {
|
||||
return mockDesc;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,16 +63,18 @@ public class MethodInfo {
|
||||
|
||||
if (!clazz.equals(that.clazz)) { return false; }
|
||||
if (!name.equals(that.name)) { return false; }
|
||||
if (!desc.equals(that.desc)) { return false; }
|
||||
if (!mockName.equals(that.mockName)) { return false; }
|
||||
return desc.equals(that.desc);
|
||||
return mockDesc.equals(that.mockDesc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = clazz.hashCode();
|
||||
result = 31 * result + name.hashCode();
|
||||
result = 31 * result + mockName.hashCode();
|
||||
result = 31 * result + desc.hashCode();
|
||||
result = 31 * result + mockName.hashCode();
|
||||
result = 31 * result + mockDesc.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.alibaba.testable.agent.model;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class MockContext {
|
||||
|
||||
public Map<String, Object> parameters = new HashMap<String, Object>();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.alibaba.testable.agent.model;
|
||||
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
|
||||
public class ModifiedInsnNodes {
|
||||
|
||||
public AbstractInsnNode[] nodes;
|
||||
|
||||
public int stackDiff;
|
||||
|
||||
public ModifiedInsnNodes(AbstractInsnNode[] nodes, int stackDiff) {
|
||||
this.nodes = nodes;
|
||||
this.stackDiff = stackDiff;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,26 +3,31 @@ package com.alibaba.testable.agent.transformer;
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.handler.SourceClassHandler;
|
||||
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.ImmutablePair;
|
||||
import com.alibaba.testable.agent.util.AnnotationUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.agent.util.GlobalConfig;
|
||||
import com.alibaba.testable.agent.util.StringUtil;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import com.alibaba.testable.core.model.MockDiagnose;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.DOT;
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.SLASH;
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
|
||||
|
||||
/**
|
||||
@@ -31,6 +36,7 @@ import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassNa
|
||||
public class TestableClassTransformer implements ClassFileTransformer {
|
||||
|
||||
private static final String FIELD_DIAGNOSE = "diagnose";
|
||||
private static final String COMMA = ",";
|
||||
|
||||
/**
|
||||
* Just avoid spend time to scan those surely non-user classes
|
||||
@@ -52,21 +58,21 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
try {
|
||||
if (shouldTransformAsSourceClass(className)) {
|
||||
// it's a source class with testable enabled
|
||||
LogUtil.diagnose("Handling source class %s", className);
|
||||
List<MethodInfo> injectMethods = getTestableMockMethods(ClassUtil.getTestClassName(className));
|
||||
LogUtil.diagnose("Handling source class %s", className);
|
||||
bytes = new SourceClassHandler(injectMethods).getBytes(classFileBuffer);
|
||||
dumpByte(className, bytes);
|
||||
resetMockContext();
|
||||
} else if (shouldTransformAsTestClass(className)) {
|
||||
// it's a test class with testable enabled
|
||||
LogUtil.diagnose("Handling test class %s", className);
|
||||
bytes = new TestClassHandler().getBytes(classFileBuffer);
|
||||
dumpByte(className, bytes);
|
||||
resetMockContext();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
LogUtil.warn("Failed to transform class " + className);
|
||||
LogUtil.diagnose(t.toString());
|
||||
} finally {
|
||||
LogUtil.resetLogLevel();
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
@@ -77,7 +83,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String dumpFile = StringUtil.joinPath(dumpDir, className.replaceAll("/", ".") + ".class");
|
||||
String dumpFile = StringUtil.joinPath(dumpDir, className.replaceAll(SLASH, DOT) + ".class");
|
||||
LogUtil.verbose("Dump class: " + dumpFile);
|
||||
FileOutputStream stream = new FileOutputStream(dumpFile);
|
||||
stream.write(bytes);
|
||||
@@ -100,14 +106,25 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
if (null == className) {
|
||||
return true;
|
||||
}
|
||||
for (String prefix : WHITELIST_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return false;
|
||||
String whitePrefix = GlobalConfig.getPkgPrefix();
|
||||
if (whitePrefix != null) {
|
||||
for (String prefix : whitePrefix.split(COMMA)) {
|
||||
if (className.startsWith(prefix)) {
|
||||
// Only consider package in provided list as non-system class
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String prefix : BLACKLIST_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return true;
|
||||
return true;
|
||||
} else {
|
||||
for (String prefix : WHITELIST_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (String prefix : BLACKLIST_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -129,37 +146,52 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
}
|
||||
|
||||
private void checkMethodAnnotation(ClassNode cn, List<MethodInfo> methodInfos, MethodNode mn) {
|
||||
ImmutablePair<String, String> methodDescPair = extractFirstParameter(mn.desc);
|
||||
if (methodDescPair == null || mn.visibleAnnotations == null) {
|
||||
if (mn.visibleAnnotations == null) {
|
||||
return;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
String fullClassName = toDotSeparateFullClassName(an.desc);
|
||||
if (fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
|
||||
addMockConstructor(cn, methodInfos, mn);
|
||||
LogUtil.verbose(" Mock constructor \"%s\" as \"(%s)V\" for \"%s\"", mn.name,
|
||||
ClassUtil.extractParameters(mn.desc), ClassUtil.getReturnType(mn.desc));
|
||||
addMockConstructor(methodInfos, cn, mn);
|
||||
} else if (fullClassName.equals(ConstPool.MOCK_METHOD) ||
|
||||
fullClassName.equals(ConstPool.TESTABLE_MOCK)) {
|
||||
LogUtil.verbose(" Mock method \"%s\" as \"%s\"", mn.name, mn.desc);
|
||||
String targetMethod = AnnotationUtil.getAnnotationParameter(
|
||||
an, ConstPool.FIELD_TARGET_METHOD, mn.name, String.class);
|
||||
if (targetMethod.equals(ConstPool.CONSTRUCTOR)) {
|
||||
addMockConstructor(cn, methodInfos, mn);
|
||||
if (ConstPool.CONSTRUCTOR.equals(targetMethod)) {
|
||||
addMockConstructor(methodInfos, cn, mn);
|
||||
} else {
|
||||
addMockMethod(methodInfos, mn, methodDescPair, targetMethod);
|
||||
MethodInfo mi = getMethodInfo(mn, an, targetMethod);
|
||||
if (mi != null) {
|
||||
methodInfos.add(mi);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addMockMethod(List<MethodInfo> methodInfos, MethodNode mn,
|
||||
ImmutablePair<String, String> methodDescPair, String targetMethod) {
|
||||
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
|
||||
methodInfos.add(new MethodInfo(targetClass, targetMethod, mn.name, methodDescPair.right));
|
||||
private MethodInfo getMethodInfo(MethodNode mn, AnnotationNode an, String targetMethod) {
|
||||
Type targetType = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_CLASS, null, Type.class);
|
||||
if (targetType == null || targetType.getClassName().equals(NullType.class.getName())) {
|
||||
// "targetClass" unset, use first parameter as target class type
|
||||
ImmutablePair<String, String> methodDescPair = extractFirstParameter(mn.desc);
|
||||
if (methodDescPair == null) {
|
||||
return null;
|
||||
}
|
||||
return new MethodInfo(methodDescPair.left, targetMethod, methodDescPair.right, mn.name, mn.desc);
|
||||
} else {
|
||||
// "targetClass" found, use it as target class type
|
||||
String slashSeparatedName = ClassUtil.toSlashSeparatedName(targetType.getClassName());
|
||||
return new MethodInfo(slashSeparatedName, targetMethod, mn.desc, mn.name, mn.desc);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMockConstructor(ClassNode cn, List<MethodInfo> methodInfos, MethodNode mn) {
|
||||
private void addMockConstructor(List<MethodInfo> methodInfos, ClassNode cn, MethodNode mn) {
|
||||
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
|
||||
methodInfos.add(new MethodInfo(sourceClassName, ConstPool.CONSTRUCTOR, mn.name, mn.desc));
|
||||
methodInfos.add(new MethodInfo(sourceClassName, ConstPool.CONSTRUCTOR, mn.desc, mn.name, mn.desc));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,14 +233,11 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
private void setupMockContext(AnnotationNode an) {
|
||||
MockDiagnose diagnose = AnnotationUtil.getAnnotationParameter(an, FIELD_DIAGNOSE, null, MockDiagnose.class);
|
||||
if (diagnose != null) {
|
||||
LogUtil.enableDiagnose(diagnose == MockDiagnose.ENABLE);
|
||||
LogUtil.setLevel(diagnose == MockDiagnose.ENABLE ? LogUtil.LogLevel.LEVEL_DIAGNOSE :
|
||||
(diagnose == MockDiagnose.VERBOSE ? LogUtil.LogLevel.LEVEL_VERBOSE : LogUtil.LogLevel.LEVEL_MUTE));
|
||||
}
|
||||
}
|
||||
|
||||
private void resetMockContext() {
|
||||
LogUtil.resetLogLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Split desc to "first parameter" and "desc of rest parameters"
|
||||
* @param desc method desc
|
||||
@@ -216,7 +245,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
private ImmutablePair<String, String> extractFirstParameter(String desc) {
|
||||
// assume first parameter is a class
|
||||
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(2, pos), "(" + desc.substring(pos + 1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,7 +29,11 @@ public class AnnotationUtil {
|
||||
Class<? extends Enum> enumClazz = (Class<? extends Enum>)clazz;
|
||||
return (T)Enum.valueOf(enumClazz, values[1]);
|
||||
}
|
||||
return clazz.cast(an.values.get(i + 1));
|
||||
try {
|
||||
return clazz.cast(an.values.get(i + 1));
|
||||
} catch (ClassCastException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public class ClassUtil {
|
||||
* @return is companion class or not
|
||||
*/
|
||||
public static boolean isCompanionClassName(String name) {
|
||||
return name.endsWith("$Companion");
|
||||
return name.endsWith(ConstPool.KOTLIN_POSTFIX_COMPANION);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +70,18 @@ public class ClassUtil {
|
||||
* @return original name
|
||||
*/
|
||||
public static String fitCompanionClassName(String name) {
|
||||
return name.replaceAll("\\$Companion$", "");
|
||||
return isCompanionClassName(name) ?
|
||||
name.substring(0, name.length() - ConstPool.KOTLIN_POSTFIX_COMPANION.length()) : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* fit kotlin accessor method name to original name
|
||||
* @param name a accessor name (which could be a common kotlin method)
|
||||
* @return original name
|
||||
*/
|
||||
public static String fitKotlinAccessorName(String name) {
|
||||
return name.startsWith(ConstPool.KOTLIN_PREFIX_ACCESS) ?
|
||||
name.substring(ConstPool.KOTLIN_PREFIX_ACCESS.length()) : name;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,6 +90,11 @@ public class ClassUtil {
|
||||
* @return test class name
|
||||
*/
|
||||
public static String getTestClassName(String sourceClassName) {
|
||||
// check the class if generated by CGLIB
|
||||
int idx = sourceClassName.indexOf(ConstPool.CGLIB_CLASS_INFIX);
|
||||
if (idx != -1) {
|
||||
sourceClassName = sourceClassName.substring(0, idx);
|
||||
}
|
||||
return sourceClassName + ConstPool.TEST_POSTFIX;
|
||||
}
|
||||
|
||||
@@ -99,17 +115,23 @@ public class ClassUtil {
|
||||
public static List<Byte> getParameterTypes(String desc) {
|
||||
List<Byte> parameterTypes = new ArrayList<Byte>();
|
||||
boolean travelingClass = false;
|
||||
boolean travelingArray = false;
|
||||
for (byte b : desc.getBytes()) {
|
||||
if (travelingClass) {
|
||||
if (b == CLASS_END) {
|
||||
travelingClass = false;
|
||||
travelingArray = false;
|
||||
}
|
||||
} else {
|
||||
if (isPrimaryType(b)) {
|
||||
parameterTypes.add(b);
|
||||
// should treat primary array as class (issue-48)
|
||||
parameterTypes.add(travelingArray ? TYPE_CLASS : b);
|
||||
travelingArray = false;
|
||||
} else if (b == TYPE_CLASS) {
|
||||
travelingClass = true;
|
||||
parameterTypes.add(b);
|
||||
} else if (b == TYPE_ARRAY) {
|
||||
travelingArray = true;
|
||||
} else if (b == PARAM_END) {
|
||||
break;
|
||||
}
|
||||
@@ -118,6 +140,16 @@ public class ClassUtil {
|
||||
return parameterTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* extract parameter part of method desc
|
||||
* @param desc method description
|
||||
* @return parameter value
|
||||
*/
|
||||
public static String extractParameters(String desc) {
|
||||
int returnTypeEdge = desc.lastIndexOf(PARAM_END);
|
||||
return desc.substring(1, returnTypeEdge);
|
||||
}
|
||||
|
||||
/**
|
||||
* parse method desc, fetch return value types
|
||||
* @param desc method description
|
||||
@@ -193,6 +225,15 @@ public class ClassUtil {
|
||||
return toSlashSeparatedName(className).substring(1, className.length() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* remove first parameter from method descriptor
|
||||
* @param desc original descriptor
|
||||
* @return descriptor without first parameter
|
||||
*/
|
||||
public static String removeFirstParameter(String desc) {
|
||||
return "(" + desc.substring(desc.indexOf(";") + 1);
|
||||
}
|
||||
|
||||
private static String toDescriptor(Byte type, String objectType) {
|
||||
return "(" + (char)type.byteValue() + ")L" + objectType + ";";
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ public class GlobalConfig {
|
||||
private static final String VERBOSE = "verbose";
|
||||
|
||||
private static String dumpPath = null;
|
||||
private static String pkgPrefix = null;
|
||||
|
||||
public static boolean setLogLevel(String level) {
|
||||
if (level.equals(MUTE)) {
|
||||
@@ -35,4 +36,11 @@ public class GlobalConfig {
|
||||
return dumpPath;
|
||||
}
|
||||
|
||||
public static String getPkgPrefix() {
|
||||
return pkgPrefix;
|
||||
}
|
||||
|
||||
public static void setPkgPrefix(String pkgPrefix) {
|
||||
GlobalConfig.pkgPrefix = pkgPrefix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
@@ -26,7 +28,7 @@ public class StringUtil {
|
||||
* @return joined full file path
|
||||
*/
|
||||
public static String joinPath(String folder, String file) {
|
||||
return (folder.endsWith("/") ? folder : (folder + "/")) + file;
|
||||
return (folder.endsWith(File.separator) ? folder : (folder + File.separator)) + file;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.alibaba.testable.agent.transformer;
|
||||
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TestableClassTransformerTest {
|
||||
|
||||
private TestableClassTransformer testableClassTransformer = new TestableClassTransformer();
|
||||
|
||||
@Test
|
||||
void should_split_parameters() {
|
||||
ImmutablePair<String, String> parameters =
|
||||
PrivateAccessor.invoke(testableClassTransformer, "extractFirstParameter", "()");
|
||||
assertNull(parameters);
|
||||
parameters = PrivateAccessor.invoke(testableClassTransformer, "extractFirstParameter", "(Lcom.alibaba.demo.Class;ILjava.lang.String;Z)");
|
||||
assertNotNull(parameters);
|
||||
assertEquals("com.alibaba.demo.Class", parameters.left);
|
||||
assertEquals("(ILjava.lang.String;Z)", parameters.right);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,12 @@ class ClassUtilTest {
|
||||
assertEquals(3, ClassUtil.getParameterTypes("(Ljava/lang/String;[I[Ljava/lang/String;)V").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_extract_parameter() {
|
||||
assertEquals("", ClassUtil.extractParameters("()I"));
|
||||
assertEquals("Ljava/lang/String;", ClassUtil.extractParameters("(Ljava/lang/String;)I"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_return_type() {
|
||||
assertEquals("", ClassUtil.getReturnType("(Ljava/lang/String;)V"));
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.5</version>
|
||||
<version>0.4.11</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-all</artifactId>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.5</version>
|
||||
<version>0.4.11</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-core</artifactId>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.alibaba.testable.core.annotation;
|
||||
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
@@ -18,4 +19,10 @@ public @interface MockMethod {
|
||||
*/
|
||||
String targetMethod() default "";
|
||||
|
||||
/**
|
||||
* explicit set target class instead of adding to parameter list
|
||||
* @return target class type
|
||||
*/
|
||||
Class<?> targetClass() default NullType.class;
|
||||
|
||||
}
|
||||
|
||||
@@ -14,6 +14,11 @@ public enum MockDiagnose {
|
||||
/**
|
||||
* Print diagnose logs
|
||||
*/
|
||||
ENABLE
|
||||
ENABLE,
|
||||
|
||||
/**
|
||||
* Print verbose log
|
||||
*/
|
||||
VERBOSE
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.alibaba.testable.core.tool;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
@@ -7,7 +9,9 @@ public class TestableTool {
|
||||
|
||||
/**
|
||||
* Name of current test case method
|
||||
* @deprecated prefer using `MOCK_CONTEXT` to distinguish test cases
|
||||
*/
|
||||
@Deprecated
|
||||
public static String TEST_CASE;
|
||||
|
||||
/**
|
||||
@@ -15,4 +19,9 @@ public class TestableTool {
|
||||
*/
|
||||
public static String SOURCE_METHOD;
|
||||
|
||||
/**
|
||||
* Inject extra mock parameters
|
||||
*/
|
||||
public static Map<String, Object> MOCK_CONTEXT;
|
||||
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ public class InvokeRecordUtil {
|
||||
* Record mock method invoke event
|
||||
* @param args invocation parameters
|
||||
* @param isConstructor whether mocked method is constructor
|
||||
* @param isTargetClassInParameter whether use first parameter as target class
|
||||
*/
|
||||
public static void recordMockInvoke(Object[] args, boolean isConstructor) {
|
||||
public static void recordMockInvoke(Object[] args, boolean isConstructor, boolean isTargetClassInParameter) {
|
||||
StackTraceElement mockMethodTraceElement = Thread.currentThread().getStackTrace()[INDEX_OF_TEST_CLASS];
|
||||
String mockMethodName = mockMethodTraceElement.getMethodName();
|
||||
String testClass = mockMethodTraceElement.getClassName();
|
||||
@@ -37,7 +38,7 @@ public class InvokeRecordUtil {
|
||||
records.add(args);
|
||||
LogUtil.verbose(" Mock constructor invoked \"%s\"", identify);
|
||||
} else {
|
||||
records.add(slice(args, 1));
|
||||
records.add(isTargetClassInParameter ? slice(args, 1) : args);
|
||||
LogUtil.verbose(" Mock method invoked \"%s\"", identify);
|
||||
}
|
||||
INVOKE_RECORDS.put(identify, records);
|
||||
|
||||
@@ -51,11 +51,11 @@ public class LogUtil {
|
||||
}
|
||||
|
||||
public static void error(String msg, Object... args) {
|
||||
System.err.println(String.format("[FAIL] " + msg, args));
|
||||
System.err.println(String.format("[ERROR] " + msg, args));
|
||||
}
|
||||
|
||||
public static void enableDiagnose(boolean enable) {
|
||||
currentLogLevel = enable ? LogLevel.LEVEL_DIAGNOSE : LogLevel.LEVEL_MUTE;
|
||||
public static void setLevel(LogLevel level) {
|
||||
currentLogLevel = level;
|
||||
}
|
||||
|
||||
public static void setDefaultLevel(LogLevel level) {
|
||||
|
||||
@@ -15,7 +15,8 @@ public class TypeUtil {
|
||||
public static Class<?>[] getClassesFromObjects(Object[] parameterObjects) {
|
||||
Class<?>[] cs = new Class[parameterObjects.length];
|
||||
for (int i = 0; i < cs.length; i++) {
|
||||
cs[i] = parameterObjects[i].getClass();
|
||||
Object pObj = parameterObjects[i];
|
||||
cs[i] = (pObj == null) ? null : pObj.getClass();
|
||||
}
|
||||
return cs;
|
||||
}
|
||||
@@ -41,7 +42,7 @@ public class TypeUtil {
|
||||
/**
|
||||
* type equals
|
||||
* @param classesLeft class to be compared
|
||||
* @param classesRight class to compare
|
||||
* @param classesRight class to compare (item can be null)
|
||||
* @return whether all class equals
|
||||
*/
|
||||
private static boolean typeEquals(Class<?>[] classesLeft, Class<?>[] classesRight) {
|
||||
@@ -49,6 +50,9 @@ public class TypeUtil {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < classesLeft.length; i++) {
|
||||
if (classesRight[i] == null) {
|
||||
return !classesLeft[i].isPrimitive();
|
||||
}
|
||||
if (!classesLeft[i].isAssignableFrom(classesRight[i]) && !fuzzyEqual(classesLeft[i], classesRight[i])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.5</version>
|
||||
<version>0.4.11</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.alibaba.testable;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.apache.maven.artifact.Artifact;
|
||||
import org.apache.maven.plugin.AbstractMojo;
|
||||
import org.apache.maven.plugin.MojoExecutionException;
|
||||
@@ -45,6 +46,12 @@ public class TestableMojo extends AbstractMojo
|
||||
@Parameter
|
||||
private String dumpPath;
|
||||
|
||||
/**
|
||||
* Package prefixes of class need to be mocked (comma split)
|
||||
*/
|
||||
@Parameter
|
||||
private String pkgPrefix;
|
||||
|
||||
/**
|
||||
* Name of the Testable Agent artifact.
|
||||
*/
|
||||
@@ -71,13 +78,17 @@ public class TestableMojo extends AbstractMojo
|
||||
getLog().error("failed to fetch project properties");
|
||||
return;
|
||||
}
|
||||
|
||||
String extraArgs = "";
|
||||
if (logLevel != null && !logLevel.isEmpty()) {
|
||||
if (!Strings.isNullOrEmpty(logLevel)) {
|
||||
extraArgs += "&logLevel=" + logLevel;
|
||||
}
|
||||
if (dumpPath != null && !dumpPath.isEmpty()) {
|
||||
if (!Strings.isNullOrEmpty(dumpPath)) {
|
||||
extraArgs += "&dumpPath=" + dumpPath;
|
||||
}
|
||||
if (!Strings.isNullOrEmpty(pkgPrefix)) {
|
||||
extraArgs += "&pkgPrefix=" + pkgPrefix;
|
||||
}
|
||||
final String oldArgs = projectProperties.getProperty(testArgsPropertyKey);
|
||||
String newArgs = (oldArgs == null) ? getAgentJarArgs().trim() : (oldArgs + getAgentJarArgs());
|
||||
if (!extraArgs.isEmpty()) {
|
||||
@@ -91,6 +102,7 @@ public class TestableMojo extends AbstractMojo
|
||||
final Artifact testableAgentArtifact = pluginArtifactMap.get(AGENT_ARTIFACT_NAME);
|
||||
if (testableAgentArtifact == null) {
|
||||
getLog().error("failed to find testable agent jar");
|
||||
return "";
|
||||
}
|
||||
return " -javaagent:" + testableAgentArtifact.getFile().getAbsolutePath();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.5</version>
|
||||
<version>0.4.11</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>testable-parent</name>
|
||||
<description>Unit test enhancement toolkit</description>
|
||||
@@ -42,7 +42,7 @@
|
||||
<plugin.gpg.version>1.6</plugin.gpg.version>
|
||||
<plugin.staging.version>1.6.8</plugin.staging.version>
|
||||
<plugin.maven.version>3.6.0</plugin.maven.version>
|
||||
<testable.version>0.4.5</testable.version>
|
||||
<testable.version>0.4.11</testable.version>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.5</version>
|
||||
<version>0.4.11</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-processor</artifactId>
|
||||
|
||||
@@ -7,11 +7,13 @@ import com.alibaba.testable.processor.translator.EnablePrivateAccessTranslator;
|
||||
import com.alibaba.testable.processor.util.JavacUtil;
|
||||
import com.alibaba.testable.processor.util.TestableLogger;
|
||||
import com.sun.tools.javac.api.JavacTrees;
|
||||
import com.sun.tools.javac.code.Attribute;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
|
||||
import com.sun.tools.javac.tree.JCTree;
|
||||
import com.sun.tools.javac.tree.TreeMaker;
|
||||
import com.sun.tools.javac.util.Names;
|
||||
import com.sun.tools.javac.util.Pair;
|
||||
|
||||
import javax.annotation.processing.AbstractProcessor;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
@@ -19,7 +21,6 @@ import javax.annotation.processing.RoundEnvironment;
|
||||
import javax.annotation.processing.SupportedAnnotationTypes;
|
||||
import javax.lang.model.SourceVersion;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.Name;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -29,6 +30,8 @@ import java.util.Set;
|
||||
@SupportedAnnotationTypes("com.alibaba.testable.processor.annotation.EnablePrivateAccess")
|
||||
public class EnablePrivateAccessProcessor extends AbstractProcessor {
|
||||
|
||||
private static final String SRC_CLASS = "srcClass";
|
||||
|
||||
private TestableContext cx;
|
||||
|
||||
@Override
|
||||
@@ -53,8 +56,10 @@ public class EnablePrivateAccessProcessor extends AbstractProcessor {
|
||||
}
|
||||
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(EnablePrivateAccess.class);
|
||||
for (Element element : elements) {
|
||||
if (element.getKind().isClass() && isTestClass(element.getSimpleName())) {
|
||||
processClassElement((Symbol.ClassSymbol)element);
|
||||
if (element.getKind().isClass()) {
|
||||
Symbol.ClassSymbol testClass = (Symbol.ClassSymbol)element;
|
||||
String sourceClassName = getSourceClassName(testClass);
|
||||
processClassElement(testClass, sourceClassName);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -66,6 +71,19 @@ public class EnablePrivateAccessProcessor extends AbstractProcessor {
|
||||
return SourceVersion.values()[SourceVersion.values().length - 1];
|
||||
}
|
||||
|
||||
private String getSourceClassName(Symbol.ClassSymbol testClass) {
|
||||
for (Attribute.Compound annotation : testClass.getMetadata().getDeclarationAttributes()) {
|
||||
if (ConstPool.ENABLE_PRIVATE_ACCESS.equals(annotation.type.tsym.toString())) {
|
||||
for (Pair<Symbol.MethodSymbol, Attribute> p : annotation.values) {
|
||||
if (SRC_CLASS.equals(p.fst.name.toString())) {
|
||||
return p.snd.getValue().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JavacProcessingEnvironment getJavacProcessingEnvironment(ProcessingEnvironment processingEnv) {
|
||||
try {
|
||||
return JavacUtil.getJavacProcessingEnvironment(processingEnv);
|
||||
@@ -74,13 +92,11 @@ public class EnablePrivateAccessProcessor extends AbstractProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTestClass(Name name) {
|
||||
return name.toString().endsWith(ConstPool.TEST_POSTFIX);
|
||||
}
|
||||
|
||||
private void processClassElement(Symbol.ClassSymbol clazz) {
|
||||
JCTree tree = cx.trees.getTree(clazz);
|
||||
tree.accept(new EnablePrivateAccessTranslator(clazz, cx));
|
||||
private void processClassElement(Symbol.ClassSymbol testClass, String sourceClassName) {
|
||||
if (cx.trees != null) {
|
||||
JCTree tree = cx.trees.getTree(testClass);
|
||||
tree.accept(new EnablePrivateAccessTranslator(cx, testClass, sourceClassName));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.alibaba.testable.processor.annotation;
|
||||
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
@@ -11,4 +12,11 @@ import java.lang.annotation.*;
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
public @interface EnablePrivateAccess {
|
||||
|
||||
/**
|
||||
* explicit specify the source class to be tested
|
||||
* @return
|
||||
*/
|
||||
Class<?> srcClass() default NullType.class;
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package com.alibaba.testable.processor.constant;
|
||||
*/
|
||||
public final class ConstPool {
|
||||
|
||||
public static final String ENABLE_PRIVATE_ACCESS = "com.alibaba.testable.processor.annotation.EnablePrivateAccess";
|
||||
public static final String TESTABLE_PRIVATE_ACCESSOR = "com.alibaba.testable.core.accessor.PrivateAccessor";
|
||||
public static final String TEST_POSTFIX = "Test";
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.alibaba.testable.processor.exception;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class MemberNotExistException extends RuntimeException {
|
||||
|
||||
public MemberNotExistException(String type, String className, String target) {
|
||||
super(String.format("%s \"%s\" not exist in class \"%s\"", type, target, className));
|
||||
}
|
||||
|
||||
public MemberNotExistException(String type, String className, String target, int count) {
|
||||
super(String.format("%s \"%s\" with %d %s not exist in class \"%s\"",
|
||||
type, target, count, parameters(count), className));
|
||||
}
|
||||
|
||||
private static String parameters(int count) {
|
||||
return count > 1 ? "parameters" : "parameter";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.alibaba.testable.processor.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class MemberRecord {
|
||||
|
||||
/**
|
||||
* Record private and final fields
|
||||
*/
|
||||
public final List<String> privateOrFinalFields = new ArrayList<String>();
|
||||
/**
|
||||
* Record non-private fields
|
||||
*/
|
||||
public final List<String> nonPrivateNorFinalFields = new ArrayList<String>();
|
||||
/**
|
||||
* Record private methods and possible parameter counts (negative number means large or equals)
|
||||
*/
|
||||
public final Map<String, List<Integer>> privateMethods = new HashMap<String, List<Integer>>();
|
||||
/**
|
||||
* Record non-private methods and possible parameter counts (negative number means large or equals)
|
||||
*/
|
||||
public final Map<String, List<Integer>> nonPrivateMethods = new HashMap<String, List<Integer>>();
|
||||
|
||||
}
|
||||
@@ -13,8 +13,8 @@ public enum MemberType {
|
||||
STATIC_PRIVATE,
|
||||
|
||||
/**
|
||||
* None private member
|
||||
* Non-private member
|
||||
*/
|
||||
NONE_PRIVATE
|
||||
NON_PRIVATE
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
package com.alibaba.testable.processor.translator;
|
||||
|
||||
import com.alibaba.testable.processor.constant.ConstPool;
|
||||
import com.alibaba.testable.processor.generator.PrivateAccessStatementGenerator;
|
||||
import com.alibaba.testable.processor.model.MemberRecord;
|
||||
import com.alibaba.testable.processor.model.MemberType;
|
||||
import com.alibaba.testable.processor.model.TestableContext;
|
||||
import com.alibaba.testable.processor.util.PathUtil;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import com.sun.tools.javac.tree.JCTree.*;
|
||||
import com.sun.tools.javac.util.ListBuffer;
|
||||
import com.sun.tools.javac.util.Name;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.alibaba.testable.processor.constant.ConstPool.TEST_POSTFIX;
|
||||
|
||||
/**
|
||||
* Travel AST
|
||||
@@ -24,6 +32,8 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
|
||||
private static final String IDEA_PATHS_SELECTOR = "idea.paths.selector";
|
||||
private static final String USER_DIR = "user.dir";
|
||||
private static final String GRADLE_CLASS_FOLDER = "/build/classes/java/main/";
|
||||
private static final String MAVEN_CLASS_FOLDER = "/target/classes/";
|
||||
|
||||
/**
|
||||
* Name of source class
|
||||
@@ -34,67 +44,45 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
*/
|
||||
private final ListBuffer<Name> sourceClassIns = new ListBuffer<Name>();
|
||||
/**
|
||||
* Record private and final fields
|
||||
* Member information of source class
|
||||
*/
|
||||
private final ListBuffer<String> privateOrFinalFields = new ListBuffer<String>();
|
||||
/**
|
||||
* Record private methods
|
||||
*/
|
||||
private final ListBuffer<String> privateMethods = new ListBuffer<String>();
|
||||
private final MemberRecord memberRecord = new MemberRecord();
|
||||
|
||||
private final PrivateAccessStatementGenerator privateAccessStatementGenerator;
|
||||
private final PrivateAccessChecker privateAccessChecker;
|
||||
|
||||
public EnablePrivateAccessTranslator(Symbol.ClassSymbol clazz, TestableContext cx) {
|
||||
String pkgName = ((Symbol.PackageSymbol)clazz.owner).fullname.toString();
|
||||
String testClassName = clazz.getSimpleName().toString();
|
||||
String sourceClass = testClassName.substring(0, testClassName.length() - ConstPool.TEST_POSTFIX.length());
|
||||
public EnablePrivateAccessTranslator(TestableContext cx, Symbol.ClassSymbol clazz, String srcClassName) {
|
||||
String sourceClassFullName;
|
||||
if (srcClassName == null) {
|
||||
String testClassFullName = clazz.fullname.toString();
|
||||
sourceClassFullName = testClassFullName.substring(0, testClassFullName.length() - TEST_POSTFIX.length());
|
||||
} else {
|
||||
sourceClassFullName = srcClassName;
|
||||
}
|
||||
String sourceClassShortName = sourceClassFullName.substring(sourceClassFullName.lastIndexOf('.') + 1);
|
||||
this.privateAccessStatementGenerator = new PrivateAccessStatementGenerator(cx);
|
||||
this.sourceClassName = cx.names.fromString(sourceClass);
|
||||
this.sourceClassName = cx.names.fromString(sourceClassShortName);
|
||||
try {
|
||||
Class<?> cls = null;
|
||||
String sourceClassFullName = pkgName + "." + sourceClass;
|
||||
try {
|
||||
cls = Class.forName(sourceClassFullName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
if (System.getProperty(IDEA_PATHS_SELECTOR) != null) {
|
||||
// fit for intellij 2020.3+
|
||||
String sourceFileWrapperString = clazz.sourcefile.toString();
|
||||
String sourceFilePath = sourceFileWrapperString.substring(
|
||||
sourceFileWrapperString.lastIndexOf("[") + 1, sourceFileWrapperString.indexOf("]"));
|
||||
String targetFolderPath = sourceFilePath.substring(0, sourceFilePath.lastIndexOf("/src/")) +
|
||||
"/target/classes/";
|
||||
cls = new URLClassLoader(new URL[] {new URL(targetFolderPath)}).loadClass(sourceClassFullName);
|
||||
} else {
|
||||
// fit for gradle build
|
||||
String path = "file:" + System.getProperty(USER_DIR) + "/build/classes/java/main/";
|
||||
cls = new URLClassLoader(new URL[] {new URL(path)}).loadClass(sourceClassFullName);
|
||||
}
|
||||
}
|
||||
Class<?> cls = getSourceClass(clazz, sourceClassFullName);
|
||||
if (cls == null) {
|
||||
System.err.println("Failed to load source class: " + sourceClassFullName);
|
||||
return;
|
||||
}
|
||||
Field[] fields = cls.getDeclaredFields();
|
||||
for (Field f : fields) {
|
||||
if (Modifier.isFinal(f.getModifiers()) || Modifier.isPrivate(f.getModifiers())) {
|
||||
privateOrFinalFields.add(f.getName());
|
||||
}
|
||||
}
|
||||
Method[] methods = cls.getDeclaredMethods();
|
||||
for (Method m : methods) {
|
||||
if (Modifier.isPrivate(m.getModifiers())) {
|
||||
privateMethods.add(m.getName());
|
||||
}
|
||||
cx.logger.error("Failed to load source class: " + sourceClassFullName);
|
||||
} else {
|
||||
findAllPrivateMembers(cls);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
this.privateAccessChecker = new PrivateAccessChecker(cx, sourceClassShortName, memberRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* var = d.privateMethod(args) → var = PrivateAccessor.invoke(d, "privateMethod", args)
|
||||
*/
|
||||
@Override
|
||||
public void visitVarDef(JCVariableDecl jcVariableDecl) {
|
||||
jcVariableDecl.init = checkAndExchange(jcVariableDecl.init);
|
||||
super.visitVarDef(jcVariableDecl);
|
||||
if (jcVariableDecl.vartype.getClass().equals(JCIdent.class) &&
|
||||
if (jcVariableDecl.vartype instanceof JCIdent &&
|
||||
((JCIdent)jcVariableDecl.vartype).name.equals(sourceClassName)) {
|
||||
sourceClassIns.add(jcVariableDecl.name);
|
||||
}
|
||||
@@ -107,7 +95,7 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
@Override
|
||||
public void visitExec(JCExpressionStatement jcExpressionStatement) {
|
||||
// visitExec could be an assign statement to a private field
|
||||
if (jcExpressionStatement.expr.getClass().equals(JCAssign.class)) {
|
||||
if (jcExpressionStatement.expr instanceof JCAssign) {
|
||||
MemberType memberType = checkSetterType((JCAssign)jcExpressionStatement.expr);
|
||||
if (memberType.equals(MemberType.PRIVATE_OR_FINAL)) {
|
||||
jcExpressionStatement.expr = privateAccessStatementGenerator.fetchSetterStatement(
|
||||
@@ -152,7 +140,7 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
@Override
|
||||
protected JCExpression checkAndExchange(JCExpression expr) {
|
||||
// check is accessing a private field of source class
|
||||
if (expr.getClass().equals(JCFieldAccess.class)) {
|
||||
if (expr instanceof JCFieldAccess) {
|
||||
MemberType memberType = checkGetterType((JCFieldAccess)expr);
|
||||
if (memberType.equals(MemberType.PRIVATE_OR_FINAL)) {
|
||||
expr = privateAccessStatementGenerator.fetchGetterStatement((JCFieldAccess)expr);
|
||||
@@ -161,41 +149,119 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
}
|
||||
}
|
||||
// check is invoking a private method of source class
|
||||
if (expr.getClass().equals(JCMethodInvocation.class)) {
|
||||
MemberType memberType = checkInvokeType((JCMethodInvocation)expr);
|
||||
if (expr instanceof JCMethodInvocation) {
|
||||
JCMethodInvocation invocation = (JCMethodInvocation)expr;
|
||||
MemberType memberType = checkInvokeType(invocation);
|
||||
if (memberType.equals(MemberType.PRIVATE_OR_FINAL)) {
|
||||
expr = privateAccessStatementGenerator.fetchInvokeStatement((JCMethodInvocation)expr);
|
||||
expr = privateAccessStatementGenerator.fetchInvokeStatement(invocation);
|
||||
} else if (memberType.equals(MemberType.STATIC_PRIVATE)) {
|
||||
expr = privateAccessStatementGenerator.fetchStaticInvokeStatement((JCMethodInvocation)expr);
|
||||
expr = privateAccessStatementGenerator.fetchStaticInvokeStatement(invocation);
|
||||
}
|
||||
privateAccessChecker.validate((JCMethodInvocation)expr);
|
||||
}
|
||||
// check the casted expression
|
||||
if (expr instanceof JCTypeCast) {
|
||||
JCTypeCast typeCast = (JCTypeCast)expr;
|
||||
typeCast.expr = checkAndExchange(typeCast.expr);
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Class<?> getSourceClass(Symbol.ClassSymbol clazz, String sourceClassFullName)
|
||||
throws MalformedURLException, ClassNotFoundException {
|
||||
Class<?> cls;
|
||||
try {
|
||||
// maven build goes here
|
||||
cls = Class.forName(sourceClassFullName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
if (System.getProperty(IDEA_PATHS_SELECTOR) != null) {
|
||||
// fit for intellij build
|
||||
String sourceFileWrapperString = clazz.sourcefile.toString();
|
||||
String sourceFilePath = sourceFileWrapperString.substring(
|
||||
sourceFileWrapperString.lastIndexOf("[") + 1, sourceFileWrapperString.indexOf("]"));
|
||||
int indexOfSrc = sourceFilePath.lastIndexOf(File.separator + "src" + File.separator);
|
||||
String basePath = sourceFilePath.substring(0, indexOfSrc);
|
||||
try {
|
||||
String targetFolderPath = PathUtil.fitPathString(basePath + MAVEN_CLASS_FOLDER);
|
||||
cls = loadClass(targetFolderPath, sourceClassFullName);
|
||||
} catch (ClassNotFoundException e2) {
|
||||
String buildFolderPath = PathUtil.fitPathString(basePath + GRADLE_CLASS_FOLDER);
|
||||
cls = loadClass(buildFolderPath, sourceClassFullName);
|
||||
}
|
||||
} else {
|
||||
// fit for gradle build
|
||||
String path = PathUtil.fitPathString("file:" + System.getProperty(USER_DIR) + GRADLE_CLASS_FOLDER);
|
||||
cls = loadClass(path, sourceClassFullName);
|
||||
}
|
||||
}
|
||||
return cls;
|
||||
}
|
||||
|
||||
private Class<?> loadClass(String targetFolderPath, String sourceClassFullName)
|
||||
throws ClassNotFoundException, MalformedURLException {
|
||||
return new URLClassLoader(new URL[] {new URL(targetFolderPath)}).loadClass(sourceClassFullName);
|
||||
}
|
||||
|
||||
private void findAllPrivateMembers(Class<?> cls) {
|
||||
Field[] fields = cls.getDeclaredFields();
|
||||
for (Field f : fields) {
|
||||
if (Modifier.isFinal(f.getModifiers()) || Modifier.isPrivate(f.getModifiers())) {
|
||||
memberRecord.privateOrFinalFields.add(f.getName());
|
||||
} else {
|
||||
memberRecord.nonPrivateNorFinalFields.add(f.getName());
|
||||
}
|
||||
}
|
||||
Method[] methods = cls.getDeclaredMethods();
|
||||
for (final Method m : methods) {
|
||||
if (Modifier.isPrivate(m.getModifiers())) {
|
||||
checkAndAdd(memberRecord.privateMethods, m.getName(), getParameterLength(m));
|
||||
} else {
|
||||
checkAndAdd(memberRecord.nonPrivateMethods, m.getName(), getParameterLength(m));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndAdd(Map<String, List<Integer>> map, String key, final int value) {
|
||||
if (map.containsKey(key)) {
|
||||
map.get(key).add(value);
|
||||
} else {
|
||||
map.put(key, new ArrayList<Integer>() {{ add(value); }});
|
||||
}
|
||||
}
|
||||
|
||||
private int getParameterLength(Method m) {
|
||||
int length = m.getParameterTypes().length;
|
||||
if (length == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (m.getParameterTypes()[length - 1].getName().startsWith("[")) {
|
||||
return -(length - 1);
|
||||
} else {
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
private MemberType checkGetterType(JCFieldAccess access) {
|
||||
if (access.selected.getClass().equals(JCIdent.class) &&
|
||||
privateOrFinalFields.contains(access.name.toString())) {
|
||||
if (access.selected instanceof JCIdent && memberRecord.privateOrFinalFields.contains(access.name.toString())) {
|
||||
return checkSourceClassOrIns(((JCIdent)access.selected).name);
|
||||
}
|
||||
return MemberType.NONE_PRIVATE;
|
||||
return MemberType.NON_PRIVATE;
|
||||
}
|
||||
|
||||
private MemberType checkSetterType(JCAssign assign) {
|
||||
if (assign.lhs.getClass().equals(JCFieldAccess.class) &&
|
||||
((JCFieldAccess)(assign).lhs).selected.getClass().equals(JCIdent.class) &&
|
||||
privateOrFinalFields.contains(((JCFieldAccess)(assign).lhs).name.toString())) {
|
||||
if (assign.lhs instanceof JCFieldAccess && ((JCFieldAccess)(assign).lhs).selected instanceof JCIdent &&
|
||||
memberRecord.privateOrFinalFields.contains(((JCFieldAccess)(assign).lhs).name.toString())) {
|
||||
return checkSourceClassOrIns(((JCIdent)((JCFieldAccess)(assign).lhs).selected).name);
|
||||
}
|
||||
return MemberType.NONE_PRIVATE;
|
||||
return MemberType.NON_PRIVATE;
|
||||
}
|
||||
|
||||
private MemberType checkInvokeType(JCMethodInvocation expr) {
|
||||
if (expr.meth.getClass().equals(JCFieldAccess.class) &&
|
||||
((JCFieldAccess)(expr).meth).selected.getClass().equals(JCIdent.class) &&
|
||||
privateMethods.contains(((JCFieldAccess)(expr).meth).name.toString())) {
|
||||
if (expr.meth instanceof JCFieldAccess && ((JCFieldAccess)(expr).meth).selected instanceof JCIdent &&
|
||||
memberRecord.privateMethods.containsKey(((JCFieldAccess)(expr).meth).name.toString())) {
|
||||
return checkSourceClassOrIns(((JCIdent)((JCFieldAccess)(expr).meth).selected).name);
|
||||
}
|
||||
return MemberType.NONE_PRIVATE;
|
||||
return MemberType.NON_PRIVATE;
|
||||
}
|
||||
|
||||
private MemberType checkSourceClassOrIns(Name name) {
|
||||
@@ -204,7 +270,7 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
} else if (sourceClassIns.contains(name)) {
|
||||
return MemberType.PRIVATE_OR_FINAL;
|
||||
}
|
||||
return MemberType.NONE_PRIVATE;
|
||||
return MemberType.NON_PRIVATE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.alibaba.testable.processor.translator;
|
||||
|
||||
import com.alibaba.testable.processor.exception.MemberNotExistException;
|
||||
import com.alibaba.testable.processor.model.MemberRecord;
|
||||
import com.alibaba.testable.processor.model.TestableContext;
|
||||
import com.sun.tools.javac.tree.JCTree;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Validate parameter of PrivateAccessor methods to prevent broken by refactor
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
public class PrivateAccessChecker {
|
||||
|
||||
private static final String CLASS_NAME_PRIVATE_ACCESSOR = "PrivateAccessor";
|
||||
private static final String CLASS_NAME_PRIVATE_ACCESSOR_FULL = "com.alibaba.testable.core.accessor.PrivateAccessor";
|
||||
private static final List<String> FIELD_ACCESS_METHOD = Arrays.asList(new String[]
|
||||
{ "get", "set", "getStatic", "setStatic" }.clone());
|
||||
private static final List<String> FIELD_INVOKE_METHOD = Arrays.asList(new String[]
|
||||
{ "invoke", "invokeStatic" }.clone());
|
||||
private static final String TYPE_FIELD = "Field";
|
||||
private static final String TYPE_METHOD = "Method";
|
||||
|
||||
private final TestableContext cx;
|
||||
private final String className;
|
||||
private final MemberRecord sourceMembers;
|
||||
|
||||
public PrivateAccessChecker(TestableContext cx, String className, MemberRecord memberRecord) {
|
||||
this.cx = cx;
|
||||
this.className = className;
|
||||
this.sourceMembers = memberRecord;
|
||||
}
|
||||
|
||||
public void validate(JCTree.JCMethodInvocation invocation) {
|
||||
if (invocation.meth instanceof JCTree.JCFieldAccess && invocation.args.length() >= 2) {
|
||||
JCTree.JCFieldAccess fieldAccess = (JCTree.JCFieldAccess)invocation.meth;
|
||||
if (invocation.args.get(1) instanceof JCTree.JCLiteral && isPrivateAccessor(fieldAccess)) {
|
||||
Object target = ((JCTree.JCLiteral)invocation.args.get(1)).getValue();
|
||||
if (target instanceof String) {
|
||||
String methodName = fieldAccess.name.toString();
|
||||
if (FIELD_ACCESS_METHOD.contains(methodName)) {
|
||||
if (sourceMembers.nonPrivateNorFinalFields.contains(target)) {
|
||||
cx.logger.warn("Field " + className + "::" + target + " is neither private nor final.");
|
||||
} else if (!sourceMembers.privateOrFinalFields.contains(target)) {
|
||||
throw new MemberNotExistException(TYPE_FIELD, className, (String)target);
|
||||
}
|
||||
} else if (FIELD_INVOKE_METHOD.contains(methodName)) {
|
||||
int parameterCount = invocation.args.length() - 2;
|
||||
// Because of override, check private method list first
|
||||
if (sourceMembers.privateMethods.containsKey(target) &&
|
||||
checkParameterCount(sourceMembers.privateMethods, (String)target, parameterCount)) {
|
||||
// Let it go
|
||||
} else if (sourceMembers.nonPrivateMethods.containsKey(target) &&
|
||||
checkParameterCount(sourceMembers.nonPrivateMethods, (String)target, parameterCount)) {
|
||||
cx.logger.warn("Method " + className + "::" + target + " is not private.");
|
||||
} else {
|
||||
throw new MemberNotExistException(TYPE_METHOD, className, (String)target, parameterCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPrivateAccessor(JCTree.JCFieldAccess fieldAccess) {
|
||||
return isPrivateAccessorWithShortRef(fieldAccess) || isPrivateAccessorWithFullRef(fieldAccess);
|
||||
}
|
||||
|
||||
private boolean isPrivateAccessorWithShortRef(JCTree.JCFieldAccess fieldAccess) {
|
||||
return fieldAccess.selected instanceof JCTree.JCIdent &&
|
||||
((JCTree.JCIdent)fieldAccess.selected).name.toString().equals(CLASS_NAME_PRIVATE_ACCESSOR);
|
||||
}
|
||||
|
||||
private boolean isPrivateAccessorWithFullRef(JCTree.JCFieldAccess fieldAccess) {
|
||||
return fieldAccess.selected instanceof JCTree.JCFieldAccess &&
|
||||
fieldAccess.selected.toString().equals(CLASS_NAME_PRIVATE_ACCESSOR_FULL);
|
||||
}
|
||||
|
||||
private boolean checkParameterCount(Map<String, List<Integer>> methods, String target, int parameterCount) {
|
||||
for (Integer expectCount : methods.get(target)) {
|
||||
if (countMatch(parameterCount, expectCount)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean countMatch(int parameterCount, Integer expectCount) {
|
||||
return expectCount == parameterCount || (expectCount < 0 && parameterCount >= -expectCount);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.alibaba.testable.processor.util;
|
||||
|
||||
public class PathUtil {
|
||||
|
||||
private static final String PREFIX_WIN = "win";
|
||||
private static final String PROPERTY_OS_NAME = "os.name";
|
||||
private static final String PATH_SPLIT_UNIX = "/";
|
||||
private static final String PATH_SPLIT_WIN = "\\\\";
|
||||
private static final String PROTOCOL_FILE = "file:";
|
||||
|
||||
/**
|
||||
* Fit path according to operation system type
|
||||
*
|
||||
* @param path original path
|
||||
* @return fitted path
|
||||
*/
|
||||
public static String fitPathString(String path) {
|
||||
String os = System.getProperty(PROPERTY_OS_NAME);
|
||||
if (os.toLowerCase().startsWith(PREFIX_WIN)) {
|
||||
path = path.replaceAll(PATH_SPLIT_UNIX, PATH_SPLIT_WIN);
|
||||
}
|
||||
return path.startsWith(PROTOCOL_FILE) ? path : (PROTOCOL_FILE + path);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,16 +9,14 @@ public class StringUtil {
|
||||
|
||||
/**
|
||||
* Join strings
|
||||
* @param list strings to join
|
||||
* @param list strings to join
|
||||
* @param conjunction connection character
|
||||
* @return joined string
|
||||
*/
|
||||
static public String join(List<String> list, String conjunction)
|
||||
{
|
||||
static public String join(List<String> list, String conjunction) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean first = true;
|
||||
for (String item : list)
|
||||
{
|
||||
for (String item : list) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
|
||||
@@ -17,10 +17,12 @@ public class TestableLogger {
|
||||
}
|
||||
|
||||
public void info(String msg) {
|
||||
// Message level lower than warning is not shown by default, use stdout instead
|
||||
System.out.println("[INFO] " + msg);
|
||||
}
|
||||
|
||||
public void warn(String msg) {
|
||||
// Message level WARNING won't show, use MANDATORY_WARNING instead
|
||||
messager.printMessage(Diagnostic.Kind.MANDATORY_WARNING, msg);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user