mirror of
https://github.com/alibaba/testable-mock.git
synced 2026-08-19 09:43:29 +08:00
Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f96d272f97 | ||
|
|
b1e6ad58cc | ||
|
|
48d6ef328b | ||
|
|
84df2a0fd8 | ||
|
|
f9621a6f2e | ||
|
|
55ccabbb39 | ||
|
|
cfc28467bb | ||
|
|
23a0a0cd90 | ||
|
|
a1086ff402 | ||
|
|
6237e19a32 | ||
|
|
df73dac54f | ||
|
|
f660ccb88f | ||
|
|
82d87f3447 | ||
|
|
65cc631d39 | ||
|
|
bdd99577c4 | ||
|
|
8be5550331 | ||
|
|
6a5c817a3a | ||
|
|
c76ed2d84a | ||
|
|
14549040d9 | ||
|
|
03ec857cd0 | ||
|
|
b4a29e7e8e | ||
|
|
02c74299f4 | ||
|
|
4c6fc5b228 | ||
|
|
6d6e2ecb7f | ||
|
|
c364e96f77 | ||
|
|
dab1d36a81 | ||
|
|
01bd676df7 | ||
|
|
02d7fb6e91 | ||
|
|
d8ffcacdaf | ||
|
|
be53ea2d9c | ||
|
|
9eb8682ec0 | ||
|
|
2a54fe70dd | ||
|
|
790409a336 | ||
|
|
189ef43565 | ||
|
|
34f1dc8f73 | ||
|
|
b60ebf2f2c | ||
|
|
d95cba6d37 | ||
|
|
5ef06c4bde | ||
|
|
8814276d63 | ||
|
|
374a4d5442 | ||
|
|
9a22f361f3 | ||
|
|
94c5aa8621 | ||
|
|
74e3cf16a4 | ||
|
|
19f28c32db | ||
|
|
affa56d057 | ||
|
|
f1edf92626 | ||
|
|
6f9accf319 | ||
|
|
cefc5fb1df | ||
|
|
948878687c | ||
|
|
d626906d28 | ||
|
|
705ba796f1 | ||
|
|
7e44a66205 | ||
|
|
d57dedb6fc | ||
|
|
d860684e91 | ||
|
|
7bbdf82a41 | ||
|
|
126c7257d8 | ||
|
|
adfba4bac0 | ||
|
|
2c18ea12ed | ||
|
|
d4bce13918 | ||
|
|
b23b8b98b3 | ||
|
|
f228ed5002 | ||
|
|
2303ac188e | ||
|
|
290d0db400 | ||
|
|
77b955a5ce | ||
|
|
ab8a0b32e5 | ||
|
|
39c788cd02 | ||
|
|
7029579534 | ||
|
|
ba951ce744 | ||
|
|
2bbe2eba93 | ||
|
|
4c96f4cc0e | ||
|
|
3a2e3032df | ||
|
|
530f3295bd | ||
|
|
650b385785 | ||
|
|
99e7c3bf18 | ||
|
|
ef5d21baf6 | ||
|
|
f9be92e9d0 | ||
|
|
019e407a4f | ||
|
|
6951b98c7e | ||
|
|
369c2bd192 | ||
|
|
13de5e42ad | ||
|
|
453908cd46 | ||
|
|
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 |
18
README.md
18
README.md
@@ -2,13 +2,27 @@
|
||||
|
||||
换种思路写Mock,让单元测试更简单。
|
||||
|
||||
无需初始化,不挑测试框架,甭管要换的是私有方法、静态方法、构造方法还是其他任何类的任何方法,也甭管要换的对象是怎么创建的。写好Mock定义,加个`@MockMethod`注解,一切统统搞定。
|
||||
无需初始化,不挑服务框架,甭管要换的是私有方法、静态方法、构造方法还是其他任何类的任何方法,也甭管要换的对象是怎么创建的。写好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)了解更多故事。
|
||||
|
||||
<font size="5">**0.5版本已发布**</font>,从`0.4.x`升级到`0.5.x`版本请参考[0.5版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-to-v05)
|
||||
|
||||
如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将在24小时内回复并处理
|
||||
|
||||
-----
|
||||
|
||||
## 版本计划
|
||||
|
||||
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
|
||||
|
||||
- `0.5` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
|
||||
- `0.6` 实现第四项单元测试增强能力"[快速入参构造器](https://alibaba.github.io/testable-mock/#/zh-cn/doc/parameter-constructor)"
|
||||
- `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,13 +7,18 @@ 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('com.alibaba.testable:testable-all:0.5.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.0')
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
test {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<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>
|
||||
<testable.version>0.5.0</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -32,15 +32,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>-->
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
*/
|
||||
public class DemoPrivateAccess {
|
||||
|
||||
/**
|
||||
* a private static field
|
||||
*/
|
||||
private static int staticCount;
|
||||
/**
|
||||
* a private member field
|
||||
*/
|
||||
private int count;
|
||||
/**
|
||||
* a constant field
|
||||
*/
|
||||
public final Double pi = 3.14;
|
||||
|
||||
/**
|
||||
* private static method
|
||||
*/
|
||||
private static String privateStaticFunc(String str, int i) {
|
||||
return str + " + " + i;
|
||||
}
|
||||
|
||||
/**
|
||||
* private member method
|
||||
*/
|
||||
private String privateFunc(List<String> list, String str, int i) {
|
||||
return list.stream().reduce((a, s) -> a + s).orElse("") + " + " + str + " + " + i;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.model.Box;
|
||||
import com.alibaba.testable.demo.model.Color;
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox;
|
||||
import com.alibaba.testable.demo.basic.model.Box;
|
||||
import com.alibaba.testable.demo.basic.model.Color;
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
*/
|
||||
public class DemoPrivateAccess {
|
||||
|
||||
/**
|
||||
* a private static field
|
||||
*/
|
||||
private static int staticCount;
|
||||
/**
|
||||
* a private member field
|
||||
*/
|
||||
private int count;
|
||||
/**
|
||||
* a constant field
|
||||
*/
|
||||
public final Double pi = 3.14;
|
||||
|
||||
/**
|
||||
* private static method without arguments
|
||||
*/
|
||||
private static String privateStaticFunc() {
|
||||
return "static";
|
||||
}
|
||||
|
||||
/**
|
||||
* private static method with arguments
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
package com.alibaba.testable.demo.basic.model;
|
||||
|
||||
public class BlackBox extends Box implements Color {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
package com.alibaba.testable.demo.basic.model;
|
||||
|
||||
abstract public class Box {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
package com.alibaba.testable.demo.basic.model;
|
||||
|
||||
public interface Color {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
public class ASvc {
|
||||
|
||||
public String demo(String name) {
|
||||
return String.format("a_%s", name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
public class BSvc {
|
||||
|
||||
public String demo(String name) {
|
||||
return String.format("b_%s", name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
public class CSvc {
|
||||
|
||||
public String demo(String name) {
|
||||
return String.format("c_%s", name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
*/
|
||||
@EnablePrivateAccess
|
||||
class DemoPrivateAccessTest {
|
||||
|
||||
private DemoPrivateAccess demoPrivateAccess = new DemoPrivateAccess();
|
||||
|
||||
@Test
|
||||
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));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_field() {
|
||||
demoPrivateAccess.count = 2;
|
||||
assertEquals(new Integer(2), demoPrivateAccess.count);
|
||||
|
||||
PrivateAccessor.set(demoPrivateAccess, "count", 3);
|
||||
assertEquals(new Integer(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));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_static_field() {
|
||||
DemoPrivateAccess.staticCount = 2;
|
||||
assertEquals(new Integer(2), DemoPrivateAccess.staticCount);
|
||||
|
||||
PrivateAccessor.setStatic(DemoPrivateAccess.class, "staticCount", 3);
|
||||
assertEquals(new Integer(3), PrivateAccessor.getStatic(DemoPrivateAccess.class, "staticCount"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_update_final_field() {
|
||||
demoPrivateAccess.pi = 4.13;
|
||||
assertEquals(4.13, demoPrivateAccess.pi);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
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 java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示模板方法的Mock场景
|
||||
* Demonstrate scenario of mocking template method
|
||||
*/
|
||||
class DemoTemplateTest {
|
||||
|
||||
private DemoTemplate demoTemplate = new DemoTemplate();
|
||||
|
||||
/* 第一种写法:使用泛型定义 */
|
||||
/* First solution: use generics type */
|
||||
|
||||
@MockMethod
|
||||
private static <T> List<T> getList(DemoTemplate self, T value) {
|
||||
return new ArrayList<T>() {{ add((T)(value.toString() + "_mock_list")); }};
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private static <K, V> Map<K, V> getMap(DemoTemplate self, K key, V value) {
|
||||
return new HashMap<K, V>() {{ put(key, (V)(value.toString() + "_mock_map")); }};
|
||||
}
|
||||
|
||||
@MockConstructor
|
||||
private <T> HashSet<T> newHashSet() {
|
||||
HashSet<T> set = new HashSet<>();
|
||||
set.add((T)"insert_mock");
|
||||
return set;
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private <E> boolean add(Set s, E e) {
|
||||
s.add(e.toString() + "_mocked");
|
||||
return true;
|
||||
}
|
||||
|
||||
/* 第二种写法:使用Object类型 */
|
||||
/* Second solution: use object type */
|
||||
|
||||
//@MockMethod
|
||||
//private static List<Object> getList(DemoTemplate self, Object value) {
|
||||
// return new ArrayList<Object>() {{ add(value.toString() + "_mock_list"); }};
|
||||
//}
|
||||
//
|
||||
//@MockMethod
|
||||
//private static Map<Object, Object> getMap(DemoTemplate self, Object key, Object value) {
|
||||
// return new HashMap<Object, Object>() {{ put(key, value.toString() + "_mock_map"); }};
|
||||
//}
|
||||
//
|
||||
//@MockConstructor
|
||||
//private HashSet newHashSet() {
|
||||
// HashSet<Object> set = new HashSet<>();
|
||||
// set.add("insert_mock");
|
||||
// return set;
|
||||
//}
|
||||
//
|
||||
//@MockMethod
|
||||
//private boolean add(Set s, Object e) {
|
||||
// s.add(e.toString() + "_mocked");
|
||||
// return true;
|
||||
//}
|
||||
|
||||
|
||||
@Test
|
||||
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() {
|
||||
String res = demoTemplate.doubleTemplateMethod();
|
||||
assertEquals("testable_mock_map", res);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_new_template_method() {
|
||||
Set<?> res = demoTemplate.newTemplateMethod();
|
||||
assertEquals(2, res.size());
|
||||
Iterator<?> iterator = res.stream().iterator();
|
||||
assertEquals("insert_mock", iterator.next());
|
||||
assertEquals("world_mocked", iterator.next());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
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 com.alibaba.testable.demo.basic.DemoInherit;
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox;
|
||||
import com.alibaba.testable.demo.basic.model.Box;
|
||||
import com.alibaba.testable.demo.basic.model.Color;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
@@ -17,36 +18,37 @@ class DemoInheritTest {
|
||||
|
||||
private DemoInherit demoInherit = new DemoInherit();
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private void put_into_box(Box self, String something) {
|
||||
self.put("put_" + something + "_into_box");
|
||||
}
|
||||
public static class Mock {
|
||||
@MockMethod(targetMethod = "put")
|
||||
private void put_into_box(Box self, String something) {
|
||||
self.put("put_" + something + "_into_box");
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private void put_into_blackbox(BlackBox self, String something) {
|
||||
self.put("put_" + something + "_into_blackbox");
|
||||
}
|
||||
@MockMethod(targetMethod = "put")
|
||||
private void put_into_blackbox(BlackBox self, String something) {
|
||||
self.put("put_" + something + "_into_blackbox");
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private String get_from_box(Box self) {
|
||||
return "get_from_box";
|
||||
}
|
||||
@MockMethod(targetMethod = "get")
|
||||
private String get_from_box(Box self) {
|
||||
return "get_from_box";
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private String get_from_blackbox(BlackBox self) {
|
||||
return "get_from_blackbox";
|
||||
}
|
||||
@MockMethod(targetMethod = "get")
|
||||
private String get_from_blackbox(BlackBox self) {
|
||||
return "get_from_blackbox";
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private String get_color_from_color(Color self) {
|
||||
return "color_from_color";
|
||||
}
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private String get_color_from_color(Color self) {
|
||||
return "color_from_color";
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private String get_color_from_blackbox(BlackBox self) {
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private String get_color_from_blackbox(BlackBox self) {
|
||||
return "color_from_blackbox";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_sub_object_method_by_parent_object() {
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.core.error.VerifyFailedError;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.basic.DemoMatcher;
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeMatcher.*;
|
||||
@@ -17,15 +18,16 @@ class DemoMatcherTest {
|
||||
|
||||
private DemoMatcher demoMatcher = new DemoMatcher();
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithoutArgument(DemoMatcher self) {}
|
||||
public static class Mock {
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithoutArgument(DemoMatcher self) {}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArguments(DemoMatcher self, Object a1, Object a2) {}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArrayArgument(DemoMatcher self, Object[] a) {}
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArguments(DemoMatcher self, Object a1, Object a2) {}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArrayArgument(DemoMatcher self, Object[] a) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_match_no_argument() {
|
||||
@@ -1,15 +1,16 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.basic.DemoMock;
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox;
|
||||
import org.junit.jupiter.api.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;
|
||||
|
||||
/**
|
||||
@@ -20,53 +21,56 @@ class DemoMockTest {
|
||||
|
||||
private DemoMock demoMock = new DemoMock();
|
||||
|
||||
@MockConstructor
|
||||
private BlackBox createBlackBox(String text) {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private String innerFunc(DemoMock self, String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private String staticFunc(DemoMock self) {
|
||||
return "_MOCK_TAIL";
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private String trim(String self) {
|
||||
return "trim_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "substring")
|
||||
private String sub(String self, int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private boolean startsWith(String self, String s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private BlackBox secretBox(BlackBox ignore) {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private String callFromDifferentMethod(DemoMock self) {
|
||||
if (TEST_CASE.equals("should_able_to_get_test_case_name")) {
|
||||
return "mock_special";
|
||||
public static class Mock {
|
||||
@MockConstructor
|
||||
private BlackBox createBlackBox(String text) {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
switch (SOURCE_METHOD) {
|
||||
case "callerOne": return "mock_one";
|
||||
default: return "mock_others";
|
||||
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String staticFunc() {
|
||||
return "_MOCK_TAIL";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class)
|
||||
private String trim() {
|
||||
return "trim_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "substring")
|
||||
private String sub(int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class)
|
||||
private boolean startsWith(String s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = BlackBox.class)
|
||||
private BlackBox secretBox() {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String callFromDifferentMethod() {
|
||||
if ("special_case".equals(MOCK_CONTEXT.get("case"))) {
|
||||
return "mock_special";
|
||||
}
|
||||
switch (SOURCE_METHOD) {
|
||||
case "callerOne":
|
||||
return "mock_one";
|
||||
default:
|
||||
return "mock_others";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_new_object() {
|
||||
assertEquals("mock_something", demoMock.newFunc());
|
||||
@@ -77,6 +81,7 @@ class DemoMockTest {
|
||||
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
|
||||
@@ -104,7 +109,8 @@ class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_test_case_name() throws Exception {
|
||||
void should_able_to_set_mock_context() throws Exception {
|
||||
MOCK_CONTEXT.put("case", "special_case");
|
||||
// synchronous
|
||||
assertEquals("mock_special", demoMock.callerOne());
|
||||
// asynchronous
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import com.alibaba.testable.demo.basic.DemoPrivateAccess;
|
||||
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
*/
|
||||
@EnablePrivateAccess
|
||||
class DemoPrivateAccessTest {
|
||||
|
||||
private DemoPrivateAccess demoPrivateAccess = new DemoPrivateAccess();
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_method() {
|
||||
List<String> list = new ArrayList<String>() {{ add("a"); add("b"); add("c"); }};
|
||||
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() {
|
||||
demoPrivateAccess.count = 2;
|
||||
assertEquals(Integer.valueOf(2), demoPrivateAccess.count);
|
||||
|
||||
PrivateAccessor.set(demoPrivateAccess, "count", 3);
|
||||
assertEquals(Integer.valueOf(3), PrivateAccessor.get(demoPrivateAccess, "count"));
|
||||
}
|
||||
|
||||
@Test
|
||||
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() {
|
||||
DemoPrivateAccess.staticCount = 2;
|
||||
assertEquals(Integer.valueOf(2), DemoPrivateAccess.staticCount);
|
||||
|
||||
PrivateAccessor.setStatic(DemoPrivateAccess.class, "staticCount", 3);
|
||||
assertEquals(Integer.valueOf(3), PrivateAccessor.getStatic(DemoPrivateAccess.class, "staticCount"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_update_final_field() {
|
||||
demoPrivateAccess.pi = 4.13;
|
||||
assertEquals(Double.valueOf(4.13), demoPrivateAccess.pi);
|
||||
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", 3.14);
|
||||
assertEquals(Double.valueOf(3.14), PrivateAccessor.get(demoPrivateAccess, "pi"));
|
||||
}
|
||||
|
||||
@Test
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.demo.basic.DemoTemplate;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示模板方法的Mock场景
|
||||
* Demonstrate scenario of mocking template method
|
||||
*/
|
||||
class DemoTemplateTest {
|
||||
|
||||
private DemoTemplate demoTemplate = new DemoTemplate();
|
||||
|
||||
public static class Mock {
|
||||
/* 第一种写法:使用泛型定义 */
|
||||
/* First solution: use generics type */
|
||||
|
||||
@MockMethod
|
||||
private <T> List<T> getList(DemoTemplate self, T value) {
|
||||
return new ArrayList<T>() {{ add((T)(value.toString() + "_mock_list")); }};
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private <K, V> Map<K, V> getMap(DemoTemplate self, K key, V value) {
|
||||
return new HashMap<K, V>() {{ put(key, (V)(value.toString() + "_mock_map")); }};
|
||||
}
|
||||
|
||||
@MockConstructor
|
||||
private <T> HashSet<T> newHashSet() {
|
||||
HashSet<T> set = new HashSet<>();
|
||||
set.add((T)"insert_mock");
|
||||
return set;
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private <E> boolean add(Set s, E e) {
|
||||
s.add(e.toString() + "_mocked");
|
||||
return true;
|
||||
}
|
||||
|
||||
/* 第二种写法:使用Object类型 */
|
||||
/* Second solution: use object type */
|
||||
|
||||
//@MockMethod
|
||||
//private List<Object> getList(DemoTemplate self, Object value) {
|
||||
// return new ArrayList<Object>() {{ add(value.toString() + "_mock_list"); }};
|
||||
//}
|
||||
//
|
||||
//@MockMethod
|
||||
//private Map<Object, Object> getMap(DemoTemplate self, Object key, Object value) {
|
||||
// return new HashMap<Object, Object>() {{ put(key, value.toString() + "_mock_map"); }};
|
||||
//}
|
||||
//
|
||||
//@MockConstructor
|
||||
//private HashSet newHashSet() {
|
||||
// HashSet<Object> set = new HashSet<>();
|
||||
// set.add("insert_mock");
|
||||
// return set;
|
||||
//}
|
||||
//
|
||||
//@MockMethod
|
||||
//private boolean add(Set s, Object e) {
|
||||
// s.add(e.toString() + "_mocked");
|
||||
// return true;
|
||||
//}
|
||||
}
|
||||
|
||||
@Test
|
||||
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() {
|
||||
String res = demoTemplate.doubleTemplateMethod();
|
||||
assertEquals("testable_mock_map", res);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_new_template_method() {
|
||||
Set<?> res = demoTemplate.newTemplateMethod();
|
||||
assertEquals(2, res.size());
|
||||
Iterator<?> iterator = res.stream().iterator();
|
||||
assertEquals("insert_mock", iterator.next());
|
||||
assertEquals("world_mocked", iterator.next());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
public class ASvcMock {
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "format")
|
||||
public String a_format(String format, Object... args) {
|
||||
return "a_mock";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
public class BSvcMock {
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "format")
|
||||
public String b_format(String format, Object... args) {
|
||||
return "b_mock";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
public class CSvcMock {
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "format")
|
||||
public String c_format(String format, Object... args) {
|
||||
return "c_mock";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockWith;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@MockWith
|
||||
public class OneToMultiSvcTest {
|
||||
|
||||
private ASvc aSvc = new ASvc();
|
||||
private BSvc bSvc = new BSvc();
|
||||
private CSvc cSvc = new CSvc();
|
||||
|
||||
@Test
|
||||
public void should_able_to_test_multi_class_together() {
|
||||
assertEquals("a_mock", aSvc.demo("test"));
|
||||
assertEquals("b_mock", bSvc.demo("test"));
|
||||
assertEquals("c_mock", cSvc.demo("test"));
|
||||
verify("a_format").withTimes(1);
|
||||
verify("b_format").withTimes(1);
|
||||
verify("c_format").withTimes(1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.5.0")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.5.0")
|
||||
}
|
||||
|
||||
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.5.0</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>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
class DemoPrivateAccess {
|
||||
|
||||
/**
|
||||
* a private member field
|
||||
*/
|
||||
private var count = 0
|
||||
|
||||
/**
|
||||
* a constant field
|
||||
*/
|
||||
val pi = 3.14
|
||||
|
||||
/**
|
||||
* private member method
|
||||
*/
|
||||
private fun privateFunc(s: String, i: Int): String {
|
||||
return "$s - $i"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* a private static field
|
||||
*/
|
||||
private var staticCount = 0
|
||||
|
||||
/**
|
||||
* private static method
|
||||
*/
|
||||
private fun privateStaticFunc(s: String, i: Int): String {
|
||||
return "$s + $i"
|
||||
}
|
||||
|
||||
/**
|
||||
* private jvm static method
|
||||
*/
|
||||
@JvmStatic private fun privateJvmStaticFunc(s: String, i: Int): String {
|
||||
return "$s * $i"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.Box
|
||||
import com.alibaba.testable.demo.model.Color
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox
|
||||
import com.alibaba.testable.demo.basic.model.Box
|
||||
import com.alibaba.testable.demo.basic.model.Color
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
|
||||
*/
|
||||
class DemoInherit {
|
||||
|
||||
/**
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
* 演示Mock方法调用校验器
|
||||
* Demonstrate mock method invocation verifier
|
||||
*/
|
||||
class DemoMatcher {
|
||||
/**
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.ColorBox
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox
|
||||
import com.alibaba.testable.demo.basic.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()}"
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
*/
|
||||
class DemoPrivateAccess {
|
||||
|
||||
/**
|
||||
* a private member field
|
||||
*/
|
||||
private var count = 0
|
||||
|
||||
/**
|
||||
* a constant field
|
||||
*/
|
||||
val pi = 3.14
|
||||
|
||||
/**
|
||||
* private member method
|
||||
*/
|
||||
private fun privateFunc(list: List<String>, str: String, i: Int): String {
|
||||
return list.reduce { a: String, s: String -> a + s } + " + " + "$str + $i"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* a private static field
|
||||
*/
|
||||
private var staticCount = 0
|
||||
|
||||
/**
|
||||
* private static method
|
||||
*/
|
||||
private fun privateStaticFunc(str: String, i: Int): String {
|
||||
return "$str + $i"
|
||||
}
|
||||
|
||||
/**
|
||||
* private jvm static method
|
||||
*/
|
||||
@JvmStatic private fun privateJvmStaticFunc(list: List<String>, str: String, i: Int): String {
|
||||
return list.reduce { a: String, s: String -> a + s } + " * " + "$str * $i"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
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()
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.model
|
||||
package com.alibaba.testable.demo.basic.model
|
||||
|
||||
|
||||
class BlackBox(var input: String) : Box(), Color {
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.model
|
||||
package com.alibaba.testable.demo.basic.model
|
||||
|
||||
abstract class Box {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.alibaba.testable.demo.basic.model
|
||||
|
||||
interface Color {
|
||||
|
||||
val color: String
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.util
|
||||
package com.alibaba.testable.demo.java2kotlin
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.alibaba.testable.demo.model
|
||||
|
||||
interface Color {
|
||||
|
||||
val color: String
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
class ASvc {
|
||||
|
||||
fun demo(name: String): String {
|
||||
return String.format("a_%s", name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
class BSvc {
|
||||
|
||||
fun demo(name: String): String {
|
||||
return String.format("b_%s", name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
class CSvc {
|
||||
|
||||
fun demo(name: String): String {
|
||||
return String.format("c_%s", name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.Box
|
||||
import com.alibaba.testable.demo.model.Color
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox
|
||||
import com.alibaba.testable.demo.basic.model.Box
|
||||
import com.alibaba.testable.demo.basic.model.Color
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -16,37 +16,38 @@ internal class DemoInheritTest {
|
||||
|
||||
private val demoInherit = DemoInherit()
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_box(self: Box, something: String) {
|
||||
self.put("put_" + something + "_into_box")
|
||||
}
|
||||
class Mock {
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_box(self: Box, something: String) {
|
||||
self.put("put_" + something + "_into_box")
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_blackbox(self: BlackBox, something: String) {
|
||||
self.put("put_" + something + "_into_blackbox")
|
||||
}
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_blackbox(self: BlackBox, something: String) {
|
||||
self.put("put_" + something + "_into_blackbox")
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_box(self: Box): String {
|
||||
return "get_from_box"
|
||||
}
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_box(self: Box): String {
|
||||
return "get_from_box"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_blackbox(self: BlackBox): String {
|
||||
return "get_from_blackbox"
|
||||
}
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_blackbox(self: BlackBox): String {
|
||||
return "get_from_blackbox"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_color(self: Color): String {
|
||||
return "color_from_color"
|
||||
}
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_color(self: Color): String {
|
||||
return "color_from_color"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_blackbox(self: BlackBox): String {
|
||||
return "color_from_blackbox"
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_blackbox(self: BlackBox): String {
|
||||
return "color_from_blackbox"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_sub_object_method_by_parent_object() {
|
||||
val box = demoInherit.putIntoBox() as BlackBox
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.error.VerifyFailedError
|
||||
import com.alibaba.testable.core.matcher.InvokeMatcher
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -16,19 +16,20 @@ internal class DemoMatcherTest {
|
||||
|
||||
private val demoMatcher = DemoMatcher()
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithoutArgument(self: DemoMatcher) {
|
||||
}
|
||||
class Mock {
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithoutArgument(self: DemoMatcher) {
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArguments(self: DemoMatcher, a1: Any, a2: Any) {
|
||||
}
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArguments(self: DemoMatcher, a1: Any, a2: Any) {
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArrayArgument(self: DemoMatcher, a: Array<Any>) {
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArrayArgument(self: DemoMatcher, a: Array<Any>) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun should_match_no_argument() {
|
||||
demoMatcher.callMethodWithoutArgument()
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
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.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.ColorBox
|
||||
import com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox
|
||||
import com.alibaba.testable.demo.basic.model.ColorBox
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.concurrent.Executors
|
||||
@@ -19,44 +19,50 @@ internal class DemoMockTest {
|
||||
|
||||
private val demoMock = DemoMock()
|
||||
|
||||
@MockConstructor
|
||||
private fun createBlackBox(text: String) = BlackBox("mock_$text")
|
||||
class Mock {
|
||||
@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 {
|
||||
return BlackBox("not_secret_box")
|
||||
}
|
||||
@MockMethod(targetClass = BlackBox::class)
|
||||
private fun startsWith(s: String) = false
|
||||
|
||||
@MockMethod
|
||||
private fun createBox(ignore: ColorBox, color: String, box: BlackBox): BlackBox {
|
||||
return BlackBox("White_${box.get()}")
|
||||
}
|
||||
@MockMethod(targetClass = BlackBox::class)
|
||||
private fun secretBox(): BlackBox {
|
||||
return BlackBox("not_secret_box")
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private fun callFromDifferentMethod(self: DemoMock): String {
|
||||
return if (TEST_CASE == "should_able_to_get_test_case_name") {
|
||||
"mock_special"
|
||||
} else {
|
||||
when (SOURCE_METHOD) {
|
||||
"callerOne" -> "mock_one"
|
||||
else -> "mock_others"
|
||||
@MockMethod(targetClass = ColorBox::class)
|
||||
private fun createBox(color: String, box: BlackBox): BlackBox {
|
||||
return BlackBox("White_${box.get()}")
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoMock::class)
|
||||
private fun callFromDifferentMethod(): String {
|
||||
return if (MOCK_CONTEXT["case"] == "special_case") {
|
||||
"mock_special"
|
||||
} else {
|
||||
when (SOURCE_METHOD) {
|
||||
"callerOne" -> "mock_one"
|
||||
else -> "mock_others"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_new_object() {
|
||||
assertEquals("mock_something", demoMock.newFunc())
|
||||
@@ -65,10 +71,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 +110,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 +118,6 @@ internal class DemoMockTest {
|
||||
demoMock.callerOne()
|
||||
}.get())
|
||||
verify("callFromDifferentMethod").withTimes(2)
|
||||
MOCK_CONTEXT.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
@@ -7,37 +7,38 @@ 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 {
|
||||
|
||||
private val demoTemplate = DemoTemplate()
|
||||
|
||||
@MockMethod
|
||||
private fun <T> getList(self: DemoTemplate, value: T): List<T> {
|
||||
return mutableListOf((value.toString() + "_mock_list") as T)
|
||||
}
|
||||
class Mock {
|
||||
@MockMethod
|
||||
private fun <T> getList(self: DemoTemplate, value: T): List<T> {
|
||||
return mutableListOf((value.toString() + "_mock_list") as T)
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private fun <K, V> getMap(self: DemoTemplate, key: K, value: V): Map<K, V> {
|
||||
return mutableMapOf(key to (value.toString() + "_mock_map") as V)
|
||||
}
|
||||
@MockMethod
|
||||
private fun <K, V> getMap(self: DemoTemplate, key: K, value: V): Map<K, V> {
|
||||
return mutableMapOf(key to (value.toString() + "_mock_map") as V)
|
||||
}
|
||||
|
||||
@MockConstructor
|
||||
private fun newHashSet(): HashSet<*> {
|
||||
val set = HashSet<Any>()
|
||||
set.add("insert_mock")
|
||||
return set
|
||||
}
|
||||
@MockConstructor
|
||||
private fun newHashSet(): HashSet<*> {
|
||||
val set = HashSet<Any>()
|
||||
set.add("insert_mock")
|
||||
return set
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private fun <E> add(s: MutableSet<E>, e: E): Boolean {
|
||||
s.add((e.toString() + "_mocked") as E)
|
||||
return true
|
||||
@MockMethod
|
||||
private fun <E> add(s: MutableSet<E>, e: E): Boolean {
|
||||
s.add((e.toString() + "_mocked") as E)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_single_template_method() {
|
||||
val res = demoTemplate.singleTemplateMethod()
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.alibaba.testable.demo.java2kotlin
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
|
||||
class PathUtilTest {
|
||||
|
||||
class Mock {
|
||||
@MockMethod
|
||||
fun exists(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> true
|
||||
"/a/b/c" -> true
|
||||
else -> f.exists()
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun isDirectory(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b/c" -> true
|
||||
else -> f.isDirectory
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun delete(f: File): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun listFiles(f: File): Array<File>? {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> arrayOf(File("/a/b/c"), File("/a/b/d"))
|
||||
"/a/b/c" -> arrayOf(File("/a/b/c/e"))
|
||||
else -> f.listFiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_java_method_invoke_in_kotlin() {
|
||||
PathUtil.deleteRecursively(File("/a/b/"))
|
||||
verify("listFiles").withTimes(2)
|
||||
verify("delete").withTimes(4)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
|
||||
class ASvcMock {
|
||||
|
||||
@MockMethod(targetClass = String::class, targetMethod = "format")
|
||||
fun a_format(format: String, vararg args: Any?): String {
|
||||
return "a_mock"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
|
||||
class BSvcMock {
|
||||
|
||||
@MockMethod(targetClass = String::class, targetMethod = "format")
|
||||
fun b_format(format: String, vararg args: Any?): String {
|
||||
return "b_mock"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
|
||||
class CSvcMock {
|
||||
|
||||
@MockMethod(targetClass = String::class, targetMethod = "format")
|
||||
fun c_format(format: String, vararg args: Any?): String {
|
||||
return "c_mock"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockWith
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@MockWith
|
||||
class OneToMultiSvcTest {
|
||||
|
||||
private val aSvc = ASvc()
|
||||
private val bSvc = BSvc()
|
||||
private val cSvc = CSvc()
|
||||
|
||||
@Test
|
||||
fun should_able_to_test_multi_class_together() {
|
||||
Assertions.assertEquals("a_mock", aSvc.demo("test"))
|
||||
Assertions.assertEquals("b_mock", bSvc.demo("test"))
|
||||
Assertions.assertEquals("c_mock", cSvc.demo("test"))
|
||||
verify("a_format").withTimes(1)
|
||||
verify("b_format").withTimes(1)
|
||||
verify("c_format").withTimes(1)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.alibaba.testable.demo.util
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
|
||||
class PathUtilTest {
|
||||
|
||||
@MockMethod
|
||||
fun exists(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> true
|
||||
"/a/b/c" -> true
|
||||
else -> f.exists()
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun isDirectory(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b/c" -> true
|
||||
else -> f.isDirectory
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun delete(f: File): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun listFiles(f: File): Array<File>? {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> arrayOf(File("/a/b/c"), File("/a/b/d"))
|
||||
"/a/b/c" -> arrayOf(File("/a/b/c/e"))
|
||||
else -> f.listFiles()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_java_method_invoke_in_kotlin() {
|
||||
PathUtil.deleteRecursively(File("/a/b/"))
|
||||
verify("listFiles").withTimes(2)
|
||||
verify("delete").withTimes(4)
|
||||
}
|
||||
|
||||
}
|
||||
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,14 @@ 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?
|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
#### 7. Can `TestableMock` be used for testing Android projects?
|
||||
#### 6. 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.
|
||||
|
||||
#### 7. 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.6` version.
|
||||
@@ -9,26 +9,42 @@ In addition, before unit testing begin, it is often necessary to initialize spec
|
||||
|
||||
Just add `@EnablePrivateAccess` annotation to the test class, then you have got the following enhancements in the test case:
|
||||
|
||||
- Invoke private methods (including static methods) of the class under test
|
||||
- Read private fields (including static fields) of the class under test
|
||||
- Modify private fields (including static fields) of the class under test
|
||||
- Modify the constant fields of the class under test (fields modified with final, including static fields)
|
||||
- Invoke private methods (including static methods) of the **class under test**
|
||||
- Read private fields (including static fields) of the **class under test**
|
||||
- Modify private fields (including static fields) of the **class under test**
|
||||
- Modify the constant fields of the **class under test** (fields modified with final, including static fields)
|
||||
|
||||
When accessing and modifying private and constant members, the IDE may prompt some syntax errors, but the compiler will be able to run the test normally.
|
||||
|
||||
For the effect, see the use case in the test class of the `java-demo` sample project `DemoPrivateAccessTest`. (Using compile-time code enhancement, currently only the adaptation of the Java language is implemented)
|
||||
|
||||
> This function assumes that the test class is in the same package as the class under test, and the name is `<ClassUnderTest>+Test`. When this convention is not met, you can use the `srcClass` parameter on the `@EnablePrivateAccess` annotation to specify the actual class under test. E.g:
|
||||
>
|
||||
> ```java
|
||||
> @EnablePrivateAccess(srcClass = DemoServiceImpl.class)
|
||||
> class DemoServiceTest() { ... }
|
||||
> ```
|
||||
|
||||
### Solution 2: Use the `PrivateAccessor` tool class
|
||||
|
||||
If you don't want to see the IDE's syntax error reminder, or in a non-Java language JVM project (such as Kotlin language), you can also use the `PrivateAccessor` tool class to directly access private members.
|
||||
|
||||
This class provides 6 static methods:
|
||||
This class provides 7 static methods:
|
||||
|
||||
- `PrivateAccessor.get(<ObjectUnderTest>, "<private-field-name>")` ➜ read the private field of the class under test
|
||||
- `PrivateAccessor.set(<ObjectUnderTest>, "<private-field-name>", <new-value>)` ➜ modify the private field (or constant field) of the class under test
|
||||
- `PrivateAccessor.invoke(<ObjectUnderTest>, "<private-method-name>", <call-parameters>..)` ➜ call the private method of the class under test
|
||||
- `PrivateAccessor.getStatic(<ClassUnderTest>, "<private-static-field-name>")` ➜ read the **static** private field of the class under test
|
||||
- `PrivateAccessor.setStatic(<ClassUnderTest>, "<private-static-field-name>", <new-value>)` ➜ modify the **static** private field (or **static** constant field) of the class under test
|
||||
- `PrivateAccessor.invokeStatic(<ClassUnderTest>, "<private-static-method-name>", <call-parameters>..)` ➜ call the **static** private method of the class under test
|
||||
- `PrivateAccessor.get(<AnyObject>, "<private-field-name>")` ➜ read the private field of any object
|
||||
- `PrivateAccessor.set(<AnyObject>, "<private-field-name>", <new-value>)` ➜ modify the private field (or constant field) of any object
|
||||
- `PrivateAccessor.invoke(<AnyObject>, "<private-method-name>", <call-parameters>...)` ➜ call the private method of any object
|
||||
- `PrivateAccessor.getStatic(<AnyClass>, "<private-static-field-name>")` ➜ read the **static** private field of any class
|
||||
- `PrivateAccessor.setStatic(<AnyClass>, "<private-static-field-name>", <new-value>)` ➜ modify the **static** private field (or **static** constant field) of any class
|
||||
- `PrivateAccessor.invokeStatic(<AnyClass>, "<private-static-method-name>", <call-parameters>...)` ➜ call the **static** private method of any class
|
||||
- `PrivateAccessor.construct(<AnyClass>, <constructor-parameters>...)` ➜ create a new object by the private constructor of any class
|
||||
|
||||
> Using the `PrivateAccessor` class does not require the test class to have `@EnablePrivateAccess` annotation, but adding this annotation will enable the compile-time verification for the private members of the class under test.
|
||||
|
||||
For details, see the use cases in the test classes of the `java-demo` and `kotlin-demo` sample projects `DemoPrivateAccessTest`.
|
||||
|
||||
### Compile-time verification of private members
|
||||
|
||||
Both of the above two methods essentially use JVM reflection mechanism to achieve private member access, but the JVM compiler will not check the existence of the reflection target. When the code is refactored, if the private method names and parameters in the source class are modified, it would cause exceptions to be discovered only when the unit test is triggered. For this reason, another function of the `@EnablePrivateAccess` annotation is to perform additional compile-time checks for existence of private member of the **class under test**.
|
||||
|
||||
**Note**: When the private member verification function is enabled, the `PrivateAccessor` class can only be used to access the private members of the **class under test**, which will help limit the using of the `PrivateAccessor` tool class for "unauthorized" operations unrelated to the current test. If you really need to access private members of other classes, you can remove the `@EnablePrivateAccess` annotation, or set the `verifyTargetOnCompile` parameter of the annotation to `false` to manually turn off the verification function.
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
# Release Note
|
||||
|
||||
## 0.4.12
|
||||
- support verbose diagnose log for better self-troubleshooting
|
||||
- support disable private access target existence check
|
||||
- support specify mock scanning packages
|
||||
- fix an ArrayIndexOutOfBoundsException issue when transforming native method
|
||||
|
||||
## 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 +78,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.5.0</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.5.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.0')
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -76,11 +76,13 @@ After executing the void type method under test, use `InvokeVerifier.verify()` t
|
||||
class DemoTest {
|
||||
private Demo demo = new Demo();
|
||||
|
||||
// Intercept `System.out.println` invocation
|
||||
@MockMethod
|
||||
public void println(PrintStream ps, String msg) {
|
||||
// Execute the original call
|
||||
ps.println(msg);
|
||||
public static class Mock {
|
||||
// Intercept `System.out.println` invocation
|
||||
@MockMethod
|
||||
public void println(PrintStream ps, String msg) {
|
||||
// Execute the original call
|
||||
ps.println(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -16,25 +16,77 @@ The output log example is as follows:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
|
||||
[DIAGNOSE] Found 7 mock methods
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[DIAGNOSE] Line 14, mock method createBlackBox used
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[DIAGNOSE] Line 22, mock method innerFunc used
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
[DIAGNOSE] Line 27, mock method "staticFunc" used
|
||||
[DIAGNOSE] Handling method commonFunc
|
||||
[DIAGNOSE] Line 29, mock method trim used
|
||||
[DIAGNOSE] Line 29, mock method sub used
|
||||
[DIAGNOSE] Line 29, mock method startsWith used
|
||||
[DIAGNOSE] Handling method getBox
|
||||
[DIAGNOSE] Line 36, mock method secretBox used
|
||||
[DIAGNOSE] Handling method callerOne
|
||||
[DIAGNOSE] Line 43, mock method callFromDifferentMethod used
|
||||
[DIAGNOSE] Handling method callerTwo
|
||||
[DIAGNOSE] Line 47, mock method callFromDifferentMethod used
|
||||
[DIAGNOSE] Handling method innerFunc
|
||||
[DIAGNOSE] Handling method callFromDifferentMethod
|
||||
[DIAGNOSE] Line 34, mock method "trim" used
|
||||
[DIAGNOSE] Line 34, mock method "sub" used
|
||||
[DIAGNOSE] Line 34, mock method "startsWith" used
|
||||
... ...
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
For situations where expected mocking is not take effect, you could set the diagnosis level to `MockDiagnose.VERBOSE` for further investigation information.
|
||||
|
||||
```java
|
||||
@MockWith(diagnose = MockDiagnose.VERBOSE)
|
||||
class DemoTest {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Executing the unit test again will print out the runtime-signatures of all mock methods, and the runtime-signatures of all invocations scanned in the class under test:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
|
||||
[VERBOSE] Mock constructor "createBlackBox" as "(Ljava/lang/String;)V" for "com/alibaba/testable/demo/model/BlackBox"
|
||||
[VERBOSE] Mock method "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
|
||||
[VERBOSE] Mock method "staticFunc" as "()Ljava/lang/String;"
|
||||
[VERBOSE] Mock method "trim" as "()Ljava/lang/String;"
|
||||
[VERBOSE] Mock method "sub" as "(II)Ljava/lang/String;"
|
||||
[VERBOSE] Mock method "startsWith" as "(Ljava/lang/String;)Z"
|
||||
[VERBOSE] Mock method "secretBox" as "()Lcom/alibaba/testable/demo/model/BlackBox;"
|
||||
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[VERBOSE] Line 13, constructing "java/lang/Object" as "()V"
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[VERBOSE] Line 19, constructing "com/alibaba/testable/demo/model/BlackBox" as "(Ljava/lang/String;)V"
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[VERBOSE] Line 19, invoking "createBlackBox" as "(Ljava/lang/String;)Lcom/alibaba/testable/demo/model/BlackBox;"
|
||||
[VERBOSE] Line 20, invoking "get" as "()Ljava/lang/String;"
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[VERBOSE] Line 27, constructing "java/lang/StringBuilder" as "()V"
|
||||
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
|
||||
[VERBOSE] Line 27, invoking "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
[VERBOSE] Line 27, invoking "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
|
||||
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
|
||||
[VERBOSE] Line 27, invoking "staticFunc" as "()Ljava/lang/String;"
|
||||
[DIAGNOSE] Line 27, mock method "staticFunc" used
|
||||
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
|
||||
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
|
||||
[VERBOSE] Line 27, invoking "toString" as "()Ljava/lang/String;"
|
||||
... ...
|
||||
```
|
||||
|
||||
The logs are formatted in follow pattern:
|
||||
|
||||
- `Mock constructor "<MockMethodName>" as "<Signature>" for "<TypeName>"` Mock constructor found in test class
|
||||
- `Mock method "<MockMethodName>" as "<Signature>"` Mock method found in test class (the first parameter that identify the mock target class is currently kept)
|
||||
- `Line XX, constructing "<TypeName>" as "<Signature>"` Constructor invocation found in test under class
|
||||
- `Line XX, invoking "<MethodName>" as "<Signature>"` Member method invocation found in test under class
|
||||
|
||||
49
docs/en-us/doc/upgrade-to-v05.md
Normal file
49
docs/en-us/doc/upgrade-to-v05.md
Normal file
@@ -0,0 +1,49 @@
|
||||
Upgrade to version 0.5
|
||||
---
|
||||
|
||||
After nearly a month of design and development, the `0.5` version of TestableMock has finally come out. Compared with the `0.4` version, the new version solves the three historical problems left over before:
|
||||
|
||||
1. <s>**Mock method cannot call other non-static methods**</s>. The mock method in the new version no longer has any difference from the ordinary method, and can access any external method and member variable.
|
||||
2. <s>**Mock method always acts on the entire test life cycle**</s>. From now on, the mock method supports restricting the effective scope to the test cases in the test class of which it belongs, so there is no need to worry about accidentally mocking cross-class test invocations.
|
||||
3. <s>**The MOCK_CONTEXT needs manually cleaned up and only supports class-level parallel testing**</s>. Now each test case has an independent `MOCK_CONTEXT` variable, no need to clean up after used, and you can use unit test with any parallel level.
|
||||
|
||||
In order to better realize the reuse of Mock methods, we have made a clear boundary between the mock class and the test class in the new version. When upgrading from `0.4` to `0.5`, the only change required is to wrap all mock methods in the test class with a `public static class Mock {}`.
|
||||
|
||||
For example, the original test class definition was as follows:
|
||||
|
||||
```java
|
||||
public class DemoMockTest {
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "hello_" + text;
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After upgrading to the `0.5` version, move all mock methods (in this example, only the `innerFunc` method) to a static inner class named `Mock`, which is equivalent to adding two lines of code:
|
||||
|
||||
```java
|
||||
public class DemoMockTest {
|
||||
|
||||
public static class Mock { // Add this line
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "hello_" + text;
|
||||
}
|
||||
} // Add this line
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then upgrade the `TestableMock` dependency in the `pom.xml` or `build.gradle` file to `0.5.0` or above.
|
||||
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,100 @@ 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 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
|
||||
> - Mock non-constructive method, copy the original method definition to the mock class, add a `@MockMethod` annotation
|
||||
> - Mock construction method, copy the original method definition to the mock 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.
|
||||
|
||||
The detail mock method definition convention is as follows:
|
||||
#### 0. Pre-step, prepare the mock class
|
||||
|
||||
First, create a mock class as the container for mock methods associated with the test class. The simplest way is to add a static inner class named `Mock` to the test class. E.g:
|
||||
|
||||
```java
|
||||
public class DemoTest {
|
||||
|
||||
public static class Mock {
|
||||
// mock methods goes here
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
#### 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 mock 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:
|
||||
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 mock 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:
|
||||
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 mock 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");
|
||||
}
|
||||
```
|
||||
@@ -84,7 +106,7 @@ For complete code examples, see the `should_able_to_mock_static_method()` test c
|
||||
|
||||
#### 4. Mock `new` operation of any type
|
||||
|
||||
Define an ordinary method annotated with `@MockContructor` in the test class, make the return value type of the method the type of the object to be created, and the method parameters are exactly the same as the constructor parameters to be mocked, the method name is arbitrary.
|
||||
Define an ordinary method annotated with `@MockContructor` in the mock class, make the return value type of the method the type of the object to be created, and the method parameters are exactly the same as the constructor parameters to be mocked, the method name is arbitrary.
|
||||
|
||||
At this time, all operations in the class under test that use `new` to create the specified class (and use the constructor that is consistent with the mock method parameters) will be replaced with calls to the custom method.
|
||||
|
||||
@@ -103,11 +125,37 @@ private BlackBox createBlackBox(String text) {
|
||||
|
||||
For complete code examples, see the `should_able_to_mock_new_object()` test case in the `java-demo` and `kotlin-demo` sample projects.
|
||||
|
||||
#### 5. Identify the current test case and invoke source
|
||||
#### 5. Identify different invocation source in mock method
|
||||
|
||||
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());
|
||||
}
|
||||
```
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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 +164,4 @@ 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.
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
- 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
|
||||
- [Upgrade To 0.5 Version](en-us/doc/upgrade-to-v05.md)
|
||||
- [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>'
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5,7 +5,7 @@ TestableMock简介
|
||||
|
||||
在定义Mock方法时,开发者真正关心的只有一件事:"<u>这个调用,在测试的时候要换成那个假的Mock方法</u>"。
|
||||
|
||||
然而当下主流的Mock框架在实现Mock功能时,需要开发者操心的事情实在太多:Mock框架如何初始化、与所用的单元测试框架是否兼容、要被Mock的方法是不是私有的、是不是静态的、被Mock对象是new出来的还是注入的、怎样把被测对象送回被测类里...这些非关键的额外工作极大分散了使用Mock工具应有的乐趣。
|
||||
然而当下主流的Mock框架在实现Mock功能时,需要开发者操心的事情实在太多:Mock框架如何初始化、与所用的服务框架是否兼容、要被Mock的方法是不是私有的、是不是静态的、被Mock对象是new出来的还是注入的、怎样把被测对象送回被测类里...这些非关键的额外工作极大分散了使用Mock工具应有的乐趣。
|
||||
|
||||
于是,我们开发了`TestableMock`,**一款特立独行的轻量Mock工具**。
|
||||
|
||||
|
||||
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语言遗留的测试难题。与其束手束脚的采用“测试技巧”间接测试功能,不如索性解除限制,就让单元测试来得更猛烈一些吧!🤠
|
||||
61
docs/zh-cn/doc/annotations.md
Normal file
61
docs/zh-cn/doc/annotations.md
Normal file
@@ -0,0 +1,61 @@
|
||||
注解参数清单
|
||||
---
|
||||
|
||||
基于轻量的原则,`TestableMock`为开发者提供了尽可能精炼、易用的注解组合,以下参数信息可供开发参考。
|
||||
|
||||
#### @EnablePrivateAccess
|
||||
|
||||
启用对被测类的<u>私有成员访问编译期增强</u>和<u>私有目标存在性的编译期校验</u>功能。
|
||||
|
||||
- 作用于:测试类
|
||||
|
||||
| 参数 | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| srcClass | Class | 否 | N/A | 当测试类命名不符合约定时,指定实际被测类 |
|
||||
| verifyTargetOnCompile | boolean | 否 | true | 是否启用私有目标的编译期存在性校验 |
|
||||
|
||||
#### @MockMethod
|
||||
|
||||
将当前方法标识为待匹配的Mock成员方法。
|
||||
|
||||
- 作用于:Mock容器类中的方法
|
||||
|
||||
| 参数 | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| targetClass | Class | 否 | N/A | 指定Mock目标的调用者类型 |
|
||||
| targetMethod | String | 否 | N/A | 指定Mock目标的方法名 |
|
||||
| scope | MockScope | 否 | MockScope.GLOBAL | 指定Mock的生效范围 |
|
||||
|
||||
#### @MockConstructor
|
||||
|
||||
将当前方法标识为待匹配的Mock构造方法。
|
||||
|
||||
- 作用于:Mock容器类中的方法
|
||||
|
||||
| 参数 | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| scope | MockScope | 否 | MockScope.GLOBAL | 指定Mock的生效范围 |
|
||||
|
||||
|
||||
#### @MockWith
|
||||
|
||||
显式指定当前类型关联的Mock容器类。
|
||||
|
||||
- 作用于:测试类、被测类
|
||||
|
||||
| 参数(`N/A`为默认参数) | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| N/A | Class | 否 | NullType.class | 指定使用的Mock容器类 |
|
||||
| treatAs | ClassType | 否 | ClassType.GuessByName | 指定当前类是测试类或被测类 |
|
||||
| diagnose | LogLevel | 否 | N/A | (**deprecated**)指定Mock诊断日志级别 |
|
||||
|
||||
#### @MockDiagnose
|
||||
|
||||
启用或禁止Mock相关的诊断信息输出。
|
||||
|
||||
- 作用于:Mock容器类
|
||||
|
||||
| 参数(`N/A`为默认参数) | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| N/A | LogLevel | 是 | N/A | 指定当前Mock容器关联测试用例的诊断日志级别 |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#### 2. `TestableMock`是否能够与其他Mock工具一起使用?
|
||||
|
||||
`TestableMock`可与其他基于动态代理机制的Mock工具安全的共同使用,譬如`Mockito`、`EasyMock`、`MockRunner`等皆属此范畴。
|
||||
`TestableMock`可与其他基于动态代理机制的Mock工具安全的共同使用,譬如`Mockito`、`Spock`、`EasyMock`等皆属此范畴。
|
||||
|
||||
对于会修改类加载器或被测类字节码的Mock工具,譬如`PowerMock`和`JMockit`,尚无案例证明会与`TestableMock`发生冲突,但从原理来说二者可能存在不兼容风险,请谨慎使用。
|
||||
|
||||
@@ -35,14 +35,14 @@ Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.S
|
||||
|
||||
实际场景中需要对`String`类中的方法进行Mock的场景很少,`TestableMock`暂未对这种情况做特别处理。
|
||||
|
||||
#### 6. 在IntelliJ IDE 2020.3版本中运行单个测试用例时,用了`@EnablePrivateAccess`注解还是报私有成员访问错误?
|
||||
|
||||
IntelliJ从`2020.2.2`版本以后,IntelliJ对`JSR-269`规范注解处理器的处理机制发生了变化,与Maven标准不再完全兼容。可通过IntelliJ系统配置的"Build Tools > Maven > Runner"中开启"Delegate IDE build/run actions to maven"选项解决:
|
||||
|
||||

|
||||
|
||||
#### 7. `TestableMock`能否用于Android项目的测试?
|
||||
#### 6. `TestableMock`能否用于Android项目的测试?
|
||||
|
||||
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。
|
||||
|
||||
Android系统的`Dalvik`和`ART`虚拟机采用了与标准JVM不同的字节码体系,会影响`TestableMock`的正常工作。`Roboelectric`框架能在普通JVM虚拟机上运行Android单元测试,其速度比通过Android虚拟机运行单元测试快非常多,绝大多数Android App的单元测试都在使用`Roboelectric`框架。
|
||||
|
||||
#### 7. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
|
||||
|
||||
这个问题是由于系统ClassPath包含太多路径所致,与是否使用`TestableMock`无关。但需要注意的是,IntelliJ提供了两种辅助解决机制:`JAR manifest`和`classpath file`,若测试中使用了`TestableMock`,请选择`JAR manifest`。
|
||||
|
||||

|
||||
|
||||
45
docs/zh-cn/doc/javaagent-args.md
Normal file
45
docs/zh-cn/doc/javaagent-args.md
Normal file
@@ -0,0 +1,45 @@
|
||||
全局运行参数
|
||||
---
|
||||
|
||||
`TestableMock`的许多功能采用了基于JavaAgent的运行时字节码修改实现。在JavaAgent启动时,可以通过额外的全局参数来调整`TestableMock`的执行过程。
|
||||
|
||||
### 可用参数清单
|
||||
|
||||
| 参数 | 描述 | 可用值和示例(`N/A`表示无需赋值) |
|
||||
| ---- | ---- | ---- |
|
||||
| logLevel | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
|
||||
| dumpPath | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 例如 `/tmp/bytecode`(需要此目录事先存在) |
|
||||
| pkgPrefix | 限定`TestableMock`仅对部分包生效 | 使用`,`分隔的包路径前缀列表,例如 `com.demo.svc,com.demo.dao` |
|
||||
| mockScope | 修改默认的Mock生效范围(详见[Mock生效范围](zh-cn/doc/scope-of-mock)) | 可用值为:`global`(全局生效) / `associated`(只对关联的测试用例生效) |
|
||||
| useThreadPool | 启用基于`TransmittableThreadLocal`的Mock上下文存储(用于包含线程池的测试用例) | `N/A` |
|
||||
|
||||
### 参数的连接
|
||||
|
||||
若参数有值,参数名和值之间用`=`符合连接。例如:
|
||||
|
||||
`useThreadPool`、`logLevel=debug`、`dumpPath=/tmp/debug`
|
||||
|
||||
多个参数之间使用`&`符号连接,例如:
|
||||
|
||||
`useThreadPool&logLevel=debug`、`logLevel=debug&dumpPath=/tmp/debug`
|
||||
|
||||
### 添加运行参数
|
||||
|
||||
与其他基于JavaAgent的工具相似,`TestableMock`通过在单元测试引入`testable-agent`包的末尾加上一个`=`符号,然后连接额外参数来传递用户的自定义参数。
|
||||
|
||||
对于Maven项目,可将参数追加到`maven-surefire-plugin`参数`testable-agent`包尾部,紧接着`.jar`的位置。例如:
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar=mockScope=associated&pkgPrefix=com.demo.</argLine>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
> 注意:在`xml`文件中,连接参数的`&`符号需要写为`&`
|
||||
|
||||
对于Gradle项目,同样是直接将参数追加到引入`testable-agent`的配置末尾。例如:
|
||||
|
||||
|
||||
```groovy
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}=mockScope=associated&pkgPrefix=com.demo."
|
||||
```
|
||||
63
docs/zh-cn/doc/mock-method-reusing.md
Normal file
63
docs/zh-cn/doc/mock-method-reusing.md
Normal file
@@ -0,0 +1,63 @@
|
||||
复用Mock类与方法
|
||||
---
|
||||
|
||||
Don't Repeat Yourself是软件开发过程当中的一项重要原则(即“DRY原则”),在编写测试代码时,有些通用的基础功能调用语句常常出现在许多相似的业务类里,若测试中需要Mock这些调用,就要在各个测试类中重复提供同样的Mock方法。而通过Mock方法的复用机制,能够很好的避免编写臃肿重复Mock代码的麻烦。
|
||||
|
||||
TestableMock支持两种粒度的Mock复用方式:<u>复用Mock类</u>和<u>复用Mock方法</u>。
|
||||
|
||||
## 复用Mock类
|
||||
|
||||
如果有两个或以上测试类需要Mock的方法近乎相同,那么采用类级别的Mock复用就是最省心的一种方式。
|
||||
|
||||
进行类级别的Mock复用,只需将Mock容器定义为独立的类,然后在要使用它的测试类上通过`@MockWith`进行引用。例如:
|
||||
|
||||
|
||||
```java
|
||||
@MockWith(ServiceMock.class)
|
||||
public class AaaServiceTest {
|
||||
...
|
||||
}
|
||||
|
||||
@MockWith(ServiceMock.class)
|
||||
public class BbbServiceTest {
|
||||
...
|
||||
}
|
||||
|
||||
public class ServiceMock {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
这样在`AaaServiceTest`和`BbbServiceTest`类中的测试用例在执行时,都会用`ServiceMock`容器类中定义的Mock方法进行调用匹配和Mock替换。
|
||||
|
||||
## 复用Mock方法
|
||||
|
||||
实际场景中,相比一次性复用整个Mock类的情况,更常见的是对部分高频Mock方法进行复用。
|
||||
|
||||
Mock方法的复用可以通过Mock容器类的继承来实现,父类中定义的所有Mock方法都会在子类中自然存在,例如:
|
||||
|
||||
```java
|
||||
public class AaaServiceTest {
|
||||
public static class Mock extends BasicMock {
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
public class BbbServiceTest {
|
||||
public static class Mock extends BasicMock {
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
public class BasicMock {
|
||||
@MockMethod(targetClass = UserDao.class)
|
||||
protected String getById(int id) {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
则名为`getById`的Mock方法在`AaaServiceTest`和`BbbServiceTest`的测试用例执行时都会生效。
|
||||
|
||||
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.6`版本中推出。
|
||||
@@ -9,26 +9,42 @@
|
||||
|
||||
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
|
||||
|
||||
- 调用被测类的私有方法(包括静态方法)
|
||||
- 读取被测类的私有字段(包括静态字段)
|
||||
- 修改被测类的私有字段(包括静态字段)
|
||||
- 修改被测类的常量字段(使用final修饰的字段,包括静态字段)
|
||||
- 调用**被测类**的私有方法(包括静态方法)
|
||||
- 读取**被测类**的私有字段(包括静态字段)
|
||||
- 修改**被测类**的私有字段(包括静态字段)
|
||||
- 修改**被测类**的常量字段(使用final修饰的字段,包括静态字段)
|
||||
|
||||
访问和修改私有、常量成员时,IDE可能会提示语法有误,但编译器将能够正常运行测试。(使用编译期代码增强,目前仅实现了Java语言的适配)
|
||||
|
||||
效果见`java-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
|
||||
|
||||
> 此功能默认假设测试类与被测类同包,且名称为`被测类+Test`。当不符合此约定时,可在测试类的`@EnablePrivateAccess`注解上使用`srcClass`参数指定实际的被测类。例如:
|
||||
>
|
||||
> ```java
|
||||
> @EnablePrivateAccess(srcClass = DemoServiceImpl.class)
|
||||
> class DemoServiceTest() { ... }
|
||||
> ```
|
||||
|
||||
### 方法二:使用`PrivateAccessor`工具类
|
||||
|
||||
若不希望看到IDE的语法错误提醒,或是在非Java语言的JVM工程(譬如Kotlin语言)里,也可以借助`PrivateAccessor`工具类来直接访问私有成员。
|
||||
若不希望看到IDE的语法错误提醒,或是在非Java语言的JVM工程(譬如Kotlin语言)里,可以借助`PrivateAccessor`工具类来直接访问私有成员。
|
||||
|
||||
这个类提供了6个静态方法:
|
||||
这个类提供了7个静态方法:
|
||||
|
||||
- `PrivateAccessor.get(被测对象, "私有字段名")` ➜ 读取被测类的私有字段
|
||||
- `PrivateAccessor.set(被测对象, "私有字段名", 新的值)` ➜ 修改被测类的私有字段(或常量字段)
|
||||
- `PrivateAccessor.invoke(被测对象, "私有方法名", 调用参数..)` ➜ 调用被测类的私有方法
|
||||
- `PrivateAccessor.getStatic(被测类型, "私有静态字段名")` ➜ 读取被测类的**静态**私有字段
|
||||
- `PrivateAccessor.setStatic(被测类型, "私有静态字段名", 新的值)` ➜ 修改被测类的**静态**私有字段(或**静态**常量字段)
|
||||
- `PrivateAccessor.invokeStatic(被测类型, "私有静态方法名", 调用参数..)` ➜ 调用被测类的**静态**私有方法
|
||||
- `PrivateAccessor.get(任意对象, "私有字段名")` ➜ 读取任意类的私有字段
|
||||
- `PrivateAccessor.set(任意对象, "私有字段名", 新的值)` ➜ 修改任意类的私有字段(或常量字段)
|
||||
- `PrivateAccessor.invoke(任意对象, "私有方法名", 调用参数...)` ➜ 调用任意类的私有方法
|
||||
- `PrivateAccessor.getStatic(任意类型, "私有静态字段名")` ➜ 读取任意类的**静态**私有字段
|
||||
- `PrivateAccessor.setStatic(任意类型, "私有静态字段名", 新的值)` ➜ 修改任意类的**静态**私有字段(或**静态**常量字段)
|
||||
- `PrivateAccessor.invokeStatic(任意类型, "私有静态方法名", 调用参数...)` ➜ 调用任意类的**静态**私有方法
|
||||
- `PrivateAccessor.construct(任意类型, 构造方法参数...)` ➜ 调用任意类的私有构造方法
|
||||
|
||||
> 使用`PrivateAccessor`工具类并不需要测试类具有`@EnablePrivateAccess`注解,但加上此注解将开启被测类私有成员的编译期校验功能。
|
||||
|
||||
详见`java-demo`和`kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
|
||||
|
||||
### 私有成员编译期校验
|
||||
|
||||
上述两种私有成员访问功能的本质都是利用了JVM的反射机制,但JVM编译器不会检查反射目标的存在性,当代码重构时,如果对源类型中的私有方法名称、参数进行了修改,就会导致错误要在单元测试运行时才能被发现。为此,`@EnablePrivateAccess`注解的另一项功能是为**被测类**的私有成员访问进行额外的编译期校验。
|
||||
|
||||
**注意**:当私有成员校验功能开启时,`PrivateAccessor`工具类将只能用于访问**被测类**的私有成员,这会有助于限制将`PrivateAccessor`工具类用于与当前测试无关的“越权”操作。如果确实需要访问其他类的私有成员,可将`@EnablePrivateAccess`注解移除,或将注解的`verifyTargetOnCompile`参数设为`false`,手工关闭校验功能。
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
# Release Note
|
||||
|
||||
## 0.5.0
|
||||
- 分离测试类与Mock类,实现Mock类和Mock方法的复用机制
|
||||
- 支持测试类与被测类在不同包路径的情况下实施Mock
|
||||
- 支持将Mock方法生效范围缩小为所属测试类的测试用例
|
||||
- 使用`TransmittableThreadLocal`替换基于线程堆栈的Mock上下文识别机制
|
||||
- 增加专用于输出诊断信息的`@MockDiagnose`注解
|
||||
|
||||
## 0.4.12
|
||||
- 支持`VERBOSE`级别的Mocking过程日志,增强错误自助排查能力
|
||||
- 支持使用`verifyTargetOnCompile`参数禁用编译期私有目标校验功能
|
||||
- 支持通过agent参数指定Mock目标的扫描包范围
|
||||
- 修复一处`ArrayIndexOutOfBoundsException`异常 (issue-52)
|
||||
|
||||
## 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)
|
||||
|
||||
27
docs/zh-cn/doc/scope-of-mock.md
Normal file
27
docs/zh-cn/doc/scope-of-mock.md
Normal file
@@ -0,0 +1,27 @@
|
||||
Mock的生效范围
|
||||
---
|
||||
|
||||
在`@MockMethod`和`@MockConstructor`注解上都有一个`scope`参数,其可选值有两种
|
||||
|
||||
- `MockScope.GLOBAL`:该Mock方法将全局生效
|
||||
- `MockScope.ASSOCIATED`:该Mock方法仅对Mock容器关联测试类中的测试用例生效
|
||||
|
||||
举例来说,`AaaService`和`BbbService`是两个需要被测试的类,在`BbbService`的代码里有个`recordTicket()`调用依赖外部系统。因此在进行单元测试时,开发者在`BbbService`关联的Mock容器里使用`@MockMethod`注解定义了这个调用的替代方法。此时,若该Mock方法的`scope`值为`MockScope.GLOBAL`,则不论是在`AaaServiceTest`测试类还是在`BbbServiceTest`测试类的测试用例,只要直接或间接的执行到这行调用,都会被置换为调用Mock方法。若该Mock方法的`scope`值为`MockScope.ASSOCIATED`,则Mock只对`BbbServiceTest`类中的测试用例生效,而`AaaServiceTest`类中的测试用例在运行过程中执行到了`BbbService`类的相关代码,将会执行`recordTicket()`的原本调用。
|
||||
|
||||
对于常规项目而言,单元测试里需要被Mock的调用都是由于其中包含了不需要或不便于测试的逻辑,譬如“依赖外部系统”、“包含随机结果”、“执行非常耗时”等等,这类调用在整个单元测试的生命周期里都应该被Mock方法置换,不论调用的发起者是谁。因此`TestableMock`默认所有Mock方法都是全局生效的,即`scope`默认值为`MockScope.GLOBAL`。
|
||||
|
||||
在一些大型项目中,会有“下层模块编写单元测试,上层模块编写端到端集成测试,两者混合在一起运行”的情况,这时候大部分Mock方法都应该使用`MockScope.ASSOCIATED`作为生效范围。针对这种情况,`TestableMock`支持通过`mockScope`运行参数来修改默认的Mock方法生效范围,详见[全局运行参数](zh-cn/doc/javaagent-args.md)文档。
|
||||
|
||||
> 特别说明。若要Mock静态块里的调用,Mock方法的`scope`必须为`MockScope.GLOBAL`,因为静态块中的代码在程序初始化时就会执行,不属于任何测试用例。典型场景是在使用JNI开发的项目中Mock系统库的加载方法。
|
||||
> ```java
|
||||
> static {
|
||||
> System.loadLibrary("native-lib");
|
||||
> }
|
||||
> ```
|
||||
> 若默认的`scope`参数不是`MockScope.GLOBAL`,则相应Mock方法应当显式的声明`scope`值,例如:
|
||||
> ```java
|
||||
> @MockMethod(targetClass = System.class, scope = MockScope.GLOBAL)
|
||||
> private void loadLibrary(String libname) {
|
||||
> System.err.println("loadLibrary " + libname);
|
||||
> }
|
||||
> ```
|
||||
@@ -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.5.0</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.5.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.0')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -77,18 +77,20 @@ class Demo {
|
||||
class DemoTest {
|
||||
private Demo demo = new Demo();
|
||||
|
||||
// 拦截`System.out.println`调用
|
||||
@MockMethod
|
||||
public void println(PrintStream ps, String msg) {
|
||||
// 执行原调用
|
||||
ps.println(msg);
|
||||
public static class Mock {
|
||||
// 拦截System.out.println调用
|
||||
@MockMethod
|
||||
public void println(PrintStream ps, String msg) {
|
||||
// 执行原调用
|
||||
ps.println(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecordAction() {
|
||||
Action action = new Action("click", ":download");
|
||||
demo.recordAction();
|
||||
// 验证Mock方法`println`被调用,且传入参数符合预期
|
||||
// 验证Mock方法println被调用,且传入参数格式符合预期
|
||||
verify("println").with(matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
自助问题排查
|
||||
---
|
||||
|
||||
相比`Mockito`等由开发者手工放置Mock类的做法,`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也有可能发生预料之外的Mock替换。
|
||||
相比`Mockito`等由开发者手工放置Mock类的做法,`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也容易导致对“Mock究竟有没生效”的疑问。
|
||||
|
||||
若要排查Mock相关的问题,只需在测试类上添加`@MockWith`注解,并配置参数`diagnose`值为`MockDiagnose.ENABLE`,在运行测试时就会打印出详细的Mock方法替换过程。
|
||||
若要排查Mock相关的问题,只需在相应的**Mock容器类**上添加`@MockDiagnose`注解,并配置参数值为`LogLevel.ENABLE`,在运行测试时就会打印出详细的Mock方法替换过程。例如:
|
||||
|
||||
```java
|
||||
@MockWith(diagnose = MockDiagnose.ENABLE)
|
||||
class DemoTest {
|
||||
...
|
||||
@MockDiagnose(LogLevel.ENABLE)
|
||||
public static class Mock {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -16,25 +18,80 @@ class DemoTest {
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
|
||||
[DIAGNOSE] Found 6 test cases
|
||||
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/DemoMockTest$Mock
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
|
||||
[DIAGNOSE] Found 7 mock methods
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[DIAGNOSE] Line 14, mock method createBlackBox used
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[DIAGNOSE] Line 22, mock method innerFunc used
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
[DIAGNOSE] Line 27, mock method "staticFunc" used
|
||||
[DIAGNOSE] Handling method commonFunc
|
||||
[DIAGNOSE] Line 29, mock method trim used
|
||||
[DIAGNOSE] Line 29, mock method sub used
|
||||
[DIAGNOSE] Line 29, mock method startsWith used
|
||||
[DIAGNOSE] Handling method getBox
|
||||
[DIAGNOSE] Line 36, mock method secretBox used
|
||||
[DIAGNOSE] Handling method callerOne
|
||||
[DIAGNOSE] Line 43, mock method callFromDifferentMethod used
|
||||
[DIAGNOSE] Handling method callerTwo
|
||||
[DIAGNOSE] Line 47, mock method callFromDifferentMethod used
|
||||
[DIAGNOSE] Handling method innerFunc
|
||||
[DIAGNOSE] Handling method callFromDifferentMethod
|
||||
[DIAGNOSE] Line 34, mock method "trim" used
|
||||
[DIAGNOSE] Line 34, mock method "sub" used
|
||||
[DIAGNOSE] Line 34, mock method "startsWith" used
|
||||
... ...
|
||||
```
|
||||
|
||||
该日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
|
||||
其中`Line XX, mock method "XXX" used`日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
|
||||
|
||||
简单排查方法:
|
||||
|
||||
- 若没有任何输出,请检查`pom.xml`或`build.gradle`配置是否正确引入了`TestableMock`依赖
|
||||
- 若只输出了`Handling mock class`,请检查Mock容器类的名称和位置是否符合规范
|
||||
- 若只输出了`Handling mock class`和`Handling test class`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test",或者是否正确的使用了`@MockWith`注解
|
||||
- 若输出了`Handling source class`以及`Handling method xxx`,但预期的代码行位置没有发生Mock替换,请继续检查Mock方法定义是否未与目标方法匹配
|
||||
|
||||
对于上述的最后一种情况(预期Mock未生效),可将日志级别提升到`LogLevel.VERBOSE`做进一步排查。例如:
|
||||
|
||||
```java
|
||||
class DemoTest {
|
||||
@MockDiagnose(LogLevel.VERBOSE)
|
||||
public static class Mock {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
再次执行单元测试,此时将会打印出所有Mock方法的运行期签名,以及被测类中扫描到所有调用的运行期签名:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
|
||||
[VERBOSE] Test case "should_able_to_mock_new_object"
|
||||
... ...
|
||||
[VERBOSE] Test case "should_able_to_set_mock_context"
|
||||
[DIAGNOSE] Found 6 test cases
|
||||
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/DemoMockTest$Mock
|
||||
[VERBOSE] Mock constructor "createBlackBox" as "(Ljava/lang/String;)V" for "com/alibaba/testable/demo/model/BlackBox"
|
||||
[VERBOSE] Mock method "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
|
||||
... ...
|
||||
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[VERBOSE] Line 13, constructing "java/lang/Object" as "()V"
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[VERBOSE] Line 19, constructing "com/alibaba/testable/demo/model/BlackBox" as "(Ljava/lang/String;)V"
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[VERBOSE] Line 19, invoking "createBlackBox" as "(Ljava/lang/String;)Lcom/alibaba/testable/demo/model/BlackBox;"
|
||||
[VERBOSE] Line 20, invoking "get" as "()Ljava/lang/String;"
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[VERBOSE] Line 27, constructing "java/lang/StringBuilder" as "()V"
|
||||
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
|
||||
[VERBOSE] Line 27, invoking "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
... ...
|
||||
```
|
||||
|
||||
输出日志结构参考如下:
|
||||
|
||||
- `Mock constructor "<Mock方法名>" as "<方法签名>" for "<类型>"` 在测试类中扫描到的**Mock构造方法**及其运行期签名
|
||||
- `Mock method "<Mock方法名>" as "<方法签名>"` 在测试类中扫描到的**普通Mock方法**及其运行期签名
|
||||
- `Line XX, constructing "<类型>" as "<方法签名>"` 在被测类中扫描掉的**构造方法调用**及其运行期签名
|
||||
- `Line XX, invoking "<方法名>" as "<方法签名>"` 在被测类中扫描到的**成员方法调用**及其运行期签名
|
||||
|
||||
"运行期签名"是目标方法参数和返回值类型的在字节码中的表示形式,其结构相比方法的原始Java签名更紧凑精炼,通过对比相应代码行调用时的方法签名与Mock方法的实际签名,通常能够快速定位出Mock未匹配的原因。
|
||||
|
||||
> 在`0.4.x`版本使用测试类添加`@MockWith`注解的`diagnose`参数来启用诊断信息的方法在`0.5`版本中依然可用,但将在未来版本中移除,请优先使用`@MockDiagnose`注解替代。
|
||||
|
||||
49
docs/zh-cn/doc/upgrade-to-v05.md
Normal file
49
docs/zh-cn/doc/upgrade-to-v05.md
Normal file
@@ -0,0 +1,49 @@
|
||||
升级到0.5版本
|
||||
---
|
||||
|
||||
经过近一个月的设计和开发,TestableMock的`0.5`版本终于和大家见面了。相比`0.4`版本,新版解决了此前遗留的三大历史问题:
|
||||
|
||||
1. <s>**Mock方法无法调用其他非静态方法**</s>。新版中的Mock方法与普通方法不再有任何差别,可以访问任意外部方法和成员变量。
|
||||
2. <s>**Mock方法总是作用于整个测试生命周期**</s>。从现在开始,Mock方法支持将生效范围限定为**所属测试类里的测试用例**,不用担心跨类测试调用被意外Mock掉了。
|
||||
3. <s>**需手工清理MOCK_CONTEXT且只支持类粒度的并行测试**</s>。现在每个测试用例拥有了独立的`MOCK_CONTEXT`变量,无需清理也不会串号,而且可以放心使用**任意粒度**的并行单元测试啦。
|
||||
|
||||
在使用方式上,`0.5`版本延续`TestableMock`简洁轻量的原则,同时为了更好的实现Mock方法复用,新版本的Mock类与测试类之间有了明确的边界。从`0.4`版本升级到`0.5`时,唯一需要的改变是将测试类中的所有Mock方法使用一个`public static class Mock { }`包裹起来。
|
||||
|
||||
例如,原先有如下测试类定义:
|
||||
|
||||
```java
|
||||
public class DemoMockTest {
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "hello_" + text;
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
升级为`0.5`版本后,将所有Mock方法(此例中只有`innerFunc`这一个方法)移到一个名称为`Mock`的静态内部类中,相当于增加两行代码:
|
||||
|
||||
```java
|
||||
public class DemoMockTest {
|
||||
|
||||
public static class Mock { // 增加此行
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "hello_" + text;
|
||||
}
|
||||
} // 增加此行
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后将`pom.xml`或`build.gradle`文件中的TestableMock依赖升级到`0.5.0`或以上版本即可。
|
||||
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为例,打开运行菜单的"编辑配置..."选型,如图中位置①
|
||||
|
||||
|
||||
97
docs/zh-cn/doc/use-mock-with.md
Normal file
97
docs/zh-cn/doc/use-mock-with.md
Normal file
@@ -0,0 +1,97 @@
|
||||
使用MockWith注解
|
||||
---
|
||||
|
||||
`@MockWith`注释的功能是为测试显式指定Mock容器,通常用于测试类或Mock容器类没有在标准约定位置的情况,以下列举几种典型使用场景。
|
||||
|
||||
### 1. 非标准位置的Mock容器类
|
||||
|
||||
`TestableMock`会依次在以下两个位置寻找Mock容器:
|
||||
|
||||
- 默认位置测试类中名为`Mock`的静态内部类(譬如原类型是`Demo`,Mock容器类为`DemoTest.Mock`)
|
||||
- 同包路径下名为`被测类+Mock`的独立类(譬如原类型是`Demo`,Mock容器类为`DemoMock`)
|
||||
|
||||
倘若实际要使用的Mock容器类不在这两个位置,就需要在测试类上使用`@MockWith`注释了。一般来说,造成Mock容器类不在默认位置的原因可能有两种:复用Mock容器、集中管理Mock容器。
|
||||
|
||||
当对一批功能近似的类型进行测试的时候,由于需要进行Mock的外部调用基本一致,可以将这些类型所需的所有Mock方法集中写在一个Mock容器类里,然后让相关测试类共同引用这个公共Mock容器。详见[复用Mock类与方法](zh-cn/doc/mock-method-reusing.md)文档。
|
||||
|
||||
另一种情况是开发者希望将Mock方法的定义与测试类本身分开,以便进行集中管理(或规避某些扫描工具的路径规则),譬如形成下面这种目录结构:
|
||||
|
||||
```
|
||||
src/
|
||||
main/
|
||||
com/
|
||||
demo/
|
||||
service/
|
||||
DemoService.java
|
||||
test/
|
||||
com/
|
||||
demo/
|
||||
service/
|
||||
DemoServiceTest.java
|
||||
mock/
|
||||
service/
|
||||
DemoServiceMock.java
|
||||
```
|
||||
|
||||
此时需要在测试类上显式的指定相应的Mock容器类,不过这种情况在实际中并不太常见。
|
||||
|
||||
### 2. 非标准位置的测试类
|
||||
|
||||
从`TestableMock`的原理来说,测试类的位置其实只是作为“被测类”与“Mock容器类”之间建立关联的参照物。当测试类的位置不是默认约定的`被测类+Test`时,上述首选的Mock容器位置就不成立了。但此时次选Mock位置依然可用,即如果Mock容器类的位置是`被测类+Mock`,那么Mock置换就依然能够正常进行。
|
||||
|
||||
但此时测试类与Mock容器之间的关联丢失了,因此需要为测试类使用`@MockWith`注解来显式的建立关联。例如:
|
||||
|
||||
```java
|
||||
public class DemoService { // 被测类
|
||||
...
|
||||
}
|
||||
|
||||
public class DemoServiceMock { // Mock容器类
|
||||
...
|
||||
}
|
||||
|
||||
@MockWith(DemoServiceMock.class) // 测试类由于丢失与Mock容器的关联,需要@MockWith注解
|
||||
public class ServiceTest {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
另一种相对少见的情况是Mock容器采用测试类的静态内部类,但测试类由于某些原因无法置于约定位置(或无法遵循约定命名)。此时测试类与Mock类的关联能够自动建立,但被测类无法找到自己的Mock容器,因此需要在**被测类**上添加`@MockWith`注解来显式的建立关联。例如:
|
||||
|
||||
```java
|
||||
@MockWith(ServiceTest.Mock.class)
|
||||
public class DemoService {
|
||||
...
|
||||
}
|
||||
|
||||
public class ServiceTest {
|
||||
public static class Mock extends BasicMock {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
对于更特殊的一种情况,即测试类与Mock容器类均不在约定位置的时候,则需要同时在**测试类**与**被测类**上都使用`@MockWith`指向同一个Mock容器类来建立三者的关联。复杂的关联对代码阅读会造成一定不便,在实际运用中应当尽量避免这种情况发生。
|
||||
|
||||
> 特别说明:`@MockWith`默认使用被注解类名字的尾缀判断当前类是被测类(名字非`Test`结尾)还是测试类(名字`Test`结尾),若遇到不符合此规则的类型,应使用注解的`treatAs`参数显式的指定(`ClassType.SourceClass`-被测类/`ClassType.TestClass`-测试类)
|
||||
|
||||
> Q:为何当测试类在非约定位置时,是在被测类上使用`@MockWith`,而不在测试类上指定被测类?
|
||||
>
|
||||
> A:从原则上来说,凡是能只改测试类就实现的,肯定不应该为了测试而去动业务代码(被测类)。
|
||||
> 然而由于JavaAgent只能在类首次加载进内存的时候进行字节码处理,实际情况无法保证被测的类一定在测试类之后加载(可能在其他测试用例执行的时候就被提前加载进内存了),等读取到测试类上的信息时,可能已经无法对被测类进行Mock处理。因此对于测试类和被测类相互不知道对方位置的情况,采用了两边都用`@MockWith`指定Mock容器类的折中设计。
|
||||
|
||||
### 3. 在一个测试类中测试多个被测类
|
||||
|
||||
这是非标准位置测试类的一种特殊情况,当一个测试类里同时测试了多个业务类(被测类),其名称要么只能与其中某个被测类有`+Test`的命名符合,要么不与其中任何一个被测类有命名相关性。
|
||||
|
||||
假设所有被测类的Mock容器均采用`被测类+Mock`约定命名(否则参考前一条规则,被测类也需要显式加`@MockWith`)。若该测试类本身命名不符合其中任何一个被测类+Test约定的情况,需要为该测试类加一个无参数的`@MockWith`注解(即使用默认值,相当于`@MockWith(NullType.class)`),用于标识此类需参与`TestableMock`的预处理。
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中`OneToMultiSvcTest`测试类的用例。
|
||||
|
||||
> 由于当前版本里,测试类无法通过`@MockWith`与多个Mock容器关联,对生效范围为`MockScope.ASSOCIATED`的Mock方法会遇到Mock无效的情况,为已知BUG,将在未来版本中修复。
|
||||
|
||||
### 4. 使用不包含Mock方法的Mock容器类
|
||||
|
||||
为了加快搜索Mock容器类的速度,在扫描过程中,`TestableMock`只会将自身定义有Mock方法(包含`@MockMethod`或`@MockMockConstructor`注解的方法)以及明确被`@MockWith`指向的类识别为有效的Mock容器,而不会去遍历其父类。
|
||||
|
||||
假如出于某些极特殊原因要使用无Mock方法的类型作为Mock容器,譬如希望将实际Mock方法均定义在父类,实际使用的子容器仅仅重载父类的某些特定方法。此时即使Mock容器类的位置符合约定,为了能够被识别,依然应该在相应的测试类上增加对Mock容器类的`@MockWith`引用。
|
||||
@@ -4,87 +4,111 @@
|
||||
相比以往Mock工具以类为粒度的Mock方式,`TestableMock`允许用户直接定义需要Mock的单个方法,并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测方法中的指定方法调用。
|
||||
|
||||
> 归纳起来就两条:
|
||||
> - Mock非构造方法,拷贝原方法定义到测试类,增加一个与调用者类型相同的参数,加`@MockMethod`注解
|
||||
> - Mock构造方法,拷贝原方法定义到测试类,返回值换成构造的类型,方法名随意,加`@MockContructor`注解
|
||||
> - Mock非构造方法,拷贝原方法定义到Mock容器类,加`@MockMethod`注解
|
||||
> - Mock构造方法,拷贝原方法定义到Mock容器类,返回值换成构造的类型,方法名随意,加`@MockContructor`注解
|
||||
|
||||
> **注意**:当前版本还有一项约定是,测试类与被测类的包路径应相同,且名称为`被测类名+Test`,通常采用`Maven`或`Gradle`构建的Java项目符合这种惯例。此约定在未来的`TestableMock`版本中可能会被放宽或去除。
|
||||
具体的Mock方法定义约定如下。
|
||||
|
||||
具体的Mock方法定义约定如下:
|
||||
#### 0. 前置步骤,准备Mock容器
|
||||
|
||||
首先为测试类添加一个关联的Mock类型,作为承载其Mock方法的容器,最简单的做法是在测试类里添加一个名称为`Mock`的静态内部类。例如:
|
||||
|
||||
```java
|
||||
public class DemoTest {
|
||||
|
||||
public static class Mock {
|
||||
// 放置Mock方法的地方
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
#### 1. 覆写任意类的方法调用
|
||||
|
||||
在测试类里定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
|
||||
在Mock容器类中定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,并在注解的`targetClass`参数指定该方法原本所属对象类型。
|
||||
|
||||
此时被测类中所有对该需覆写方法的调用,将在单元测试运行时,将自动被替换为对上述自定义Mock方法的调用。
|
||||
|
||||
**注意**:当遇到待覆写方法有重名时,可以将需覆写的方法名写到`@MockMethod`注解的`targetMethod`参数里,这样Mock方法自身就可以随意命名了。
|
||||
|
||||
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在测试类定义如下方法:
|
||||
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在Mock容器类定义如下方法:
|
||||
|
||||
```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)`的私有方法,我们希望在测试的时候将它替换掉,则只需在测试类定义如下方法:
|
||||
例如,被测类中有一个签名为`String innerFunc(String)`的私有方法,我们希望在测试的时候将它替换掉,则只需在Mock容器类定义如下方法:
|
||||
|
||||
```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容器类里定义一个返回值类型为要被创建的对象类型,且方法参数与要Mock的构造函数参数完全一致的方法,名称随意,然后加上`@MockContructor`注解。
|
||||
|
||||
此时被测类中所有用`new`创建指定类的操作(并使用了与Mock方法参数一致的构造函数)将被替换为对该自定义方法的调用。
|
||||
|
||||
@@ -92,22 +116,46 @@ 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. 识别当前测试用例和调用来源
|
||||
#### 5. 在Mock方法中区分调用来源
|
||||
|
||||
在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方法中取出注入的参数,根据情况返回不同结果:
|
||||
|
||||
```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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_get_source_method_name()`和`should_able_to_get_test_case_name()`测试用例。
|
||||
|
||||
@@ -116,3 +164,18 @@ private BlackBox createBlackBox(String text) {
|
||||
在测试用例中可用通过`TestableTool.verify()`方法,配合`with()`、`withInOrder()`、`without()`、`withTimes()`等方法实现对Mock调用情况的验证。
|
||||
|
||||
详见[校验Mock调用](zh-cn/doc/matcher.md)文档。
|
||||
|
||||
#### 特别说明
|
||||
|
||||
> **Mock只对被测类的代码有效**
|
||||
>
|
||||
> 在`TestableMock`的[Issues](https://github.com/alibaba/testable-mock/issues)列表中,最常见的一类问题是“Mock为什么没生效”,其中最多的一种情况是“在测试用例里直接调用了Mock的方法,发现没有替换”。这是因为<u>Mock替换只会作用在**被测类**的代码里</u>哦(~ ̄▽ ̄)~。知道大家是想快速验证一下`TestableMock`的功能,不过测试用例的代码真滴无需被Mock(这心意我们领了👻)。
|
||||
>
|
||||
> 除去这种情况,若Mock未生效,请参考[自助问题排查](zh-cn/doc/troubleshooting.md)提供的方法对比<u>Mock方法签名</u>和<u>目标位置的调用方法签名</u>。若依然无法定位原因,欢迎提交Issues告诉我们。
|
||||
|
||||
> **测试类和Mock容器的命名约定**:
|
||||
>
|
||||
> 默认情况下,`TestableMock`假设测试类与被测类的<u>包路径相同,且名称为`被测类名+Test`</u>(通常采用`Maven`或`Gradle`构建的Java项目均符合这种惯例)。
|
||||
> 同时约定测试类关联的Mock容器为<u>在其内部且名为`Mock`的静态类</u>,或<u>相同包路径下名为`被测类名+Mock`的独立类</u>。
|
||||
>
|
||||
> 当测试类或Mock容器路径不符合此约定时,可使用`@MockWith`注解显式指定,详见[使用MockWith注解](zh-cn/doc/use-mock-with.md)。
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
- 快速上手
|
||||
- [使用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)
|
||||
- [Mock的生效范围](zh-cn/doc/scope-of-mock.md)
|
||||
- [复用Mock类与方法](zh-cn/doc/mock-method-reusing.md)
|
||||
- [使用MockWith注解](zh-cn/doc/use-mock-with.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)
|
||||
|
||||
- 技术参考
|
||||
- [升级到0.5版本](zh-cn/doc/upgrade-to-v05.md)
|
||||
- [全局运行参数](zh-cn/doc/javaagent-args.md)
|
||||
- [注解参数清单](zh-cn/doc/annotations.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.5.0</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.alibaba.testable.agent;
|
||||
|
||||
import com.alibaba.testable.agent.transformer.TestableClassTransformer;
|
||||
import com.alibaba.testable.agent.util.GlobalConfig;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
import com.alibaba.ttl.threadpool.agent.TtlAgent;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
|
||||
@@ -12,12 +14,21 @@ import java.lang.instrument.Instrumentation;
|
||||
public class PreMain {
|
||||
|
||||
private static final String AND = "&";
|
||||
private static final String USE_THREAD_POOL = "useThreadPool";
|
||||
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 MOCK_SCOPE = "mockScope";
|
||||
private static final String EQUAL = "=";
|
||||
private static boolean enhanceThreadLocal = false;
|
||||
|
||||
public static void premain(String agentArgs, Instrumentation inst) {
|
||||
parseArgs(agentArgs);
|
||||
if (enhanceThreadLocal) {
|
||||
// add transmittable thread local transformer
|
||||
TtlAgent.premain(agentArgs, inst);
|
||||
}
|
||||
// add testable mock transformer
|
||||
inst.addTransformer(new TestableClassTransformer());
|
||||
}
|
||||
|
||||
@@ -28,15 +39,23 @@ public class PreMain {
|
||||
for (String a : args.split(AND)) {
|
||||
int i = a.indexOf(EQUAL);
|
||||
if (i > 0) {
|
||||
// parameter with key = value
|
||||
String k = a.substring(0, i);
|
||||
String v = a.substring(i + 1);
|
||||
if (k.equals(LOG_LEVEL)) {
|
||||
GlobalConfig.setLogLevel(v);
|
||||
} else if (k.equals(DUMP_PATH)) {
|
||||
GlobalConfig.setDumpPath(v);
|
||||
} else if (k.equals(PKG_PREFIX)) {
|
||||
GlobalConfig.setPkgPrefix(v);
|
||||
} else if (k.equals(MOCK_SCOPE)) {
|
||||
GlobalConfig.setDefaultMockScope(MockScope.of(v));
|
||||
}
|
||||
} else {
|
||||
GlobalConfig.setLogLevel(a);
|
||||
// parameter with single value
|
||||
if (a.equals(USE_THREAD_POOL)) {
|
||||
enhanceThreadLocal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.alibaba.testable.agent.constant;
|
||||
|
||||
public class ByteCodeConst {
|
||||
|
||||
public static final byte TYPE_BYTE = 'B';
|
||||
public static final byte TYPE_CHAR = 'C';
|
||||
public static final byte TYPE_DOUBLE = 'D';
|
||||
public static final byte TYPE_FLOAT = 'F';
|
||||
public static final byte TYPE_INT = 'I';
|
||||
public static final byte TYPE_LONG = 'J';
|
||||
public static final byte TYPE_CLASS = 'L';
|
||||
public static final byte TYPE_SHORT = 'S';
|
||||
public static final byte TYPE_BOOL = 'Z';
|
||||
public static final byte TYPE_VOID = 'V';
|
||||
public static final byte PARAM_END = ')';
|
||||
public static final byte CLASS_END = ';';
|
||||
public static final byte TYPE_ARRAY = '[';
|
||||
|
||||
}
|
||||
@@ -7,19 +7,19 @@ public class ConstPool {
|
||||
|
||||
public static final String DOT = ".";
|
||||
public static final String SLASH = "/";
|
||||
|
||||
public static final String TEST_POSTFIX = "Test";
|
||||
public static final String DOLLAR = "$";
|
||||
public static final String UNDERLINE = "_";
|
||||
|
||||
public static final String FIELD_TARGET_METHOD = "targetMethod";
|
||||
public static final String FIELD_TARGET_CLASS = "targetClass";
|
||||
public static final String FIELD_SCOPE = "scope";
|
||||
|
||||
public static final String MOCK_WITH = "com.alibaba.testable.core.annotation.MockWith";
|
||||
public static final String MOCK_DIAGNOSE = "com.alibaba.testable.core.annotation.MockDiagnose";
|
||||
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";
|
||||
|
||||
/**
|
||||
* Name of the constructor method
|
||||
*/
|
||||
public static final String CONSTRUCTOR = "<init>";
|
||||
public static final String KOTLIN_POSTFIX_COMPANION = "$Companion";
|
||||
public static final String KOTLIN_PREFIX_ACCESS = "access$";
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.alibaba.testable.agent.handler;
|
||||
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
@@ -15,26 +16,33 @@ import java.util.Iterator;
|
||||
*/
|
||||
abstract public class BaseClassHandler implements Opcodes {
|
||||
|
||||
protected static final String TESTABLE_MARK_FIELD = "__testable";
|
||||
protected static final String TESTABLE_REF = "__testable";
|
||||
protected static final String GET_TESTABLE_REF = "testableIns";
|
||||
protected static final String VOID_ARGS = "()";
|
||||
protected static final String VOID_RES = "V";
|
||||
|
||||
protected boolean wasTransformed(ClassNode cn) {
|
||||
protected String mockClassName;
|
||||
|
||||
protected boolean markTransformed(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;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
cn.fields.add(new FieldNode(ACC_PRIVATE, TESTABLE_MARK_FIELD, "I", null, null));
|
||||
return false;
|
||||
cn.fields.add(new FieldNode(ACC_PRIVATE | ACC_STATIC, refName, refDescriptor, null, null));
|
||||
return true;
|
||||
}
|
||||
|
||||
public byte[] getBytes(byte[] classFileBuffer) throws IOException {
|
||||
ClassReader cr = new ClassReader(classFileBuffer);
|
||||
ClassNode cn = new ClassNode();
|
||||
cr.accept(cn, 0);
|
||||
transform(cn);
|
||||
if (markTransformed(cn, TESTABLE_REF, ClassUtil.toByteCodeClassName(mockClassName))) {
|
||||
transform(cn);
|
||||
}
|
||||
ClassWriter cw = new ClassWriter( 0);
|
||||
cn.accept(cw);
|
||||
return cw.toByteArray();
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.alibaba.testable.agent.handler;
|
||||
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
abstract public class BaseClassWithContextHandler extends BaseClassHandler {
|
||||
|
||||
protected static final String CLASS_MOCK_CONTEXT_UTIL = "com/alibaba/testable/core/util/MockContextUtil";
|
||||
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 FIELD_SOURCE_METHOD = "SOURCE_METHOD";
|
||||
private static final String FIELD_MOCK_CONTEXT = "MOCK_CONTEXT";
|
||||
private static final String METHOD_PARAMETERS = "parameters";
|
||||
private static final String METHOD_CURRENT_SOURCE_METHOD_NAME = "currentSourceMethodName";
|
||||
private static final String SIGNATURE_CURRENT_SOURCE_METHOD_NAME = "()Ljava/lang/String;";
|
||||
private static final String SIGNATURE_PARAMETERS = "()Ljava/util/Map;";
|
||||
|
||||
protected void handleTestableUtil(MethodNode mn) {
|
||||
AbstractInsnNode[] instructions = mn.instructions.toArray();
|
||||
// Note: instructions.length will change when instructions updated
|
||||
for (int i = 0; i < instructions.length; i++) {
|
||||
if (instructions[i].getOpcode() == GETSTATIC) {
|
||||
FieldInsnNode fieldInsnNode = (FieldInsnNode)instructions[i];
|
||||
if (isTestableUtilField(fieldInsnNode)) {
|
||||
replaceTestableUtilField(mn, instructions, fieldInsnNode.name, i);
|
||||
instructions = mn.instructions.toArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTestableUtilField(FieldInsnNode fieldInsnNode) {
|
||||
return fieldInsnNode.owner.equals(CLASS_TESTABLE_TOOL) &&
|
||||
(fieldInsnNode.name.equals(FIELD_SOURCE_METHOD) || fieldInsnNode.name.equals(FIELD_MOCK_CONTEXT));
|
||||
}
|
||||
|
||||
private void replaceTestableUtilField(MethodNode mn, AbstractInsnNode[] instructions, String fieldName, int pos) {
|
||||
InsnList il = new InsnList();
|
||||
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 MethodInsnNode(INVOKESTATIC, CLASS_MOCK_CONTEXT_UTIL, METHOD_PARAMETERS,
|
||||
SIGNATURE_PARAMETERS, false));
|
||||
}
|
||||
if (il.size() > 0) {
|
||||
mn.instructions.insert(instructions[pos], il);
|
||||
mn.instructions.remove(instructions[pos]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package com.alibaba.testable.agent.handler;
|
||||
|
||||
import com.alibaba.testable.agent.constant.ByteCodeConst;
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.util.AnnotationUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.agent.util.GlobalConfig;
|
||||
import com.alibaba.testable.agent.util.MethodUtil;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.CONSTRUCTOR;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
|
||||
private static final String CLASS_INVOKE_RECORD_UTIL = "com/alibaba/testable/core/util/InvokeRecordUtil";
|
||||
private static final String CLASS_MOCK_ASSOCIATION_UTIL = "com/alibaba/testable/core/util/MockAssociationUtil";
|
||||
private static final String METHOD_INVOKE_ORIGIN = "invokeOrigin";
|
||||
private static final String SIGNATURE_INVOKE_ORIGIN =
|
||||
"(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;";
|
||||
private static final String METHOD_RECORD_MOCK_INVOKE = "recordMockInvoke";
|
||||
private static final String SIGNATURE_RECORDER_METHOD_INVOKE = "([Ljava/lang/Object;Z)V";
|
||||
private static final String METHOD_IS_ASSOCIATED = "isAssociated";
|
||||
private static final String SIGNATURE_IS_ASSOCIATED = "()Z";
|
||||
|
||||
public MockClassHandler(String className) {
|
||||
this.mockClassName = className;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
injectGetInstanceMethod(cn);
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (isMockMethod(mn)) {
|
||||
mn.access &= ~ACC_PRIVATE;
|
||||
mn.access &= ~ACC_PROTECTED;
|
||||
mn.access |= ACC_PUBLIC;
|
||||
// below transform order is important
|
||||
unfoldTargetClass(mn);
|
||||
injectInvokeRecorder(mn);
|
||||
injectAssociationChecker(mn);
|
||||
handleTestableUtil(mn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* add method to fetch singleton instance of this mock class
|
||||
*/
|
||||
private void injectGetInstanceMethod(ClassNode cn) {
|
||||
MethodNode getInstanceMethod = new MethodNode(ACC_PUBLIC | ACC_STATIC, GET_TESTABLE_REF,
|
||||
VOID_ARGS + ClassUtil.toByteCodeClassName(mockClassName), null, null);
|
||||
InsnList il = new InsnList();
|
||||
il.add(new FieldInsnNode(GETSTATIC, mockClassName, TESTABLE_REF, ClassUtil.toByteCodeClassName(mockClassName)));
|
||||
LabelNode label = new LabelNode();
|
||||
il.add(new JumpInsnNode(IFNONNULL, label));
|
||||
il.add(new TypeInsnNode(NEW, mockClassName));
|
||||
il.add(new InsnNode(DUP));
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, mockClassName, CONSTRUCTOR, VOID_ARGS + VOID_RES, false));
|
||||
il.add(new FieldInsnNode(PUTSTATIC, mockClassName, TESTABLE_REF, ClassUtil.toByteCodeClassName(mockClassName)));
|
||||
il.add(label);
|
||||
il.add(new FrameNode(F_SAME, 0, null, 0, null));
|
||||
il.add(new FieldInsnNode(GETSTATIC, mockClassName, TESTABLE_REF, ClassUtil.toByteCodeClassName(mockClassName)));
|
||||
il.add(new InsnNode(ARETURN));
|
||||
getInstanceMethod.instructions = il;
|
||||
getInstanceMethod.maxStack = 2;
|
||||
getInstanceMethod.maxLocals = 0;
|
||||
cn.methods.add(getInstanceMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* put targetClass parameter in @MockMethod to first parameter of the mock method
|
||||
*/
|
||||
private void unfoldTargetClass(MethodNode mn) {
|
||||
String targetClassName = null;
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD).equals(an.desc)) {
|
||||
Type type = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_CLASS, null, Type.class);
|
||||
if (type != null) {
|
||||
targetClassName = ClassUtil.toByteCodeClassName(type.getClassName());
|
||||
}
|
||||
AnnotationUtil.removeAnnotationParameter(an, ConstPool.FIELD_TARGET_CLASS);
|
||||
}
|
||||
}
|
||||
if (targetClassName != null) {
|
||||
// must get label before method description changed
|
||||
ImmutablePair<LabelNode, LabelNode> labels = getStartAndEndLabel(mn);
|
||||
mn.desc = MethodUtil.addParameterAtBegin(mn.desc, targetClassName);
|
||||
int parameterOffset = MethodUtil.isStatic(mn) ? 0 : 1;
|
||||
mn.localVariables.add(parameterOffset, new LocalVariableNode("__self", targetClassName, null,
|
||||
labels.left, labels.right, parameterOffset));
|
||||
for (int i = parameterOffset + 1; i < mn.localVariables.size(); i++) {
|
||||
mn.localVariables.get(i).index++;
|
||||
}
|
||||
for (AbstractInsnNode in : mn.instructions) {
|
||||
if (in instanceof IincInsnNode) {
|
||||
((IincInsnNode)in).var++;
|
||||
} else if (in instanceof VarInsnNode && ((VarInsnNode)in).var >= parameterOffset) {
|
||||
((VarInsnNode)in).var++;
|
||||
} else if (in instanceof FrameNode && ((FrameNode)in).type == F_FULL) {
|
||||
((FrameNode)in).local.add(parameterOffset, targetClassName);
|
||||
}
|
||||
}
|
||||
mn.maxLocals++;
|
||||
}
|
||||
}
|
||||
|
||||
private ImmutablePair<LabelNode, LabelNode> getStartAndEndLabel(MethodNode mn) {
|
||||
if (MethodUtil.isStatic(mn)) {
|
||||
LabelNode startLabel = null, endLabel = null;
|
||||
for (AbstractInsnNode n = mn.instructions.getFirst(); n != null; n = n.getNext()) {
|
||||
if (n instanceof LabelNode) {
|
||||
startLabel = (LabelNode)n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (MethodUtil.extractParameters(mn.desc).isEmpty()) {
|
||||
// for method without parameter, should manually add a ending label
|
||||
endLabel = new LabelNode(new Label());
|
||||
mn.instructions.add(endLabel);
|
||||
} else {
|
||||
// for method with parameters, find the existing ending label
|
||||
for (AbstractInsnNode n = mn.instructions.getLast(); n != null; n = n.getPrevious()) {
|
||||
if (n instanceof LabelNode) {
|
||||
endLabel = (LabelNode)n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ImmutablePair.of(startLabel, endLabel);
|
||||
} else {
|
||||
LocalVariableNode thisRef = mn.localVariables.get(0);
|
||||
return ImmutablePair.of(thisRef.start, thisRef.end);
|
||||
}
|
||||
}
|
||||
|
||||
private void injectAssociationChecker(MethodNode mn) {
|
||||
if (isGlobalScope(mn)) {
|
||||
return;
|
||||
}
|
||||
LabelNode firstLine = new LabelNode(new Label());
|
||||
InsnList il = new InsnList();
|
||||
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_MOCK_ASSOCIATION_UTIL, METHOD_IS_ASSOCIATED,
|
||||
SIGNATURE_IS_ASSOCIATED, false));
|
||||
il.add(new JumpInsnNode(IFNE, firstLine));
|
||||
il.add(invokeOriginalMethod(mn));
|
||||
il.add(firstLine);
|
||||
il.add( new FrameNode(F_SAME, 0, null, 0, null));
|
||||
mn.instructions.insertBefore(mn.instructions.getFirst(), il);
|
||||
}
|
||||
|
||||
private InsnList invokeOriginalMethod(MethodNode mn) {
|
||||
InsnList il = new InsnList();
|
||||
ImmutablePair<Type, String> target = getTargetClassAndMethodName(mn);
|
||||
il.add(new LdcInsnNode(target.left));
|
||||
il.add(new LdcInsnNode(target.right));
|
||||
il.add(duplicateParameters(mn));
|
||||
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_MOCK_ASSOCIATION_UTIL, METHOD_INVOKE_ORIGIN,
|
||||
SIGNATURE_INVOKE_ORIGIN, false));
|
||||
String returnType = MethodUtil.getReturnType(mn.desc);
|
||||
if (VOID_RES.equals(returnType)) {
|
||||
il.add(new InsnNode(POP));
|
||||
il.add(new InsnNode(RETURN));
|
||||
} else if (returnType.startsWith("[") || returnType.startsWith("L")) {
|
||||
il.add(new TypeInsnNode(CHECKCAST, returnType));
|
||||
il.add(new InsnNode(ARETURN));
|
||||
} else {
|
||||
String wrapperClass = ClassUtil.toWrapperClass(returnType.getBytes()[0]);
|
||||
il.add(new TypeInsnNode(CHECKCAST, wrapperClass));
|
||||
ImmutablePair<String, String> convertMethod = ClassUtil.getWrapperTypeConvertMethod(returnType.getBytes()[0]);
|
||||
il.add(new MethodInsnNode(INVOKEVIRTUAL, wrapperClass, convertMethod.left, convertMethod.right, false));
|
||||
il.add(new InsnNode(ClassUtil.getReturnOpsCode(returnType)));
|
||||
}
|
||||
return il;
|
||||
}
|
||||
|
||||
private ImmutablePair<Type, String> getTargetClassAndMethodName(MethodNode mn) {
|
||||
Type className;
|
||||
String methodName = mn.name;
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD).equals(an.desc)) {
|
||||
String name = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_METHOD,
|
||||
null, String.class);
|
||||
if (name != null) {
|
||||
methodName = name;
|
||||
}
|
||||
} else if (ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR).equals(an.desc)) {
|
||||
methodName = CONSTRUCTOR;
|
||||
}
|
||||
}
|
||||
if (methodName.equals(CONSTRUCTOR)) {
|
||||
className = Type.getType(MethodUtil.getReturnType(mn.desc));
|
||||
} else {
|
||||
className = Type.getType(MethodUtil.getFirstParameter(mn.desc));
|
||||
}
|
||||
return ImmutablePair.of(className, methodName);
|
||||
}
|
||||
|
||||
private boolean isGlobalScope(MethodNode mn) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD).equals(an.desc) ||
|
||||
ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR).equals(an.desc)) {
|
||||
MockScope scope = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_SCOPE,
|
||||
GlobalConfig.getDefaultMockScope(), MockScope.class);
|
||||
if (scope.equals(MockScope.GLOBAL)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isMockMethod(MethodNode mn) {
|
||||
if (mn.visibleAnnotations == null) {
|
||||
return false;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD).equals(an.desc) ||
|
||||
ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR).equals(an.desc)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void injectInvokeRecorder(MethodNode mn) {
|
||||
InsnList il = new InsnList();
|
||||
il.add(duplicateParameters(mn));
|
||||
if (isMockForConstructor(mn)) {
|
||||
il.add(new InsnNode(ICONST_1));
|
||||
} else {
|
||||
il.add(new InsnNode(ICONST_0));
|
||||
}
|
||||
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_INVOKE_RECORD_UTIL, METHOD_RECORD_MOCK_INVOKE,
|
||||
SIGNATURE_RECORDER_METHOD_INVOKE, false));
|
||||
mn.instructions.insertBefore(mn.instructions.getFirst(), il);
|
||||
mn.maxStack += (2 + MethodUtil.getParameterTypes(mn.desc).size() * 3);
|
||||
}
|
||||
|
||||
private InsnList duplicateParameters(MethodNode mn) {
|
||||
InsnList il = new InsnList();
|
||||
List<Byte> types = MethodUtil.getParameterTypes(mn.desc);
|
||||
int size = types.size();
|
||||
il.add(getIntInsn(size));
|
||||
il.add(new TypeInsnNode(ANEWARRAY, ClassUtil.CLASS_OBJECT));
|
||||
int parameterOffset = MethodUtil.isStatic(mn) ? 0 : 1;
|
||||
for (int i = 0; i < size; i++) {
|
||||
il.add(new InsnNode(DUP));
|
||||
il.add(getIntInsn(i));
|
||||
ImmutablePair<Integer, Integer> code = getLoadParameterByteCode(types.get(i));
|
||||
il.add(new VarInsnNode(code.left, parameterOffset));
|
||||
parameterOffset += code.right;
|
||||
MethodInsnNode typeConvertMethodNode = ClassUtil.getPrimaryTypeConvertMethod(types.get(i));
|
||||
if (typeConvertMethodNode != null) {
|
||||
il.add(typeConvertMethodNode);
|
||||
}
|
||||
il.add(new InsnNode(AASTORE));
|
||||
}
|
||||
return il;
|
||||
}
|
||||
|
||||
private boolean isMockForConstructor(MethodNode mn) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
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 (CONSTRUCTOR.equals(method)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ImmutablePair<Integer, Integer> getLoadParameterByteCode(Byte type) {
|
||||
switch (type) {
|
||||
case ByteCodeConst.TYPE_BYTE:
|
||||
case ByteCodeConst.TYPE_CHAR:
|
||||
case ByteCodeConst.TYPE_SHORT:
|
||||
case ByteCodeConst.TYPE_INT:
|
||||
case ByteCodeConst.TYPE_BOOL:
|
||||
return ImmutablePair.of(ILOAD, 1);
|
||||
case ByteCodeConst.TYPE_DOUBLE:
|
||||
return ImmutablePair.of(DLOAD, 2);
|
||||
case ByteCodeConst.TYPE_FLOAT:
|
||||
return ImmutablePair.of(FLOAD, 1);
|
||||
case ByteCodeConst.TYPE_LONG:
|
||||
return ImmutablePair.of(LLOAD, 2);
|
||||
default:
|
||||
return ImmutablePair.of(ALOAD, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private AbstractInsnNode getIntInsn(int num) {
|
||||
switch (num) {
|
||||
case 0:
|
||||
return new InsnNode(ICONST_0);
|
||||
case 1:
|
||||
return new InsnNode(ICONST_1);
|
||||
case 2:
|
||||
return new InsnNode(ICONST_2);
|
||||
case 3:
|
||||
return new InsnNode(ICONST_3);
|
||||
case 4:
|
||||
return new InsnNode(ICONST_4);
|
||||
case 5:
|
||||
return new InsnNode(ICONST_5);
|
||||
default:
|
||||
return new IntInsnNode(BIPUSH, num);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
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.util.BytecodeUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.agent.util.MethodUtil;
|
||||
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;
|
||||
|
||||
import static com.alibaba.testable.core.constant.ConstPool.CONSTRUCTOR;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
@@ -26,8 +27,9 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
add(Opcodes.INVOKEINTERFACE);
|
||||
}};
|
||||
|
||||
public SourceClassHandler(List<MethodInfo> injectMethods) {
|
||||
public SourceClassHandler(List<MethodInfo> injectMethods, String mockClassName) {
|
||||
this.injectMethods = injectMethods;
|
||||
this.mockClassName = mockClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,74 +38,88 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
*/
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
if (wasTransformed(cn)) {
|
||||
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(CONSTRUCTOR)) {
|
||||
newOperatorInjectMethods.add(im);
|
||||
} else {
|
||||
memberInjectMethods.add(mi);
|
||||
memberInjectMethods.add(im);
|
||||
}
|
||||
}
|
||||
for (MethodNode m : cn.methods) {
|
||||
transformMethod(cn, m, memberInjectMethods, newOperatorInjectMethods);
|
||||
transformMethod(m, memberInjectMethods, newOperatorInjectMethods);
|
||||
}
|
||||
}
|
||||
|
||||
private void transformMethod(ClassNode cn, MethodNode mn, Set<MethodInfo> memberInjectMethods,
|
||||
private void transformMethod(MethodNode mn, Set<MethodInfo> memberInjectMethods,
|
||||
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;
|
||||
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
|
||||
String newOperatorInjectMethodName = getNewOperatorInjectMethodName(newOperatorInjectMethods, node);
|
||||
if (newOperatorInjectMethodName != null) {
|
||||
// and an inject method for it exist
|
||||
if (CONSTRUCTOR.equals(node.name)) {
|
||||
LogUtil.verbose(" Line %d, constructing \"%s\" as \"%s\"", getLineNum(instructions, i),
|
||||
node.owner, node.desc);
|
||||
MethodInfo newOperatorInjectMethod = getNewOperatorInjectMethod(newOperatorInjectMethods, node);
|
||||
if (newOperatorInjectMethod != null) {
|
||||
// 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);
|
||||
instructions = replaceNewOps(mn, newOperatorInjectMethod, instructions, rangeStart, i);
|
||||
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) {
|
||||
instructions = replaceMemberCallOps(mn, mockMethod,
|
||||
instructions, node.owner, node.getOpcode(), rangeStart, i);
|
||||
i = rangeStart;
|
||||
} else {
|
||||
LogUtil.warn("Potential missed mocking at %s:%s", mn.name, getLineNum(instructions, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i++;
|
||||
} while (i < instructions.length);
|
||||
}
|
||||
|
||||
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 : MethodUtil.removeFirstParameter(node.desc);
|
||||
if (m.getClazz().equals(nodeOwner) && m.getName().equals(nodeName) && m.getDesc().equals(nodeDesc)) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getNewOperatorInjectMethodName(Set<MethodInfo> newOperatorInjectMethods, MethodInsnNode node) {
|
||||
private MethodInfo getNewOperatorInjectMethod(Set<MethodInfo> newOperatorInjectMethods, MethodInsnNode node) {
|
||||
for (MethodInfo m : newOperatorInjectMethods) {
|
||||
if (m.getDesc().equals(getConstructorInjectDesc(node))) {
|
||||
return m.getMockName();
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -138,7 +154,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
}
|
||||
|
||||
private int getInitialStackLevel(MethodInsnNode instruction) {
|
||||
int stackLevel = ClassUtil.getParameterTypes((instruction).desc).size();
|
||||
int stackLevel = MethodUtil.getParameterTypes((instruction).desc).size();
|
||||
switch (instruction.getOpcode()) {
|
||||
case Opcodes.INVOKESPECIAL:
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
@@ -157,10 +173,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,20 +186,23 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private int stackEffectOfInvocation(AbstractInsnNode instruction) {
|
||||
String desc = ((MethodInsnNode)instruction).desc;
|
||||
return ClassUtil.getParameterTypes(desc).size() - (ClassUtil.getReturnType(desc).isEmpty() ? 0 : 1);
|
||||
private int stackEffectOfInvocation(String desc) {
|
||||
return MethodUtil.getParameterTypes(desc).size() - (MethodUtil.getReturnType(desc).equals(VOID_RES) ? 0 : 1);
|
||||
}
|
||||
|
||||
private AbstractInsnNode[] replaceNewOps(ClassNode cn, MethodNode mn, String newOperatorInjectMethodName,
|
||||
AbstractInsnNode[] instructions, int start, int end) {
|
||||
LogUtil.diagnose(" Line %d, mock method %s used", getLineNum(instructions, start),
|
||||
newOperatorInjectMethodName);
|
||||
private AbstractInsnNode[] replaceNewOps(MethodNode mn, MethodInfo newOperatorInjectMethod,
|
||||
AbstractInsnNode[] instructions, int start, int end) {
|
||||
String mockMethodName = newOperatorInjectMethod.getMockName();
|
||||
int invokeOpcode = newOperatorInjectMethod.isStatic() ? INVOKESTATIC : INVOKEVIRTUAL;
|
||||
LogUtil.diagnose(" Line %d, mock method \"%s\" used", getLineNum(instructions, start), mockMethodName);
|
||||
String classType = ((TypeInsnNode)instructions[start]).desc;
|
||||
String constructorDesc = ((MethodInsnNode)instructions[end]).desc;
|
||||
String testClassName = ClassUtil.getTestClassName(cn.name);
|
||||
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKESTATIC, testClassName,
|
||||
newOperatorInjectMethodName, getConstructorInjectDesc(constructorDesc, classType), false));
|
||||
if (!newOperatorInjectMethod.isStatic()) {
|
||||
mn.instructions.insertBefore(instructions[start], new MethodInsnNode(INVOKESTATIC, mockClassName,
|
||||
GET_TESTABLE_REF, VOID_ARGS + ClassUtil.toByteCodeClassName(mockClassName), false));
|
||||
}
|
||||
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(invokeOpcode, mockClassName,
|
||||
mockMethodName, getConstructorInjectDesc(constructorDesc, classType), false));
|
||||
mn.instructions.remove(instructions[start]);
|
||||
mn.instructions.remove(instructions[start + 1]);
|
||||
mn.instructions.remove(instructions[end]);
|
||||
@@ -203,25 +223,29 @@ 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];
|
||||
String testClassName = ClassUtil.getTestClassName(cn.name);
|
||||
private AbstractInsnNode[] replaceMemberCallOps(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());
|
||||
if (!mockMethod.isStatic()) {
|
||||
mn.instructions.insertBefore(instructions[start], new MethodInsnNode(INVOKESTATIC, mockClassName,
|
||||
GET_TESTABLE_REF, VOID_ARGS + ClassUtil.toByteCodeClassName(mockClassName), false));
|
||||
}
|
||||
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));
|
||||
mn.maxStack++;
|
||||
if (ClassUtil.isCompanionClassName(ownerClass)) {
|
||||
// for kotlin companion class, remove the byte code of reference to "companion" static field
|
||||
mn.instructions.remove(instructions[end - 1]);
|
||||
}
|
||||
}
|
||||
// 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));
|
||||
// method with @MockMethod will be modified as public access
|
||||
int invokeOpcode = mockMethod.isStatic() ? INVOKESTATIC : INVOKEVIRTUAL;
|
||||
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(invokeOpcode, mockClassName,
|
||||
mockMethod.getMockName(), mockMethod.getMockDesc(), false));
|
||||
mn.instructions.remove(instructions[end]);
|
||||
mn.maxStack++;
|
||||
return mn.instructions.toArray();
|
||||
}
|
||||
|
||||
@@ -229,8 +253,4 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
return Opcodes.INVOKEVIRTUAL == opcode && ClassUtil.isCompanionClassName(ownerClass);
|
||||
}
|
||||
|
||||
private String addFirstParameter(String desc, String ownerClass) {
|
||||
return "(" + ClassUtil.toByteCodeClassName(ownerClass) + desc.substring(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
package com.alibaba.testable.agent.handler;
|
||||
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.util.AnnotationUtil;
|
||||
import com.alibaba.testable.agent.handler.test.*;
|
||||
import com.alibaba.testable.agent.model.TestCaseMethodType;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class TestClassHandler extends BaseClassHandler {
|
||||
public class TestClassHandler extends BaseClassWithContextHandler {
|
||||
|
||||
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 FIELD_TEST_CASE = "TEST_CASE";
|
||||
private static final String FIELD_SOURCE_METHOD = "SOURCE_METHOD";
|
||||
private static final String METHOD_CURRENT_TEST_CASE_NAME = "currentTestCaseName";
|
||||
private static final String METHOD_CURRENT_SOURCE_METHOD_NAME = "currentSourceMethodName";
|
||||
private static final String 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 METHOD_INIT = "init";
|
||||
private static final String DESC_METHOD_INIT = "()V";
|
||||
private static final String METHOD_CLEAN = "clean";
|
||||
private static final String DESC_METHOD_CLEAN = "()V";
|
||||
private static final String THIS = "this";
|
||||
|
||||
private int testCaseCount = 0;
|
||||
|
||||
private final Framework[] frameworkClasses = new Framework[] {
|
||||
new JUnit4Framework(),
|
||||
new JUnit5Framework(),
|
||||
new TestNgFramework(),
|
||||
new TestNgOnClassFramework()
|
||||
};
|
||||
|
||||
public TestClassHandler(String mockClassName) {
|
||||
this.mockClassName = mockClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bytecode of test class
|
||||
@@ -34,193 +41,81 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
*/
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
if (wasTransformed(cn)) {
|
||||
Framework framework = checkFramework(cn);
|
||||
if (framework == null) {
|
||||
LogUtil.warn("Failed to detect test framework for " + cn.name);
|
||||
return;
|
||||
}
|
||||
if (!framework.hasTestAfterMethod) {
|
||||
addTestAfterMethod(cn, framework.getTestAfterAnnotation());
|
||||
}
|
||||
for (MethodNode mn : cn.methods) {
|
||||
handleMockMethod(cn, mn);
|
||||
handleInstruction(cn, mn);
|
||||
handleTestableUtil(mn);
|
||||
handleTestCaseMethod(mn, framework);
|
||||
}
|
||||
LogUtil.diagnose(String.format(" Found %d test cases", testCaseCount));
|
||||
}
|
||||
|
||||
private void handleMockMethod(ClassNode cn, MethodNode mn) {
|
||||
if (isMockMethod(mn)) {
|
||||
toPublicStatic(cn, mn);
|
||||
injectInvokeRecorder(mn);
|
||||
private Framework checkFramework(ClassNode cn) {
|
||||
Set<String> classAnnotationSet = new HashSet<String>();
|
||||
Set<String> methodAnnotationSet = new HashSet<String>();
|
||||
if (cn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
classAnnotationSet.add(an.desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void toPublicStatic(ClassNode cn, MethodNode mn) {
|
||||
mn.access &= ~ACC_PRIVATE;
|
||||
mn.access &= ~ACC_PROTECTED;
|
||||
mn.access |= ACC_PUBLIC;
|
||||
if ((mn.access & ACC_STATIC) == 0) {
|
||||
mn.access |= ACC_STATIC;
|
||||
// remove `this` reference
|
||||
LocalVariableNode thisRef = null;
|
||||
for (LocalVariableNode vn : mn.localVariables) {
|
||||
if (vn.index == 0) {
|
||||
thisRef = vn;
|
||||
} else {
|
||||
vn.index--;
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (mn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
methodAnnotationSet.add(an.desc);
|
||||
}
|
||||
}
|
||||
if (thisRef != null) {
|
||||
mn.localVariables.remove(thisRef);
|
||||
} else {
|
||||
LogUtil.error("Fail to find `this` reference in none-static method " + getName(cn, mn));
|
||||
return;
|
||||
}
|
||||
for (AbstractInsnNode in : mn.instructions) {
|
||||
if (in.getOpcode() >= ILOAD && in.getOpcode() <= SASTORE && 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));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
mn.maxLocals--;
|
||||
}
|
||||
}
|
||||
|
||||
private String getName(ClassNode cn, MethodNode mn) {
|
||||
return cn.name + ":" + mn.name;
|
||||
}
|
||||
|
||||
private boolean isMockMethod(MethodNode mn) {
|
||||
if (mn.visibleAnnotations == null) {
|
||||
return false;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD).equals(an.desc) ||
|
||||
ClassUtil.toByteCodeClassName(ConstPool.TESTABLE_MOCK).equals(an.desc) ||
|
||||
ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR).equals(an.desc)) {
|
||||
return true;
|
||||
for (Framework i : frameworkClasses) {
|
||||
if (i.fit(classAnnotationSet, methodAnnotationSet)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
private void handleInstruction(ClassNode cn, MethodNode mn) {
|
||||
AbstractInsnNode[] instructions = mn.instructions.toArray();
|
||||
int i = 0;
|
||||
do {
|
||||
if (instructions[i].getOpcode() == GETSTATIC) {
|
||||
FieldInsnNode fieldInsnNode = (FieldInsnNode)instructions[i];
|
||||
if (isTestableUtilField(fieldInsnNode)) {
|
||||
instructions = replaceTestableUtilField(cn, mn, instructions, fieldInsnNode.name, i);
|
||||
}
|
||||
}
|
||||
i++;
|
||||
} while (i < instructions.length);
|
||||
}
|
||||
|
||||
private boolean isTestableUtilField(FieldInsnNode fieldInsnNode) {
|
||||
return fieldInsnNode.owner.equals(CLASS_TESTABLE_TOOL) &&
|
||||
(fieldInsnNode.name.equals(FIELD_TEST_CASE) || fieldInsnNode.name.equals(FIELD_SOURCE_METHOD));
|
||||
}
|
||||
|
||||
private AbstractInsnNode[] replaceTestableUtilField(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions,
|
||||
String fieldName, int pos) {
|
||||
private void addTestAfterMethod(ClassNode cn, String testAfterAnnotation) {
|
||||
MethodNode afterTestMethod = new MethodNode(ACC_PUBLIC, "testableAfterTestCase", "()V", null, null);
|
||||
afterTestMethod.visibleAnnotations = Collections.singletonList(new AnnotationNode(testAfterAnnotation));
|
||||
InsnList il = new InsnList();
|
||||
if (FIELD_TEST_CASE.equals(fieldName)) {
|
||||
il.add(new LdcInsnNode(ClassUtil.toDotSeparatedName(cn.name)));
|
||||
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_CURRENT_TEST_CASE_NAME,
|
||||
SIGNATURE_CURRENT_TEST_CASE_NAME, false));
|
||||
} 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));
|
||||
}
|
||||
if (il.size() > 0) {
|
||||
mn.instructions.insert(instructions[pos], il);
|
||||
mn.instructions.remove(instructions[pos]);
|
||||
}
|
||||
return mn.instructions.toArray();
|
||||
LabelNode startLabel = new LabelNode(new Label());
|
||||
LabelNode endLabel = new LabelNode(new Label());
|
||||
il.add(startLabel);
|
||||
il.add(new InsnNode(RETURN));
|
||||
il.add(endLabel);
|
||||
afterTestMethod.instructions = il;
|
||||
afterTestMethod.localVariables = Collections.singletonList(
|
||||
new LocalVariableNode(THIS, ClassUtil.toByteCodeClassName(cn.name), null, startLabel, endLabel, 0));
|
||||
afterTestMethod.maxLocals = 1;
|
||||
afterTestMethod.maxStack = 0;
|
||||
cn.methods.add(afterTestMethod);
|
||||
}
|
||||
|
||||
private void injectInvokeRecorder(MethodNode mn) {
|
||||
private void handleTestCaseMethod(MethodNode mn, Framework framework) {
|
||||
TestCaseMethodType type = framework.checkMethodType(mn);
|
||||
if (type.equals(TestCaseMethodType.TEST)) {
|
||||
LogUtil.verbose(String.format(" Test case \"%s\"", mn.name));
|
||||
injectMockContextInit(mn);
|
||||
testCaseCount++;
|
||||
} else if (type.equals(TestCaseMethodType.AFTER_TEST)) {
|
||||
injectMockContextClean(mn);
|
||||
}
|
||||
}
|
||||
|
||||
private void injectMockContextInit(MethodNode mn) {
|
||||
InsnList il = new InsnList();
|
||||
List<Byte> types = ClassUtil.getParameterTypes(mn.desc);
|
||||
int size = types.size();
|
||||
int parameterOffset = 0;
|
||||
mn.maxStack += 1;
|
||||
il.add(getIntInsn(size));
|
||||
il.add(new TypeInsnNode(ANEWARRAY, ClassUtil.CLASS_OBJECT));
|
||||
for (int i = 0; i < size; i++) {
|
||||
mn.maxStack += 3;
|
||||
il.add(new InsnNode(DUP));
|
||||
il.add(getIntInsn(i));
|
||||
ImmutablePair<Integer, Integer> code = getLoadParameterByteCode(types.get(i));
|
||||
il.add(new VarInsnNode(code.left, parameterOffset));
|
||||
parameterOffset += code.right;
|
||||
MethodInsnNode typeConvertMethodNode = ClassUtil.getPrimaryTypeConvertMethod(types.get(i));
|
||||
if (typeConvertMethodNode != null) {
|
||||
il.add(typeConvertMethodNode);
|
||||
}
|
||||
il.add(new InsnNode(AASTORE));
|
||||
}
|
||||
if (isMockForConstructor(mn)) {
|
||||
il.add(new InsnNode(ICONST_1));
|
||||
} else {
|
||||
il.add(new InsnNode(ICONST_0));
|
||||
}
|
||||
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_INVOKE_RECORD_UTIL, METHOD_RECORD_MOCK_INVOKE,
|
||||
SIGNATURE_INVOKE_RECORDER_METHOD, false));
|
||||
mn.instructions.insertBefore(mn.instructions.get(0), il);
|
||||
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_MOCK_CONTEXT_UTIL, METHOD_INIT, DESC_METHOD_INIT, false));
|
||||
mn.instructions.insertBefore(mn.instructions.getFirst(), il);
|
||||
}
|
||||
|
||||
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)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ImmutablePair<Integer, Integer> getLoadParameterByteCode(Byte type) {
|
||||
switch (type) {
|
||||
case ClassUtil.TYPE_BYTE:
|
||||
case ClassUtil.TYPE_CHAR:
|
||||
case ClassUtil.TYPE_SHORT:
|
||||
case ClassUtil.TYPE_INT:
|
||||
case ClassUtil.TYPE_BOOL:
|
||||
return ImmutablePair.of(ILOAD, 1);
|
||||
case ClassUtil.TYPE_DOUBLE:
|
||||
return ImmutablePair.of(DLOAD, 2);
|
||||
case ClassUtil.TYPE_FLOAT:
|
||||
return ImmutablePair.of(FLOAD, 1);
|
||||
case ClassUtil.TYPE_LONG:
|
||||
return ImmutablePair.of(LLOAD, 2);
|
||||
default:
|
||||
return ImmutablePair.of(ALOAD, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private AbstractInsnNode getIntInsn(int num) {
|
||||
switch (num) {
|
||||
case 0:
|
||||
return new InsnNode(ICONST_0);
|
||||
case 1:
|
||||
return new InsnNode(ICONST_1);
|
||||
case 2:
|
||||
return new InsnNode(ICONST_2);
|
||||
case 3:
|
||||
return new InsnNode(ICONST_3);
|
||||
case 4:
|
||||
return new InsnNode(ICONST_4);
|
||||
case 5:
|
||||
return new InsnNode(ICONST_5);
|
||||
default:
|
||||
return new IntInsnNode(BIPUSH, num);
|
||||
}
|
||||
private void injectMockContextClean(MethodNode mn) {
|
||||
mn.instructions.insertBefore(mn.instructions.getFirst(), new MethodInsnNode(INVOKESTATIC,
|
||||
CLASS_MOCK_CONTEXT_UTIL, METHOD_CLEAN, DESC_METHOD_CLEAN, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.alibaba.testable.agent.handler.test;
|
||||
|
||||
|
||||
import com.alibaba.testable.agent.model.TestCaseMethodType;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
abstract public class Framework {
|
||||
|
||||
public boolean hasTestAfterMethod;
|
||||
|
||||
/**
|
||||
* Check whether the test class using current test framework
|
||||
* @param classAnnotations annotations of the class
|
||||
* @param methodAnnotations annotations of all methods
|
||||
* @return fit or not
|
||||
*/
|
||||
public boolean fit(Set<String> classAnnotations, Set<String> methodAnnotations) {
|
||||
if (methodAnnotations.contains(getTestAfterAnnotation())) {
|
||||
hasTestAfterMethod = true;
|
||||
return true;
|
||||
} else {
|
||||
return methodAnnotations.contains(getTestAnnotation());
|
||||
}
|
||||
}
|
||||
|
||||
public TestCaseMethodType checkMethodType(MethodNode mn) {
|
||||
if (mn.visibleAnnotations == null) {
|
||||
return TestCaseMethodType.OTHERS;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (an.desc.equals(getTestAnnotation())) {
|
||||
return TestCaseMethodType.TEST;
|
||||
} else if (an.desc.equals(getTestAfterAnnotation())) {
|
||||
return TestCaseMethodType.AFTER_TEST;
|
||||
}
|
||||
}
|
||||
return TestCaseMethodType.OTHERS;
|
||||
}
|
||||
|
||||
public abstract String getTestAnnotation();
|
||||
|
||||
public abstract String getTestAfterAnnotation();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.alibaba.testable.agent.handler.test;
|
||||
|
||||
public class JUnit4Framework extends Framework {
|
||||
|
||||
private static final String ANNOTATION_TEST = "Lorg/junit/Test;";
|
||||
private static final String ANNOTATION_AFTER_TEST = "Lorg/junit/After;";
|
||||
|
||||
@Override
|
||||
public String getTestAnnotation() {
|
||||
return ANNOTATION_TEST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTestAfterAnnotation() {
|
||||
return ANNOTATION_AFTER_TEST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.alibaba.testable.agent.handler.test;
|
||||
|
||||
public class JUnit5Framework extends Framework {
|
||||
|
||||
private static final String ANNOTATION_TEST = "Lorg/junit/jupiter/api/Test;";
|
||||
private static final String ANNOTATION_AFTER_TEST = "Lorg/junit/jupiter/api/AfterEach;";
|
||||
|
||||
@Override
|
||||
public String getTestAnnotation() {
|
||||
return ANNOTATION_TEST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTestAfterAnnotation() {
|
||||
return ANNOTATION_AFTER_TEST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.alibaba.testable.agent.handler.test;
|
||||
|
||||
public class TestNgFramework extends Framework {
|
||||
|
||||
private static final String ANNOTATION_TEST = "Lorg/testng/annotations/Test;";
|
||||
private static final String ANNOTATION_AFTER_TEST = "Lorg/testng/annotations/AfterMethod;";
|
||||
|
||||
@Override
|
||||
public String getTestAnnotation() {
|
||||
return ANNOTATION_TEST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTestAfterAnnotation() {
|
||||
return ANNOTATION_AFTER_TEST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.alibaba.testable.agent.handler.test;
|
||||
|
||||
import com.alibaba.testable.agent.model.TestCaseMethodType;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
|
||||
|
||||
public class TestNgOnClassFramework extends TestNgFramework {
|
||||
|
||||
@Override
|
||||
public boolean fit(Set<String> classAnnotations, Set<String> methodAnnotations) {
|
||||
if (classAnnotations.contains(getTestAnnotation())) {
|
||||
if (methodAnnotations.contains(getTestAfterAnnotation())) {
|
||||
hasTestAfterMethod = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TestCaseMethodType checkMethodType(MethodNode mn) {
|
||||
if (mn.visibleAnnotations == null) {
|
||||
return (mn.access & ACC_PUBLIC) != 0 ? TestCaseMethodType.TEST : TestCaseMethodType.OTHERS;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (an.desc.equals(getTestAfterAnnotation())) {
|
||||
return TestCaseMethodType.AFTER_TEST;
|
||||
}
|
||||
}
|
||||
return (mn.access & ACC_PUBLIC) != 0 ? TestCaseMethodType.TEST : TestCaseMethodType.OTHERS;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user