Compare commits

..

33 Commits

Author SHA1 Message Date
金戟
bf4c263d93 bump to v0.4.2 2020-12-15 19:19:58 +08:00
金戟
28bde0b85a method invocation could contain LabelNode and LineNumberNode 2020-12-15 14:19:05 +08:00
金戟
179e71c7c2 loaded class cache not actually working 2020-12-14 22:57:13 +08:00
金戟
f624c9131e fix null pointer exception 2020-12-14 22:45:41 +08:00
金戟
83974cfb2a avoid duplicate test class injection 2020-12-14 22:36:35 +08:00
金戟
abb6d04c16 support set log level via maven plugin 2020-12-14 21:25:51 +08:00
金戟
16ec3b9444 also add drawbacks 2020-12-14 19:13:12 +08:00
金戟
180dfe9da8 set to next snapshot 2020-12-14 19:06:38 +08:00
金戟
92a2c69e05 replace trump with simpson 2020-12-12 10:36:42 +08:00
金戟
f40834eb94 add comparation doc 2020-12-12 08:42:08 +08:00
金戟
2dea6f9480 update docs and bump version to 0.4.1 2020-12-12 00:05:53 +08:00
金戟
2bdad3c1f6 separete description of PrivateAccessor from @EnablePrivateAccess 2020-12-11 21:36:30 +08:00
金戟
3f672e45da do deprecate instead of removal 2020-12-11 21:19:16 +08:00
金戟
328c8540a8 use MockMethod and MockConstructor replace TestableMock annotation 2020-12-11 20:29:49 +08:00
金戟
857b5e2295 bump to v0.4.0 2020-12-11 12:22:22 +08:00
金戟
1ec2c48c4b releasing config needed in parent pom 2020-12-10 20:09:27 +08:00
金戟
0ae39d9deb green light the demo package 2020-12-10 20:02:23 +08:00
金戟
dad2ea3a76 update log level when default log level changed 2020-12-10 19:22:22 +08:00
金戟
260fe94a49 fix a classloader filter issue for java 9+ 2020-12-10 19:21:33 +08:00
金戟
dfb1e2c094 revert maven plugin pom change 2020-12-10 16:11:14 +08:00
金戟
c8115f9623 add verbose log when test start 2020-12-10 15:47:55 +08:00
金戟
4def468d85 suggest invoking original method 2020-12-10 09:17:44 +08:00
金戟
efe57feeda maven plugin will not follow the parent version 2020-12-10 08:54:39 +08:00
金戟
5150868e8c move CONSTRUCTOR to TestableConst 2020-12-10 08:45:01 +08:00
金戟
757becc08b update doc about generics type method and add kotlin demo 2020-12-09 23:37:26 +08:00
金戟
857cbac81c add demo for template method mocking 2020-12-09 20:03:19 +08:00
金戟
495c08f716 extra common configure to parent pom 2020-12-08 16:22:14 +08:00
金戟
da650eb14d add testable all module 2020-12-08 15:51:51 +08:00
金戟
bd0ec5f4b7 reduce log reset frequency 2020-12-07 23:26:18 +08:00
金戟
65807eece7 avoid test case naming conflict between classes 2020-12-05 10:16:22 +08:00
金戟
b74b21cae6 set to snapshot 2020-12-05 09:39:20 +08:00
金戟
ad63de628d refactor log methods 2020-12-05 09:29:14 +08:00
金戟
08aa73b4bd use global log level 2020-12-04 22:50:02 +08:00
57 changed files with 1075 additions and 475 deletions

View File

@@ -2,17 +2,19 @@
换种思路写Mock让单元测试更简单。
无需初始化不挑测试框架甭管要换的方法是被测类的私有方法、静态方法还是其他任何类的成员方法也甭管要换的对象是怎么创建的。写好Mock方法加个`@TestableMock`注解,一切统统搞定。
无需初始化不挑测试框架甭管要换的方法是被测类的私有方法、静态方法还是其他任何类的成员方法也甭管要换的对象是怎么创建的。写好Mock方法加个`@MockMethod`注解,一切统统搞定。
文档https://alibaba.github.io/testable-mock/
## 目录结构
```bash
|-- testable-core 核心组件,提供注解和工具类
|-- testable-processor ➜ 编译期代码预处理组件,提供测试辅助功能
|-- testable-agent ➜ JavaAgent组件提供Mock测试相关功能
|-- testable-maven-plugin ➜ Maven插件组件用于简化JavaAgent注入
|-- testable-parent提供各子模块的公共父pom文件
|-- testable-all ➜ 依赖聚合,便于一次性引用所有子模块功能
|-- testable-processor ➜ 编译期代码预处理模块,提供测试辅助功能
|-- testable-agent ➜ JavaAgent模块提供Mock测试相关功能
|-- testable-core ➜ 基础功能模块提供Mock相关注解和工具类
|-- testable-maven-plugin ➜ Maven插件模块用于简化JavaAgent注入
|-- demo
| |-- java-demo ➜ Java语言的示例代码
| `-- kotlin-demo ➜ Kotlin语言的示例代码
@@ -33,5 +35,5 @@ mvn clean install
docsify serve docs
```
> Testable文档使用`docsify`工具生成,构建前请安装[nodejs](https://nodejs.org/en/download/)运行时,并使用`npm install -g docsify`命令安装文档生成工具。
> TestableMock文档使用`docsify`工具生成,构建前请安装[nodejs](https://nodejs.org/en/download/)运行时,并使用`npm install -g docsify`命令安装文档生成工具。

View File

@@ -12,9 +12,8 @@ repositories {
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
testImplementation('com.alibaba.testable:testable-processor:0.3.2')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.3.2')
testRuntimeOnly('com.alibaba.testable:testable-agent:0.3.2')
testImplementation('com.alibaba.testable:testable-all:0.4.2')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.2')
}
test {

View File

@@ -11,26 +11,21 @@
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<testable.version>0.3.2</testable.version>
<junit.version>5.6.2</junit.version>
<testable.version>0.4.2</testable.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-processor</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-agent</artifactId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.2</version>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -0,0 +1,35 @@
package com.alibaba.testable.demo;
import java.util.*;
public class DemoTemplate {
private <T> List<T> getList(T value) {
List<T> l = new ArrayList<>();
l.add(value);
return l;
}
private <K, V> Map<K, V> getMap(K key, V value) {
Map<K, V> m = new HashMap<>();
m.put(key, value);
return m;
}
public String singleTemplateMethod() {
List<String> list = getList("demo");
return list.get(0);
}
public String doubleTemplateMethod() {
Map<String, String> map = getMap("hello", "testable");
return map.get("hello");
}
public Set<?> newTemplateMethod() {
Set<String> set = new HashSet<>();
set.add("world");
return set;
}
}

View File

@@ -1,85 +1,90 @@
package com.alibaba.testable.demo;
import com.alibaba.testable.core.annotation.TestableMock;
import com.alibaba.testable.core.annotation.MockMethod;
import com.alibaba.testable.demo.model.BlackBox;
import com.alibaba.testable.demo.model.Box;
import com.alibaba.testable.demo.model.Color;
import org.junit.jupiter.api.Test;
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* 演示父类变量引用子类对象时的Mock场景
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
*/
class DemoInheritTest {
@TestableMock(targetMethod = "put")
private DemoInherit demoInherit = new DemoInherit();
@MockMethod(targetMethod = "put")
private void put_into_box(Box self, String something) {
self.put("put_" + something + "_into_box");
}
@TestableMock(targetMethod = "put")
@MockMethod(targetMethod = "put")
private void put_into_blackbox(BlackBox self, String something) {
self.put("put_" + something + "_into_blackbox");
}
@TestableMock(targetMethod = "get")
@MockMethod(targetMethod = "get")
private String get_from_box(Box self) {
return "get_from_box";
}
@TestableMock(targetMethod = "get")
@MockMethod(targetMethod = "get")
private String get_from_blackbox(BlackBox self) {
return "get_from_blackbox";
}
@TestableMock(targetMethod = "getColor")
@MockMethod(targetMethod = "getColor")
private String get_color_from_color(Color self) {
return "color_from_color";
}
@TestableMock(targetMethod = "getColor")
@MockMethod(targetMethod = "getColor")
private String get_color_from_blackbox(BlackBox self) {
return "color_from_blackbox";
}
private DemoInherit demoInherit = new DemoInherit();
@Test
void should_able_to_mock_call_sub_object_method_by_parent_object() throws Exception {
void should_able_to_mock_call_sub_object_method_by_parent_object() {
BlackBox box = (BlackBox)demoInherit.putIntoBox();
verify("put_into_box").withTimes(1);
assertEquals("put_data_into_box", box.get());
}
@Test
void should_able_to_mock_call_sub_object_method_by_sub_object() throws Exception {
void should_able_to_mock_call_sub_object_method_by_sub_object() {
BlackBox box = demoInherit.putIntoBlackBox();
verify("put_into_blackbox").withTimes(1);
assertEquals("put_data_into_blackbox", box.get());
}
@Test
void should_able_to_mock_call_parent_object_method_by_parent_object() throws Exception {
void should_able_to_mock_call_parent_object_method_by_parent_object() {
String content = demoInherit.getFromBox();
verify("get_from_box").withTimes(1);
assertEquals("get_from_box", content);
}
@Test
void should_able_to_mock_call_parent_object_method_by_sub_object() throws Exception {
void should_able_to_mock_call_parent_object_method_by_sub_object() {
String content = demoInherit.getFromBlackBox();
verify("get_from_blackbox").withTimes(1);
assertEquals("get_from_blackbox", content);
}
@Test
void should_able_to_mock_call_interface_method_by_interface_object() throws Exception {
void should_able_to_mock_call_interface_method_by_interface_object() {
String color = demoInherit.getColorViaColor();
verify("get_color_from_color").withTimes(1);
assertEquals("color_from_color", color);
}
@Test
void should_able_to_mock_call_interface_method_by_sub_class_object() throws Exception {
void should_able_to_mock_call_interface_method_by_sub_class_object() {
String color = demoInherit.getColorViaBox();
verify("get_color_from_blackbox").withTimes(1);
assertEquals("color_from_blackbox", color);

View File

@@ -1,6 +1,6 @@
package com.alibaba.testable.demo;
import com.alibaba.testable.core.annotation.TestableMock;
import com.alibaba.testable.core.annotation.MockMethod;
import com.alibaba.testable.core.error.VerifyFailedError;
import com.alibaba.testable.demo.model.BlackBox;
import org.junit.jupiter.api.Test;
@@ -9,19 +9,24 @@ import static com.alibaba.testable.core.matcher.InvokeMatcher.*;
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
import static org.junit.jupiter.api.Assertions.fail;
/**
* 演示Mock方法调用校验器
* Demonstrate mock method invocation verifier
*/
class DemoMatcherTest {
private DemoMatcher demoMatcher = new DemoMatcher();
@TestableMock(targetMethod = "methodToBeMocked")
@MockMethod(targetMethod = "methodToBeMocked")
private void methodWithoutArgument(DemoMatcher self) {}
@TestableMock(targetMethod = "methodToBeMocked")
@MockMethod(targetMethod = "methodToBeMocked")
private void methodWithArguments(DemoMatcher self, Object a1, Object a2) {}
@TestableMock(targetMethod = "methodToBeMocked")
@MockMethod(targetMethod = "methodToBeMocked")
private void methodWithArrayArgument(DemoMatcher self, Object[] a) {}
@Test
void should_match_no_argument() {
demoMatcher.callMethodWithoutArgument();

View File

@@ -1,48 +1,56 @@
package com.alibaba.testable.demo;
import com.alibaba.testable.core.annotation.TestableMock;
import com.alibaba.testable.core.annotation.MockConstructor;
import com.alibaba.testable.core.annotation.MockMethod;
import com.alibaba.testable.demo.model.BlackBox;
import org.junit.jupiter.api.Test;
import java.util.concurrent.Executors;
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
import static com.alibaba.testable.core.tool.TestableTool.*;
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;
/**
* 演示基本的Mock功能
* Demonstrate basic mock functionality
*/
class DemoMockTest {
@TestableMock(targetMethod = CONSTRUCTOR)
private DemoMock demoMock = new DemoMock();
@MockConstructor
private BlackBox createBlackBox(String text) {
return new BlackBox("mock_" + text);
}
@TestableMock
@MockMethod
private String innerFunc(DemoMock self, String text) {
return "mock_" + text;
}
@TestableMock
@MockMethod
private String trim(String self) {
return "trim_string";
}
@TestableMock(targetMethod = "substring")
@MockMethod(targetMethod = "substring")
private String sub(String self, int i, int j) {
return "sub_string";
}
@TestableMock
@MockMethod
private boolean startsWith(String self, String s) {
return false;
}
@TestableMock
@MockMethod
private BlackBox secretBox(BlackBox ignore) {
return new BlackBox("not_secret_box");
}
@TestableMock
@MockMethod
private String callFromDifferentMethod(DemoMock self) {
if (TEST_CASE.equals("should_able_to_get_test_case_name")) {
return "mock_special";
@@ -53,10 +61,9 @@ class DemoMockTest {
}
}
private DemoMock demoMock = new DemoMock();
@Test
void should_able_to_mock_new_object() throws Exception {
void should_able_to_mock_new_object() {
assertEquals("mock_something", demoMock.newFunc());
verify("createBlackBox").with("something");
}
@@ -68,7 +75,7 @@ class DemoMockTest {
}
@Test
void should_able_to_mock_common_method() throws Exception {
void should_able_to_mock_common_method() {
assertEquals("trim_string__sub_string__false", demoMock.commonFunc());
verify("trim").withTimes(1);
verify("sub").withTimes(1);
@@ -76,7 +83,7 @@ class DemoMockTest {
}
@Test
void should_able_to_mock_static_method() throws Exception {
void should_able_to_mock_static_method() {
assertEquals("not_secret_box", demoMock.getBox().get());
verify("secretBox").withTimes(1);
}

View File

@@ -6,19 +6,23 @@ import org.junit.jupiter.api.Test;
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() throws Exception {
void should_able_to_access_private_method() {
assertEquals("hello - 1", demoPrivateAccess.privateFunc("hello", 1));
assertEquals("hello - 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", "hello", 1));
}
@Test
void should_able_to_access_private_field() throws Exception {
void should_able_to_access_private_field() {
demoPrivateAccess.count = 2;
assertEquals(new Integer(2), demoPrivateAccess.count);
@@ -27,13 +31,13 @@ class DemoPrivateAccessTest {
}
@Test
void should_able_to_access_private_static_method() throws Exception {
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() throws Exception {
void should_able_to_access_private_static_field() {
//DemoPrivateAccess.staticCount = 2;
//assertEquals(new Integer(2), DemoPrivateAccess.staticCount);
@@ -42,7 +46,7 @@ class DemoPrivateAccessTest {
}
@Test
void should_able_to_update_final_field() throws Exception {
void should_able_to_update_final_field() {
demoPrivateAccess.pi = 4.13;
assertEquals(4.13, demoPrivateAccess.pi);
}

View File

@@ -0,0 +1,93 @@
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 template method can be mocked
*/
class DemoTemplateTest {
private DemoTemplate demoTemplate = new DemoTemplate();
/* 第一种写法:使用泛型定义 */
/* 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());
}
}

View File

@@ -16,9 +16,8 @@ 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-processor:0.3.2")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.3.2")
testRuntimeOnly("com.alibaba.testable:testable-agent:0.3.2")
testImplementation("com.alibaba.testable:testable-all:0.4.2")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.2")
}
tasks.withType<KotlinCompile> {

View File

@@ -13,7 +13,8 @@
<kotlin.version>1.3.72</kotlin.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<testable.version>0.3.2</testable.version>
<junit.version>5.6.2</junit.version>
<testable.version>0.4.2</testable.version>
</properties>
<dependencies>
@@ -30,20 +31,14 @@
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-processor</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-agent</artifactId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.2</version>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -41,8 +41,24 @@ class DemoMatcher {
val longArray = arrayOf(1L, 2L)
methodToBeMocked(1, 2)
methodToBeMocked(1L, 2.0)
// below two invocations are equivalent
methodToBeMocked(listOf(1), setOf(1.0f))
// multiple lines method invocation
methodToBeMocked(object : ArrayList<Int?>() {
init {
add(1)
}
}, object : HashSet<Float?>() {
init {
add(1.0f)
}
})
// below two invocations are equivalent
methodToBeMocked(1.0, mapOf(1 to 1.0f))
methodToBeMocked(1.0, object : HashMap<Int?, Float?>(2) { init { put(1, 1.0f) } })
methodToBeMocked(floatList, floatList)
methodToBeMocked(longArray)
methodToBeMocked(arrayOf(1.0, 2.0))

View File

@@ -0,0 +1,35 @@
package com.alibaba.testable.demo
import java.util.ArrayList
import java.util.HashMap
import java.util.HashSet
class DemoTemplate {
private fun <T> getList(value: T): List<T> {
val l: MutableList<T> = ArrayList()
l.add(value)
return l
}
private fun <K, V> getMap(key: K, value: V): Map<K, V> {
val m: MutableMap<K, V> = HashMap()
m[key] = value
return m
}
fun singleTemplateMethod(): String {
val list = getList("demo")
return list[0]
}
fun doubleTemplateMethod(): String? {
val map = getMap("hello", "testable")
return map["hello"]
}
fun newTemplateMethod(): Set<*> {
val set: MutableSet<String> = HashSet()
set.add("world")
return set
}
}

View File

@@ -1,6 +1,6 @@
package com.alibaba.testable.demo
import com.alibaba.testable.core.annotation.TestableMock
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
@@ -8,39 +8,44 @@ import com.alibaba.testable.demo.model.Color
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
/**
* 演示父类变量引用子类对象时的Mock场景
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
*/
internal class DemoInheritTest {
@TestableMock(targetMethod = "put")
private val demoInherit = DemoInherit()
@MockMethod(targetMethod = "put")
private fun put_into_box(self: Box, something: String) {
self.put("put_" + something + "_into_box")
}
@TestableMock(targetMethod = "put")
@MockMethod(targetMethod = "put")
private fun put_into_blackbox(self: BlackBox, something: String) {
self.put("put_" + something + "_into_blackbox")
}
@TestableMock(targetMethod = "get")
@MockMethod(targetMethod = "get")
private fun get_from_box(self: Box): String {
return "get_from_box"
}
@TestableMock(targetMethod = "get")
@MockMethod(targetMethod = "get")
private fun get_from_blackbox(self: BlackBox): String {
return "get_from_blackbox"
}
@TestableMock(targetMethod = "getColor")
@MockMethod(targetMethod = "getColor")
private fun get_color_from_color(self: Color): String {
return "color_from_color"
}
@TestableMock(targetMethod = "getColor")
@MockMethod(targetMethod = "getColor")
private fun get_color_from_blackbox(self: BlackBox): String {
return "color_from_blackbox"
}
private val demoInherit = DemoInherit()
@Test
fun should_able_to_mock_call_sub_object_method_by_parent_object() {

View File

@@ -1,6 +1,6 @@
package com.alibaba.testable.demo
import com.alibaba.testable.core.annotation.TestableMock
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
@@ -8,22 +8,26 @@ import com.alibaba.testable.demo.model.BlackBox
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
/**
* 演示Mock方法调用校验器
* Demonstrate mock method invocation verifier
*/
internal class DemoMatcherTest {
@TestableMock(targetMethod = "methodToBeMocked")
private val demoMatcher = DemoMatcher()
@MockMethod(targetMethod = "methodToBeMocked")
private fun methodWithoutArgument(self: DemoMatcher) {
}
@TestableMock(targetMethod = "methodToBeMocked")
@MockMethod(targetMethod = "methodToBeMocked")
private fun methodWithArguments(self: DemoMatcher, a1: Any, a2: Any) {
}
@TestableMock(targetMethod = "methodToBeMocked")
@MockMethod(targetMethod = "methodToBeMocked")
private fun methodWithArrayArgument(self: DemoMatcher, a: Array<Any>) {
}
private val demoMatcher = DemoMatcher()
@Test
fun should_match_no_argument() {
@@ -40,8 +44,8 @@ internal class DemoMatcherTest {
InvokeVerifier.verify("methodWithArguments").withInOrder(InvokeMatcher.anyInt(), 2)
InvokeVerifier.verify("methodWithArguments").withInOrder(InvokeMatcher.anyLong(), InvokeMatcher.anyNumber())
// Note: Must use `::class.javaObjectType` for primary types check in Kotlin
InvokeVerifier.verify("methodWithArguments").with(1.0, InvokeMatcher.anyMapOf(Int::class.javaObjectType, Float::class.javaObjectType))
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyList(), InvokeMatcher.anySetOf(Float::class.javaObjectType))
InvokeVerifier.verify("methodWithArguments").with(1.0, InvokeMatcher.anyMapOf(Int::class.javaObjectType, Float::class.javaObjectType)).times(2)
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyList(), InvokeMatcher.anySetOf(Float::class.javaObjectType)).times(2)
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyList(), InvokeMatcher.anyListOf(Float::class.javaObjectType))
InvokeVerifier.verify("methodWithArrayArgument").with(InvokeMatcher.anyArrayOf(Long::class.javaObjectType))
InvokeVerifier.verify("methodWithArrayArgument").with(InvokeMatcher.anyArray())
@@ -66,12 +70,12 @@ internal class DemoMatcherTest {
@Test
fun should_match_with_times() {
demoMatcher.callMethodWithNumberArguments()
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(3)
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(4)
demoMatcher.callMethodWithNumberArguments()
var gotError = false
try {
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(4)
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(5)
} catch (e: VerifyFailedError) {
gotError = true
}

View File

@@ -1,42 +1,50 @@
package com.alibaba.testable.demo
import com.alibaba.testable.core.annotation.TestableMock
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.*
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 org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.util.concurrent.Executors
/**
* 演示基本的Mock功能
* Demonstrate basic mock functionality
*/
internal class DemoMockTest {
@TestableMock(targetMethod = CONSTRUCTOR)
private val demoMock = DemoMock()
@MockConstructor
private fun createBlackBox(text: String) = BlackBox("mock_$text")
@TestableMock
@MockMethod
private fun innerFunc(self: DemoMock, text: String) = "mock_$text"
@TestableMock
@MockMethod
private fun trim(self: BlackBox) = "trim_string"
@TestableMock(targetMethod = "substring")
@MockMethod(targetMethod = "substring")
private fun sub(self: BlackBox, i: Int, j: Int) = "sub_string"
@TestableMock
@MockMethod
private fun startsWith(self: BlackBox, s: String) = false
@TestableMock
@MockMethod
private fun secretBox(ignore: BlackBox): BlackBox {
return BlackBox("not_secret_box")
}
@TestableMock
@MockMethod
private fun createBox(ignore: ColorBox, color: String, box: BlackBox): BlackBox {
return BlackBox("White_${box.get()}")
}
@TestableMock
@MockMethod
private fun callFromDifferentMethod(self: DemoMock): String {
return if (TEST_CASE == "should_able_to_get_test_case_name") {
"mock_special"
@@ -48,7 +56,6 @@ internal class DemoMockTest {
}
}
private val demoMock = DemoMock()
@Test
fun should_able_to_mock_new_object() {

View File

@@ -4,7 +4,10 @@ import com.alibaba.testable.core.accessor.PrivateAccessor
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
/**
* 演示私有成员访问功能
* Demonstrate private member access functionality
*/
internal class DemoPrivateAccessTest {
private val demoPrivateAccess = DemoPrivateAccess()

View File

@@ -0,0 +1,62 @@
package com.alibaba.testable.demo
import com.alibaba.testable.core.annotation.MockConstructor
import com.alibaba.testable.core.annotation.MockMethod
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import java.util.*
/**
* 演示模板方法可以被Mock
* Demonstrate template method can be mocked
*/
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)
}
@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
}
@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()
Assertions.assertEquals("demo_mock_list", res)
}
@Test
fun should_able_to_mock_double_template_method() {
val res = demoTemplate.doubleTemplateMethod()
Assertions.assertEquals("testable_mock_map", res)
}
@Test
fun should_able_to_mock_new_template_method() {
val res = demoTemplate.newTemplateMethod()
Assertions.assertEquals(2, res.size)
val iterator = res.stream().iterator()
Assertions.assertEquals("insert_mock", iterator.next())
Assertions.assertEquals("world_mocked", iterator.next())
}
}

View File

@@ -1,13 +1,13 @@
package com.alibaba.testable.demo.util
import com.alibaba.testable.core.annotation.TestableMock
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 {
@TestableMock
@MockMethod
fun exists(f: File): Boolean {
return when (f.absolutePath) {
"/a/b" -> true
@@ -16,7 +16,7 @@ class PathUtilTest {
}
}
@TestableMock
@MockMethod
fun isDirectory(f: File): Boolean {
return when (f.absolutePath) {
"/a/b/c" -> true
@@ -24,12 +24,12 @@ class PathUtilTest {
}
}
@TestableMock
@MockMethod
fun delete(f: File): Boolean {
return true
}
@TestableMock
@MockMethod
fun listFiles(f: File): Array<File>? {
return when (f.absolutePath) {
"/a/b" -> arrayOf(File("/a/b/c"), File("/a/b/d"))

View File

@@ -14,7 +14,7 @@
<div id="app">Loading...</div>
<script>
window.$docsify = {
name: 'Testable',
name: 'TestableMock',
repo: 'https://github.com/alibaba/testable-mock',
loadSidebar: "sidebar.md",
loadNavbar: "navbar.md",

View File

@@ -9,4 +9,4 @@ TestableMock简介
于是,我们开发了`TestableMock`**一款特立独行的轻量Mock工具**。
![mock](https://testable-code.oss-cn-beijing.aliyuncs.com/mock.jpg)
![mock](https://testable-code.oss-cn-beijing.aliyuncs.com/mock-simpson.png)

View File

@@ -0,0 +1,15 @@
主流Mock工具对比
---
`TestableMock`目前主要的Mock工具主要有`Mockito``PowerMock``JMockit`,基本差异如下:
| 工具 | 原理 | 最小Mock单元 | 对被Mock方法的限制 | 上手难度 | IDE支持 |
| ---- | ---- | ---- | ---- | ---- | ---- |
| Mockito | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | **较容易** | **很好** |
| PowerMock | 自定义类加载器 | 类 | **任何方法皆可** | 较繁琐 | **较好** |
| JMockit | 运行时字节码修改 | 类 | 不能Mock构造方法(new操作符) | 较繁琐 | 一般 |
| TestableMock | 运行时字节码修改 | 方法 | **任何方法皆可** | **很容易** | 一般 |
相比之下,`TestabledMock`的功能与`PowerMock`基本平齐,且极易上手,只需掌握`@MockMethod`注解就可以完成绝大多数任务。
当前`TestableMock`的主要不足在于编写Mock方法时IDE尚无法即时提示方法参数是否正确匹配。若发现匹配效果不符合预期需要通过[自助问题排查](zh-cn/doc/troubleshooting.md)文档提供的方法在运行期进行校验。这个功能未来需要通过扩展主流IDE插件来提供。

View File

@@ -5,23 +5,37 @@
直接创建被测类对象,然后利用`TestableMock`访问私有成员的能力直接给这些字段赋值即可。
#### 2. 父类变量指向子类对象时如何实现Mock方法
#### 2. `TestableMock`是否能够与其他Mock工具一起使用
`TestableMock`可与其他基于动态代理机制的Mock工具安全的共同使用譬如`Mockito``EasyMock``MockRunner`等皆属此范畴。
对于其他会修改类加载器或被测类字节码的Mock工具譬如`PowerMock``JMockit`,尚无案例证明会与`TestableMock`发生冲突,但从原理来说二者可能存在不兼容风险,请谨慎使用。
#### 3. 父类变量指向子类对象时如何实现Mock方法
在代码中,经常会有使用<u>接口变量或父类变量</u>指向子类实例,调用父类或子类方法的情况。
这时候遵循一个原则Mock方法的首个参数类型**始终与发起调用的变量类型一致**。
因此,不论被调用方法来自父类还是子类,也不论子类是否覆写该方法Mock方法的首个参数类型都应该使用变量自身的接口或父类类型。
因此,不论实际被调用方法来自父类还是子类,也不论子类是否覆写该方法。若变量为父类型(或接口类型),则Mock方法的首个参数类型都应该使用相同的父类(或接口)类型。
参见Java和Kotlin示例中`DemoInheritTest`测试类的用例。
#### 3. 在Kotlin项目对`String`类中的方法进行Mock不生效
#### 4. 如何Mock对于泛型方法(模板方法)
与普通方法的Mock方法相同直接在Mock方法上使用相同的泛型参数即可。
参见Java和Kotlin示例中`DemoTemplateTest`测试类的用例。
不过由于JVM存在泛型擦除机制对于Java项目也可以直接使用`Object`类型替代泛型参数见Java版`DemoTemplateTest`测试类中被注释掉的"第二种写法"示例。
#### 5. 在Kotlin项目对`String`类中的方法进行Mock不生效
Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.String`。但在构建生成自字节码的时候又会被替换为Java的`java.lang.String`因此无论将Mock目标写为`kotlin.String``java.lang.String`均无法正常匹配到原始的被调用方法。
实际场景中需要对`String`类中的方法进行Mock的场景很少`TestableMock`暂未对这种情况做特别处理。
#### 4. `TestableMock`能否用于Android项目的测试
#### 6. `TestableMock`能否用于Android项目的测试
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。

View File

@@ -13,7 +13,7 @@ public test_case() {
}
```
这个用例会检查在执行被测方法`methodToTest()`时,名称是`mockMethod`的Mock方法应当被调用过,且调用时收到的参数值为123和"abc"假设被Mock的`mockMethod`方法有两个参数)。
这个用例会检查在执行被测方法`methodToTest()`时,名称是`mockMethod`的Mock方法是否有被调用过,且调用时收到的参数值是否为`123``"abc"`假设被Mock的`mockMethod`方法有两个参数)。
除了这种简单校验以外TestableMock当前已经支持了多种**校验器**,以及能够模糊匹配参数特征的**匹配器**。

View File

@@ -3,7 +3,9 @@
如今关于私有方法是否应该做单元测试的争论正逐渐消停,开发者的普遍实践已经给出事实答案。通过公有方法间接测私有方法在很多情况下难以进行,开发者们更愿意通过修改方法可见性的办法来让原本私有的方法在测试用例中变得可测。
此外在单元测试中时常会需要对被测对象进行特定的成员字段初始化但有时由于被测类的构造方法限制使得无法便捷的对这些字段进行赋值。那么能否在不破坏被测类型封装的情况下允许单元测试用例内的代码直接访问被测类的私有方法和成员变量呢TestableMock提供了种简单的解决方案。
此外在单元测试中时常会需要对被测对象进行特定的成员字段初始化但有时由于被测类的构造方法限制使得无法便捷的对这些字段进行赋值。那么能否在不破坏被测类型封装的情况下允许单元测试用例内的代码直接访问被测类的私有方法和成员变量呢TestableMock提供了种简单的解决方案。
### 方法一:使用`@EnablePrivateAccess`注解
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
@@ -14,6 +16,19 @@
访问和修改私有、常量成员时IDE可能会提示语法有误但编译器将能够正常运行测试。使用编译期代码增强目前仅实现了Java语言的适配
若不希望看到IDE的语法错误提醒或是在非Java语言的JVM工程譬如Kotlin语言也可以借助`PrivateAccessor`工具类来实现私有成员的访问
效果见`java-demo`示例项目`DemoPrivateAccessTest`测试类中的用例
效果见`java-demo``kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
### 方法二:使用`PrivateAccessor`工具类
若不希望看到IDE的语法错误提醒或是在非Java语言的JVM工程譬如Kotlin语言也可以借助`PrivateAccessor`工具类来直接访问私有成员。
这个类提供了6个静态方法
- `PrivateAccessor.get(被测对象, "私有字段名")` ➜ 读取被测类的私有成员
- `PrivateAccessor.set(被测对象, "私有字段名", 新的值)` ➜ 修改被测类的私有成员
- `PrivateAccessor.invoke(被测对象, "私有方法名", 调用参数..)` ➜ 调用被测类的私有方法
- `PrivateAccessor.getStatic(被测类型, "私有字段名")` ➜ 读取被测类的**静态**私有成员
- `PrivateAccessor.setStatic(被测类型, "私有字段名", 新的值)` ➜ 修改被测类的**静态**私有成员
- `PrivateAccessor.invokeStatic(被测类型, "私有方法名", 调用参数..)` ➜ 调用被测类的**静态**私有方法
详见`java-demo``kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。

View File

@@ -1,5 +1,19 @@
# Release Note
## 0.4.2
- support change javaagent global log level via maven plugin
- fix an issue of duplicate test class injection
- fix an issue cause multiple method invocation mock fail
## 0.4.1
- deprecate @TestableMock annotation, use @MockMethod and @MockConstructor instead
## 0.4.0
- fix a jvm 9+ compatibility issue cause by default classloader change
- fix a conflict issue when testcase name duplicated between different class
- refactor code structure, module `testable-all` added
- separate constant definition from `TestableTool` to `TestableConst`
## 0.3.2
- support grable project for both private member access and quick mock
- support access private static field and methods via PrivateAccessor

View File

@@ -9,13 +9,13 @@
## 在Maven项目中使用
在项目`pom.xml`文件中,增加`testable-processor`依赖和`maven-surefire-plugin`配置,具体方法如下。
在项目`pom.xml`文件中,增加`testable-all`依赖和`maven-surefire-plugin`配置,具体方法如下。
建议先添加一个标识TestableMock版本的`property`,便于统一管理:
```xml
<properties>
<testable.version>0.3.2</testable.version>
<testable.version>0.4.2</testable.version>
</properties>
```
@@ -25,20 +25,14 @@
<dependencies>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-processor</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-agent</artifactId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
```
最后在`build`区域的`plugins`列表里添加`maven-surefire-plugin`插件(如果已此插件则只需添加`<argLine>`部分配置):
最后在`build`区域的`plugins`列表里添加`maven-surefire-plugin`插件(如果已包含此插件则只需添加`<argLine>`部分配置):
```xml
<build>
@@ -68,9 +62,8 @@
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-processor:0.3.2')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.3.2')
testRuntimeOnly('com.alibaba.testable:testable-agent:0.3.2')
testImplementation('com.alibaba.testable:testable-all:0.4.2')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.2')
}
```

View File

@@ -12,13 +12,13 @@
5. ... ...
> 不返回任何值也不产生任何"副作用"的方法没有存在意义。
> 不返回任何值也不产生任何"副作用"的方法没有存在意义。
这些"副作用"归纳来说可分为两类:**修改外部变量**和**调用外部方法**。
这些"副作用"的本质归纳来说可分为两类:**修改外部变量**和**调用外部方法**。
通过TestableMock的私有字段访问和Mock校验器可以很方便的实现对"副作用"的结果检查。
#### 修改外部变量的void方法
### 1. 修改外部变量的void方法
例如,下面这个方法会根据输入修改私有成员变量`hashCache`
@@ -55,7 +55,7 @@ class DemoTest {
}
```
#### 调用外部方法的void方法
### 2. 调用外部方法的void方法
例如,下面这个方法会根据输入打印信息到控制台:
@@ -69,15 +69,20 @@ class Demo {
}
```
若要测试此方法可以利用TestableMock快速Mock掉`System.out.println`调用,然后用`InvokeVerifier.verify()`方法校验传入的打印内容:
若要测试此方法可以利用TestableMock快速Mock掉`System.out.println`方法。在Mock方法体里可以继续执行原调用相当于并不影响本来方法功能仅用于做调用记录也可以直接留空相当于去除了原方法的副作用
在执行完被测的void类型方法以后`InvokeVerifier.verify()`校验传入的打印内容是否符合预期:
```java
class DemoTest {
private Demo demo = new Demo();
// 拦截`System.out.println`调用
@TestableMock
public void println(PrintStream ps, String msg) {}
@MockMethod
public void println(PrintStream ps, String msg) {
// 执行原调用
ps.println(msg);
}
@Test
public void testRecordAction() {

View File

@@ -1,7 +1,7 @@
自助问题排查
---
相比Mockito等由开发者手工放置Mock类的做法TestableMock使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也有可能发生预料之外的Mock替换。
相比`Mockito`等由开发者手工放置Mock类的做法`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也有可能发生预料之外的Mock替换。
若要排查Mock相关的问题只需在测试类上添加`@MockWith`注解,并配置参数`diagnose`值为`MockDiagnose.ENABLE`在运行测试时就会打印出详细的Mock方法替换过程。

View File

@@ -1,17 +1,17 @@
快速Mock被测类的任意方法调用
---
相比以往Mock工具以类为粒度的Mock方式TestableMock允许用户直接定义需要Mock的单个方法并遵循约定优于配置的原则按照规则自动在测试运行时替换被测方法中的指定方法调用。
相比以往Mock工具以类为粒度的Mock方式`TestableMock`允许用户直接定义需要Mock的单个方法并遵循约定优于配置的原则按照规则自动在测试运行时替换被测方法中的指定方法调用。
具体的Mock方法定义约定如下
#### 1. 覆写任意类的方法调用
在测试类里定义一个有`@TestableMock`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
在测试类里定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
此时被测类中所有对该需覆写方法的调用将在单元测试运行时将自动被替换为对上述自定义Mock方法的调用。
**注意**:当遇到待覆写方法有重名时,可以将需覆写的方法名写到`@TestableMock`注解的`targetMethod`参数里这样Mock方法自身就可以随意命名了。
**注意**:当遇到待覆写方法有重名时,可以将需覆写的方法名写到`@MockMethod`注解的`targetMethod`参数里这样Mock方法自身就可以随意命名了。
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在测试类定义如下方法:
@@ -20,7 +20,7 @@
// 调用此方法的对象`"anything"`类型为`String`
// 则Mock方法签名在其参数列表首位增加一个类型为`String`的参数(名字随意)
// 此参数可用于获得当时的实际调用者的值和上下文
@TestableMock
@MockMethod
private String substring(String self, int i, int j) {
return "sub_string";
}
@@ -31,7 +31,7 @@ private String substring(String self, int i, int j) {
```java
// 使用`targetMethod`指定需Mock的方法名
// 此方法本身现在可以随意命名,但方法参数依然需要遵循相同的匹配规则
@TestableMock(targetMethod = "substring")
@MockMethod(targetMethod = "substring")
private String use_any_mock_method_name(String self, int i, int j) {
return "sub_string";
}
@@ -50,7 +50,7 @@ private String use_any_mock_method_name(String self, int i, int j) {
```java
// 被测类型是`DemoMock`
// 因此在定义Mock方法时在目标方法参数首位加一个类型为`DemoMock`的参数(名字随意)
@TestableMock
@MockMethod
private String innerFunc(DemoMock self, String text) {
return "mock_" + text;
}
@@ -60,7 +60,7 @@ private String innerFunc(DemoMock self, String text) {
#### 3. 覆写任意类的静态方法
对于静态方法的Mock与普通方法相同。但需要注意的是对于静态方法,传入Mock方法的第一个参数实际值始终是`null`
对于静态方法的Mock与普通方法相同。但需要注意的是静态方法Mock方法被调用时,传入的第一个参数实际值始终是`null`
例如,在被测类中调用了`BlackBox`类型中的静态方法`secretBox()`,改方法签名为`BlackBox secretBox()`则Mock方法如下
@@ -68,7 +68,7 @@ private String innerFunc(DemoMock self, String text) {
// 目标静态方法定义在`BlackBox`类型中
// 在定义Mock方法时在目标方法参数首位加一个类型为`BlackBox`的参数(名字随意)
// 此参数仅用于标识目标类型,实际传入值将始终为`null`
@TestableMock
@MockMethod
private BlackBox secretBox(BlackBox ignore) {
return new BlackBox("not_secret_box");
}
@@ -78,7 +78,7 @@ private BlackBox secretBox(BlackBox ignore) {
#### 4. 覆写任意类的new操作
在测试类里定义一个有`@TestableMock`注解的普通方法,将注解的`targetMethod`参数写为"<init>",然后使该方法与要被创建类型的构造函数参数、返回值类型完全一致,方法名称随意。
在测试类里定义一个有`@MockContructor`注解的普通方法使该方法返回值类型为要被创建的对象类型且方法参数与要Mock的构造函数参数完全一致,方法名称随意。
此时被测类中所有用`new`创建指定类的操作并使用了与Mock方法参数一致的构造函数将被替换为对该自定义方法的调用。
@@ -86,14 +86,15 @@ private BlackBox secretBox(BlackBox ignore) {
```java
// 要覆写的构造函数签名为`BlackBox(String)`
// 无需在Mock方法参数列表增加额外参数由于使用了`targetMethod`参数,Mock方法的名称随意起
// 此处的`CONSTRUCTOR`为`TestableTool`辅助类提供的常量,值为"<init>"
@TestableMock(targetMethod = CONSTRUCTOR)
// 无需在Mock方法参数列表增加额外参数Mock方法的名称随意起
@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. 识别当前测试用例和调用来源

View File

@@ -10,5 +10,6 @@
- [自助问题排查](zh-cn/doc/troubleshooting.md)
- [Testable Maven插件](zh-cn/doc/use-maven-plugin.md)
- 其他文档
- 技术参考
- [主流Mock工具对比](zh-cn/doc/comparation.md)
- [Release Note](zh-cn/doc/release-note.md)

View File

@@ -4,14 +4,16 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<artifactId>testable-aggregate</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>testable-core</module>
<module>testable-parent</module>
<module>testable-processor</module>
<module>testable-core</module>
<module>testable-agent</module>
<module>testable-all</module>
<module>testable-maven-plugin</module>
<module>demo</module>
</modules>

View File

@@ -3,47 +3,19 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.4.2</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-agent</artifactId>
<version>0.3.2</version>
<packaging>jar</packaging>
<name>testable-agent</name>
<description>Unit test enhancement toolkit</description>
<url>https://github.com/alibaba/testable-mock</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://github.com/alibaba/testable-mock/blob/master/LICENSE</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<name>金戟</name>
<email>jinji.lf@alibaba-inc.com</email>
</developer>
</developers>
<scm>
<connection>scm:git:git@github.com:alibaba/testable-mock.git</connection>
<url>git@github.com:alibaba/testable-mock.git</url>
<tag>HEAD</tag>
</scm>
<properties>
<java.version>1.6</java.version>
<project.compiler.level>1.6</project.compiler.level>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<asm.lib.version>8.0.1</asm.lib.version>
<junit.version>5.6.2</junit.version>
<testable.version>0.3.2</testable.version>
<plugin.compiler.version>3.8.1</plugin.compiler.version>
<plugin.surefire.version>3.0.0-M5</plugin.surefire.version>
<plugin.jar.version>3.2.0</plugin.jar.version>
<plugin.shade.version>3.2.4</plugin.shade.version>
<plugin.source.version>3.2.0</plugin.source.version>
<plugin.javadoc.version>3.2.0</plugin.javadoc.version>
<plugin.gpg.version>1.6</plugin.gpg.version>
<plugin.staging.version>1.6.8</plugin.staging.version>
</properties>
<dependencies>

View File

@@ -1,7 +1,7 @@
package com.alibaba.testable.agent;
import com.alibaba.testable.agent.transformer.TestableClassTransformer;
import com.alibaba.testable.agent.util.LogUtil;
import com.alibaba.testable.core.util.LogUtil;
import java.lang.instrument.Instrumentation;
@@ -13,6 +13,9 @@ public class PreMain {
private static final String AND = "&";
private static final String DEBUG = "debug";
private static final String VERBOSE = "verbose";
private static final String LOG_LEVEL = "logLevel";
private static final String EQUAL = "=";
public static void premain(String agentArgs, Instrumentation inst) {
parseArgs(agentArgs);
@@ -24,10 +27,28 @@ public class PreMain {
return;
}
for (String a : args.split(AND)) {
if (a.equals(DEBUG)) {
LogUtil.globalDebugEnable = true;
int i = a.indexOf(EQUAL);
if (i > 0) {
String k = a.substring(0, i);
String v = a.substring(i + 1);
if (k.equals(LOG_LEVEL)) {
setLogLevel(v);
}
} else {
setLogLevel(a);
}
}
}
private static boolean setLogLevel(String level) {
if (level.equals(DEBUG)) {
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_DIAGNOSE);
return true;
} else if (level.equals(VERBOSE)) {
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_VERBOSE);
return true;
}
return false;
}
}

View File

@@ -8,12 +8,19 @@ public class ConstPool {
public static final String DOT = ".";
public static final String SLASH = "/";
public static final String CONSTRUCTOR = "<init>";
public static final String TEST_POSTFIX = "Test";
public static final String TESTABLE_INJECT_REF = "_testableInternalRef";
public static final String FIELD_TARGET_METHOD = "targetMethod";
public static final String MOCK_WITH = "com.alibaba.testable.core.annotation.MockWith";
public static final String MOCK_METHOD = "com.alibaba.testable.core.annotation.MockMethod";
public static final String MOCK_CONSTRUCTOR = "com.alibaba.testable.core.annotation.MockConstructor";
public static final String TESTABLE_MOCK = "com.alibaba.testable.core.annotation.TestableMock";
/**
* Name of the constructor method
*/
public static final String CONSTRUCTOR = "<init>";
}

View File

@@ -4,7 +4,7 @@ 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.LogUtil;
import com.alibaba.testable.core.util.LogUtil;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;
@@ -52,7 +52,7 @@ public class SourceClassHandler extends BaseClassHandler {
private void transformMethod(ClassNode cn, MethodNode mn, Set<MethodInfo> memberInjectMethods,
Set<MethodInfo> newOperatorInjectMethods) {
LogUtil.debug(" Handling method %s", mn.name);
LogUtil.diagnose(" Handling method %s", mn.name);
AbstractInsnNode[] instructions = mn.instructions.toArray();
List<MethodInfo> memberInjectMethodList = new ArrayList<MethodInfo>(memberInjectMethods);
int i = 0;
@@ -67,6 +67,8 @@ public class SourceClassHandler extends BaseClassHandler {
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
@@ -119,28 +121,9 @@ public class SourceClassHandler extends BaseClassHandler {
}
private int getMemberMethodStart(AbstractInsnNode[] instructions, int rangeEnd) {
int stackLevel = ClassUtil.getParameterTypes(((MethodInsnNode)instructions[rangeEnd]).desc).size();
int stackLevel = getInitialStackLevel((MethodInsnNode)instructions[rangeEnd]);
for (int i = rangeEnd - 1; i >= 0; i--) {
switch (instructions[i].getOpcode()) {
case Opcodes.INVOKESPECIAL:
case Opcodes.INVOKEVIRTUAL:
case Opcodes.INVOKEINTERFACE:
stackLevel += stackEffectOfInvocation(instructions[i]) + 1;
if (((MethodInsnNode)instructions[i]).name.equals(ConstPool.CONSTRUCTOR)) {
// constructor must be INVOKESPECIAL and implicitly pop 1 more stack
stackLevel++;
}
break;
case Opcodes.INVOKESTATIC:
case Opcodes.INVOKEDYNAMIC:
stackLevel += stackEffectOfInvocation(instructions[i]);
break;
case -1:
// reach LineNumberNode or LabelNode
return i + 1;
default:
stackLevel -= BytecodeUtil.stackEffect(instructions[i].getOpcode());
}
stackLevel += getStackLevelChange(instructions[i]);
if (stackLevel < 0) {
return i;
}
@@ -148,6 +131,38 @@ public class SourceClassHandler extends BaseClassHandler {
return -1;
}
private int getInitialStackLevel(MethodInsnNode instruction) {
int stackLevel = ClassUtil.getParameterTypes((instruction).desc).size();
switch (instruction.getOpcode()) {
case Opcodes.INVOKESPECIAL:
case Opcodes.INVOKEVIRTUAL:
case Opcodes.INVOKEINTERFACE:
return stackLevel;
case Opcodes.INVOKESTATIC:
case Opcodes.INVOKEDYNAMIC:
return stackLevel - 1;
default:
return 0;
}
}
private int getStackLevelChange(AbstractInsnNode instruction) {
switch (instruction.getOpcode()) {
case Opcodes.INVOKESPECIAL:
case Opcodes.INVOKEVIRTUAL:
case Opcodes.INVOKEINTERFACE:
return stackEffectOfInvocation(instruction) + 1;
case Opcodes.INVOKESTATIC:
case Opcodes.INVOKEDYNAMIC:
return stackEffectOfInvocation(instruction);
case -1:
// either LabelNode or LineNumberNode
return 0;
default:
return -BytecodeUtil.stackEffect(instruction.getOpcode());
}
}
private int stackEffectOfInvocation(AbstractInsnNode instruction) {
String desc = ((MethodInsnNode)instruction).desc;
return ClassUtil.getParameterTypes(desc).size() - (ClassUtil.getReturnType(desc).isEmpty() ? 0 : 1);
@@ -155,7 +170,7 @@ public class SourceClassHandler extends BaseClassHandler {
private AbstractInsnNode[] replaceNewOps(ClassNode cn, MethodNode mn, String newOperatorInjectMethodName,
AbstractInsnNode[] instructions, int start, int end) {
LogUtil.debug(" Line %d, mock method %s used", getLineNum(instructions, start),
LogUtil.diagnose(" Line %d, mock method %s used", getLineNum(instructions, start),
newOperatorInjectMethodName);
String classType = ((TypeInsnNode)instructions[start]).desc;
String constructorDesc = ((MethodInsnNode)instructions[end]).desc;
@@ -187,7 +202,7 @@ public class SourceClassHandler extends BaseClassHandler {
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, String substitutionMethod,
AbstractInsnNode[] instructions, String ownerClass,
int opcode, int start, int end) {
LogUtil.debug(" Line %d, mock method %s used", getLineNum(instructions, start), substitutionMethod);
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);
@@ -201,7 +216,7 @@ public class SourceClassHandler extends BaseClassHandler {
mn.instructions.remove(instructions[end - 1]);
}
}
// method with @TestableMock will be modified as public access, so INVOKEVIRTUAL is used
// method with @MockMethod will be modified as public access, so INVOKEVIRTUAL is used
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,
substitutionMethod, addFirstParameter(method.desc, ClassUtil.fitCompanionClassName(ownerClass)), false));
mn.instructions.remove(instructions[end]);

View File

@@ -4,12 +4,14 @@ 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.core.util.LogUtil;
import org.objectweb.asm.tree.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
/**
* @author flin
@@ -23,13 +25,12 @@ public class TestClassHandler extends BaseClassHandler {
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_MARK_TEST_CASE_BEGIN = "markTestCaseBegin";
private static final String METHOD_RECORD_MOCK_INVOKE = "recordMockInvoke";
private static final String SIGNATURE_TESTABLE_UTIL_METHOD = "(Ljava/lang/Object;)Ljava/lang/String;";
private static final String SIGNATURE_CURRENT_TEST_CASE_NAME = "(Ljava/lang/Object;)Ljava/lang/String;";
private static final String SIGNATURE_CURRENT_SOURCE_METHOD_NAME = "()Ljava/lang/String;";
private static final String SIGNATURE_VOID_METHOD_WITHOUT_PARAMETER = "()V";
private static final String SIGNATURE_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;Z)V";
private static final Map<String, String> FIELD_TO_METHOD_MAPPING = new HashMap<String, String>() {{
put(FIELD_TEST_CASE, METHOD_CURRENT_TEST_CASE_NAME);
put(FIELD_SOURCE_METHOD, METHOD_CURRENT_SOURCE_METHOD_NAME);
}};
/**
* Handle bytecode of test class
@@ -37,11 +38,19 @@ public class TestClassHandler extends BaseClassHandler {
*/
@Override
protected void transform(ClassNode cn) {
for (MethodNode m : cn.methods) {
transformMethod(cn, m);
Iterator<FieldNode> iterator = cn.fields.iterator();
if (iterator.hasNext()) {
if (ConstPool.TESTABLE_INJECT_REF.equals(iterator.next().name)) {
// avoid duplicate injection
LogUtil.verbose("Duplicate injection found, ignore " + cn.name);
return;
}
}
cn.fields.add(new FieldNode(ACC_PUBLIC | ACC_STATIC, ConstPool.TESTABLE_INJECT_REF,
ClassUtil.toByteCodeClassName(cn.name), null, null));
for (MethodNode m : cn.methods) {
transformMethod(cn, m);
}
}
private void transformMethod(ClassNode cn, MethodNode mn) {
@@ -58,7 +67,9 @@ public class TestClassHandler extends BaseClassHandler {
for (AnnotationNode n : mn.visibleAnnotations) {
visibleAnnotationNames.add(n.desc);
}
if (visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.TESTABLE_MOCK))) {
if (visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD)) ||
visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.TESTABLE_MOCK)) ||
visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR))) {
mn.access &= ~ACC_PRIVATE;
mn.access &= ~ACC_PROTECTED;
mn.access |= ACC_PUBLIC;
@@ -90,11 +101,18 @@ public class TestClassHandler extends BaseClassHandler {
private AbstractInsnNode[] replaceTestableUtilField(MethodNode mn, AbstractInsnNode[] instructions,
String fieldName, int pos) {
InsnList il = new InsnList();
il.add(new VarInsnNode(ALOAD, 0));
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, FIELD_TO_METHOD_MAPPING.get(fieldName),
SIGNATURE_TESTABLE_UTIL_METHOD, false));
mn.instructions.insert(instructions[pos], il);
mn.instructions.remove(instructions[pos]);
if (FIELD_TEST_CASE.equals(fieldName)) {
il.add(new VarInsnNode(ALOAD, 0));
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();
}
@@ -131,6 +149,9 @@ public class TestClassHandler extends BaseClassHandler {
private boolean isMockForConstructor(MethodNode mn) {
for (AnnotationNode an : mn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.MOCK_CONSTRUCTOR)) {
return true;
}
String method = AnnotationUtil.getAnnotationParameter
(an, ConstPool.FIELD_TARGET_METHOD, null, String.class);
if (ConstPool.CONSTRUCTOR.equals(method)) {
@@ -180,18 +201,24 @@ public class TestClassHandler extends BaseClassHandler {
private void injectTestableRef(ClassNode cn, MethodNode mn) {
InsnList il = new InsnList();
// Initialize "_testableInternalRef"
il.add(new VarInsnNode(ALOAD, 0));
il.add(new FieldInsnNode(PUTSTATIC, cn.name, ConstPool.TESTABLE_INJECT_REF,
ClassUtil.toByteCodeClassName(cn.name)));
mn.instructions.insertBefore(mn.instructions.get(0), il);
// Invoke "markTestCaseBegin"
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_MARK_TEST_CASE_BEGIN,
SIGNATURE_VOID_METHOD_WITHOUT_PARAMETER, false));
mn.instructions.insertBefore(mn.instructions.getFirst(), il);
}
/**
* Different unit test framework may have different @Test annotation
* but they should always NOT private, protected or static
* and has neither parameter nor return value
*/
private boolean couldBeTestMethod(MethodNode mn) {
return (mn.access & (ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC)) == 0 ;
return (mn.access & (ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC)) == 0 &&
mn.desc.equals(SIGNATURE_VOID_METHOD_WITHOUT_PARAMETER);
}
}

View File

@@ -1,39 +0,0 @@
package com.alibaba.testable.agent.model;
import org.objectweb.asm.tree.AnnotationNode;
/**
* Record parameter fetch from @MockWith annotation
*
* @author flin
*/
public class CachedMockParameter {
private final boolean classExist;
private final AnnotationNode mockWith;
private CachedMockParameter(boolean classExist, AnnotationNode mockWith) {
this.classExist = classExist;
this.mockWith = mockWith;
}
public static CachedMockParameter notExist() {
return new CachedMockParameter(false, null);
}
public static CachedMockParameter exist() {
return new CachedMockParameter(true, null);
}
public static CachedMockParameter exist(AnnotationNode mockWith) {
return new CachedMockParameter(true, mockWith);
}
public boolean isClassExist() {
return classExist;
}
public AnnotationNode getMockWith() {
return mockWith;
}
}

View File

@@ -3,13 +3,11 @@ package com.alibaba.testable.agent.transformer;
import com.alibaba.testable.agent.constant.ConstPool;
import com.alibaba.testable.agent.handler.SourceClassHandler;
import com.alibaba.testable.agent.handler.TestClassHandler;
import com.alibaba.testable.agent.model.CachedMockParameter;
import com.alibaba.testable.agent.tool.ImmutablePair;
import com.alibaba.testable.agent.model.MethodInfo;
import com.alibaba.testable.agent.tool.ComparableWeakRef;
import com.alibaba.testable.agent.util.AnnotationUtil;
import com.alibaba.testable.agent.util.ClassUtil;
import com.alibaba.testable.agent.util.LogUtil;
import com.alibaba.testable.core.util.LogUtil;
import com.alibaba.testable.core.model.MockDiagnose;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.AnnotationNode;
@@ -18,7 +16,6 @@ import org.objectweb.asm.tree.MethodNode;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.net.URLClassLoader;
import java.security.ProtectionDomain;
import java.util.*;
@@ -30,32 +27,42 @@ import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassNa
public class TestableClassTransformer implements ClassFileTransformer {
private static final String FIELD_DIAGNOSE = "diagnose";
private final Map<ComparableWeakRef<String>, CachedMockParameter> loadedClass =
new WeakHashMap<ComparableWeakRef<String>, CachedMockParameter>();
/**
* Just avoid spend time to scan those surely non-user classes
* Should keep these lists as tiny as possible
*/
private final String[] WHITELIST_PREFIXES = new String[] { "com/alibaba/testable/demo/" };
private final String[] BLACKLIST_PREFIXES = new String[] { "jdk/", "java/", "javax/", "com/sun/",
"org/apache/maven/", "com/alibaba/testable/", "junit/", "org/junit/", "org/testng/" };
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classFileBuffer) {
if (isSystemClass(loader, className) || loadedClass.containsKey(new ComparableWeakRef<String>(className))) {
if (isSystemClass(className)) {
// Ignore system class and reloaded class
LogUtil.verbose("Ignore class: " + (className == null ? "<lambda>" : className));
return null;
}
LogUtil.verbose("Handle class: " + className);
byte[] bytes = null;
try {
if (shouldTransformAsSourceClass(className)) {
// it's a source class with testable enabled
LogUtil.debug("Handling source class %s", className);
LogUtil.diagnose("Handling source class %s", className);
List<MethodInfo> injectMethods = getTestableMockMethods(ClassUtil.getTestClassName(className));
return new SourceClassHandler(injectMethods).getBytes(classFileBuffer);
bytes = new SourceClassHandler(injectMethods).getBytes(classFileBuffer);
resetMockContext();
} else if (shouldTransformAsTestClass(className)) {
// it's a test class with testable enabled
LogUtil.debug("Handling test class %s", className);
return new TestClassHandler().getBytes(classFileBuffer);
LogUtil.diagnose("Handling test class %s", className);
bytes = new TestClassHandler().getBytes(classFileBuffer);
resetMockContext();
}
resetMockContext();
} catch (IOException e) {
return null;
LogUtil.warn("Failed to transform class " + className);
}
return null;
return bytes;
}
private boolean shouldTransformAsSourceClass(String className) {
@@ -66,9 +73,22 @@ public class TestableClassTransformer implements ClassFileTransformer {
return className.endsWith(ConstPool.TEST_POSTFIX) && hasMockAnnotation(className);
}
private boolean isSystemClass(ClassLoader loader, String className) {
private boolean isSystemClass(String className) {
// className can be null for Java 8 lambdas
return !(loader instanceof URLClassLoader) || null == className || className.startsWith("jdk/");
if (null == className) {
return true;
}
for (String prefix : WHITELIST_PREFIXES) {
if (className.startsWith(prefix)) {
return false;
}
}
for (String prefix : BLACKLIST_PREFIXES) {
if (className.startsWith(prefix)) {
return true;
}
}
return false;
}
private List<MethodInfo> getTestableMockMethods(String className) {
@@ -79,7 +99,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
for (MethodNode mn : cn.methods) {
checkMethodAnnotation(cn, methodInfos, mn);
}
LogUtil.debug(" Found %d mock methods", methodInfos.size());
LogUtil.diagnose(" Found %d mock methods", methodInfos.size());
return methodInfos;
} catch (Exception e) {
return new ArrayList<MethodInfo>();
@@ -92,32 +112,40 @@ public class TestableClassTransformer implements ClassFileTransformer {
return;
}
for (AnnotationNode an : mn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_MOCK)) {
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
String fullClassName = toDotSeparateFullClassName(an.desc);
if (fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
addMockConstructor(cn, methodInfos, mn);
} else if (fullClassName.equals(ConstPool.MOCK_METHOD) ||
fullClassName.equals(ConstPool.TESTABLE_MOCK)) {
String targetMethod = AnnotationUtil.getAnnotationParameter(
an, ConstPool.FIELD_TARGET_METHOD, mn.name, String.class);
if (targetMethod.equals(ConstPool.CONSTRUCTOR)) {
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
methodInfos.add(new MethodInfo(sourceClassName, targetMethod, mn.name, mn.desc));
addMockConstructor(cn, methodInfos, mn);
} else {
methodInfos.add(new MethodInfo(targetClass, targetMethod, mn.name, methodDescPair.right));
addMockMethod(methodInfos, mn, methodDescPair, targetMethod);
}
break;
}
}
}
private void addMockMethod(List<MethodInfo> methodInfos, MethodNode mn,
ImmutablePair<String, String> methodDescPair, String targetMethod) {
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
methodInfos.add(new MethodInfo(targetClass, targetMethod, mn.name, methodDescPair.right));
}
private void addMockConstructor(ClassNode cn, List<MethodInfo> methodInfos, MethodNode mn) {
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
methodInfos.add(new MethodInfo(sourceClassName, ConstPool.CONSTRUCTOR, mn.name, mn.desc));
}
/**
* Check whether any method in specified class has specified annotation
* @param className class that need to explore
* @return found annotation or not
*/
private boolean hasMockAnnotation(String className) {
CachedMockParameter cache = loadedClass.get(new ComparableWeakRef<String>(className));
if (cache != null) {
setupMockContext(cache.getMockWith());
return cache.isClassExist();
}
try {
ClassNode cn = new ClassNode();
new ClassReader(className).accept(cn, 0);
@@ -125,7 +153,6 @@ public class TestableClassTransformer implements ClassFileTransformer {
for (AnnotationNode an : cn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.MOCK_WITH)) {
setupMockContext(an);
loadedClass.put(new ComparableWeakRef<String>(className), CachedMockParameter.exist(an));
return true;
}
}
@@ -133,8 +160,10 @@ public class TestableClassTransformer implements ClassFileTransformer {
for (MethodNode mn : cn.methods) {
if (mn.visibleAnnotations != null) {
for (AnnotationNode an : mn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_MOCK)) {
loadedClass.put(new ComparableWeakRef<String>(className), CachedMockParameter.exist());
String fullClassName = toDotSeparateFullClassName(an.desc);
if (fullClassName.equals(ConstPool.MOCK_METHOD) ||
fullClassName.equals(ConstPool.TESTABLE_MOCK) ||
fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
return true;
}
}
@@ -144,21 +173,18 @@ public class TestableClassTransformer implements ClassFileTransformer {
// Usually class not found, return without record
return false;
}
loadedClass.put(new ComparableWeakRef<String>(className), CachedMockParameter.notExist());
return false;
}
private void setupMockContext(AnnotationNode an) {
MockDiagnose mockDebug = AnnotationUtil.getAnnotationParameter(an, FIELD_DIAGNOSE, null, MockDiagnose.class);
if (MockDiagnose.ENABLE.equals(mockDebug)) {
LogUtil.enableDebugLog();
} else if (MockDiagnose.DISABLE.equals(mockDebug)) {
LogUtil.disableDebugLog();
MockDiagnose diagnose = AnnotationUtil.getAnnotationParameter(an, FIELD_DIAGNOSE, null, MockDiagnose.class);
if (diagnose != null) {
LogUtil.enableDiagnose(diagnose == MockDiagnose.ENABLE);
}
}
private void resetMockContext() {
LogUtil.resetDebugLog();
LogUtil.resetLogLevel();
}
/**

View File

@@ -1,14 +1,12 @@
package com.alibaba.testable.agent.util;
import com.alibaba.testable.agent.constant.ConstPool;
import com.alibaba.testable.agent.tool.ComparableWeakRef;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;

View File

@@ -1,35 +0,0 @@
package com.alibaba.testable.agent.util;
/**
* @author flin
*/
public class LogUtil {
private static final int LEVEL_ERROR = 0;
private static final int LEVEL_WARN = 1;
private static final int LEVEL_DIAGNOSE = 2;
private static final ThreadLocal<Integer> LEVEL = new ThreadLocal<Integer>();
public static boolean globalDebugEnable = false;
public static void debug(String msg, Object... args) {
if (LEVEL.get() >= LEVEL_DIAGNOSE) {
System.out.println(String.format("[DIAGNOSE] " + msg, args));
}
}
public static void enableDebugLog() {
LEVEL.remove();
LEVEL.set(LEVEL_DIAGNOSE);
}
public static void disableDebugLog() {
LEVEL.remove();
LEVEL.set(LEVEL_ERROR);
}
public static void resetDebugLog() {
LEVEL.set(globalDebugEnable ? LEVEL_DIAGNOSE : LEVEL_WARN);
}
}

105
testable-all/pom.xml Normal file
View File

@@ -0,0 +1,105 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.4.2</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-all</artifactId>
<packaging>jar</packaging>
<name>testable-all</name>
<dependencies>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-processor</artifactId>
<version>${testable.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-core</artifactId>
<version>${testable.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-agent</artifactId>
<version>${testable.version}</version>
</dependency>
</dependencies>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${plugin.source.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${plugin.javadoc.version}</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>${plugin.gpg.version}</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>${plugin.staging.version}</version>
<extensions>true</extensions>
<configuration>
<serverId>oss</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<snapshotRepository>
<id>oss</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
</snapshotRepository>
<repository>
<id>oss</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
</profile>
</profiles>
</project>

View File

@@ -3,43 +3,18 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.4.2</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-core</artifactId>
<version>0.3.2</version>
<packaging>jar</packaging>
<name>testable-core</name>
<description>Unit test enhancement toolkit</description>
<url>https://github.com/alibaba/testable-mock</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://github.com/alibaba/testable-mock/blob/master/LICENSE</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<name>金戟</name>
<email>jinji.lf@alibaba-inc.com</email>
</developer>
</developers>
<scm>
<connection>scm:git:git@github.com:alibaba/testable-mock.git</connection>
<url>git@github.com:alibaba/testable-mock.git</url>
<tag>HEAD</tag>
</scm>
<properties>
<java.version>1.6</java.version>
<project.compiler.level>1.6</project.compiler.level>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.6.2</junit.version>
<plugin.compiler.version>3.8.1</plugin.compiler.version>
<plugin.surefire.version>3.0.0-M5</plugin.surefire.version>
<plugin.source.version>3.2.0</plugin.source.version>
<plugin.javadoc.version>3.2.0</plugin.javadoc.version>
<plugin.gpg.version>1.6</plugin.gpg.version>
<plugin.staging.version>1.6.8</plugin.staging.version>
</properties>
<dependencies>

View File

@@ -0,0 +1,14 @@
package com.alibaba.testable.core.annotation;
import java.lang.annotation.*;
/**
* Mark method as mock constructor
*
* @author flin
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface MockConstructor {
}

View File

@@ -0,0 +1,21 @@
package com.alibaba.testable.core.annotation;
import java.lang.annotation.*;
/**
* Mark method as mock method
*
* @author flin
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface MockMethod {
/**
* mock specified method instead of method with same name
* @return target method name
*/
String targetMethod() default "";
}

View File

@@ -18,6 +18,6 @@ public @interface MockWith {
* switch of mock diagnose information of current test class
* @return enable or disable
*/
MockDiagnose diagnose() default MockDiagnose.WARN_ONLY;
MockDiagnose diagnose() default MockDiagnose.DISABLE;
}

View File

@@ -3,13 +3,15 @@ package com.alibaba.testable.core.annotation;
import java.lang.annotation.*;
/**
* Use marked method to replace the ones in source class
* Mark method as mock method
* @deprecated will be remove in v0.5.0, use @MockMethod or @MockConstructor instead
*
* @author flin
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Deprecated
public @interface TestableMock {
/**

View File

@@ -28,7 +28,8 @@ public class InvokeVerifier {
public static InvokeVerifier verify(String mockMethodName) {
String testClass = Thread.currentThread().getStackTrace()[InvokeRecordUtil.INDEX_OF_TEST_CLASS].getClassName();
String testCaseName = TestableUtil.currentTestCaseName(testClass);
return new InvokeVerifier(InvokeRecordUtil.getInvokeRecord(mockMethodName, testCaseName));
String recordIdentify = InvokeRecordUtil.getInvokeIdentify(mockMethodName, testClass, testCaseName);
return new InvokeVerifier(InvokeRecordUtil.getInvokeRecord(recordIdentify));
}
/**

View File

@@ -12,12 +12,7 @@ public enum MockDiagnose {
DISABLE,
/**
* Only show warning message
*/
WARN_ONLY,
/**
* Print detail diagnose logs
* Print diagnose logs
*/
ENABLE

View File

@@ -5,11 +5,6 @@ package com.alibaba.testable.core.tool;
*/
public class TestableTool {
/**
* Name of the constructor method
*/
public static final String CONSTRUCTOR = "<init>";
/**
* Name of current test case method
*/

View File

@@ -31,25 +31,36 @@ public class InvokeRecordUtil {
String mockMethodName = mockMethodTraceElement.getMethodName();
String testClass = mockMethodTraceElement.getClassName();
String testCaseName = TestableUtil.currentTestCaseName(testClass);
String key = testCaseName + JOINER + mockMethodName;
List<Object[]> records = getInvokeRecord(mockMethodName, testCaseName);
String identify = getInvokeIdentify(mockMethodName, testClass, testCaseName);
List<Object[]> records = getInvokeRecord(identify);
if (isConstructor) {
records.add(args);
LogUtil.verbose(" Mock constructor invoked \"%s\"", identify);
} else {
records.add(slice(args, 1));
LogUtil.verbose(" Mock method invoked \"%s\"", identify);
}
INVOKE_RECORDS.put(key, records);
INVOKE_RECORDS.put(identify, records);
}
/**
* Get identify key for mock invocation record
* @param mockMethodName mock method name
* @param testClass test class name
* @param testCaseName test case name
* @return identify key
*/
public static String getInvokeIdentify(String mockMethodName, String testClass, String testCaseName) {
return testClass + JOINER + testCaseName + JOINER + mockMethodName;
}
/**
* Get mock method invoke count
* @param mockMethodName mock method name
* @param testCaseName test case name
* @param identify key of invocation record
* @return parameters used when specified method invoked in specified test case
*/
public static List<Object[]> getInvokeRecord(String mockMethodName, String testCaseName) {
String key = testCaseName + JOINER + mockMethodName;
List<Object[]> records = INVOKE_RECORDS.get(key);
public static List<Object[]> getInvokeRecord(String identify) {
List<Object[]> records = INVOKE_RECORDS.get(identify);
return (records == null) ? new LinkedList<Object[]>() : records;
}

View File

@@ -0,0 +1,66 @@
package com.alibaba.testable.core.util;
/**
* @author flin
*/
public class LogUtil {
public enum LogLevel {
/**
* Mute
*/
LEVEL_MUTE(0),
/**
* Warn only
*/
LEVEL_WARN(1),
/**
* Show diagnose messages
*/
LEVEL_DIAGNOSE(2),
/**
* Show detail progress logs
*/
LEVEL_VERBOSE(3);
int level;
LogLevel(int l) {
level = l;
}
}
private static LogLevel defaultLogLevel = LogLevel.LEVEL_WARN;
private static LogLevel currentLogLevel = LogLevel.LEVEL_WARN;
public static void verbose(String msg, Object... args) {
if (currentLogLevel.level >= LogLevel.LEVEL_VERBOSE.level) {
System.out.println(String.format("[VERBOSE] " + msg, args));
}
}
public static void diagnose(String msg, Object... args) {
if (currentLogLevel.level >= LogLevel.LEVEL_DIAGNOSE.level) {
System.out.println(String.format("[DIAGNOSE] " + msg, args));
}
}
public static void warn(String msg, Object... args) {
if (currentLogLevel.level >= LogLevel.LEVEL_WARN.level) {
System.out.println(String.format("[WARN] " + msg, args));
}
}
public static void enableDiagnose(boolean enable) {
currentLogLevel = enable ? LogLevel.LEVEL_DIAGNOSE : LogLevel.LEVEL_MUTE;
}
public static void setDefaultLevel(LogLevel level) {
defaultLogLevel = level;
resetLogLevel();
}
public static void resetLogLevel() {
currentLogLevel = defaultLogLevel;
}
}

View File

@@ -15,6 +15,10 @@ public class TestableUtil {
* [0]Thread.getStackTrace() → [1]previousStackLocation() → [2]Invoker -> [3]Caller of invoker
*/
private static final int INDEX_OF_CALLER_METHOD = 3;
/**
* [0]Thread.getStackTrace() → [1]markTestCaseBegin() → [2]TestCaseMethod
*/
private static final int INDEX_OF_TEST_CASE_METHOD = 2;
/**
* Just a special number to identify test worker thread
*/
@@ -22,10 +26,9 @@ public class TestableUtil {
/**
* Get the last visit method in source file
* @param testClassRef usually `this` variable of the test class
* @return method name
*/
public static String currentSourceMethodName(Object testClassRef) {
public static String currentSourceMethodName() {
return Thread.currentThread().getStackTrace()[INDEX_OF_SOURCE_METHOD].getMethodName();
}
@@ -79,6 +82,14 @@ public class TestableUtil {
return stack.getFileName() + ":" + stack.getLineNumber();
}
/**
* Do prepare when probable test case start
*/
public static void markTestCaseBegin() {
String testCaseName = Thread.currentThread().getStackTrace()[INDEX_OF_TEST_CASE_METHOD].getMethodName();
LogUtil.verbose("Start testcase: " + testCaseName);
}
private static Thread findTestWorkerThread(Set<Thread> threads) {
for (Thread t : threads) {
if (t.getPriority() == TEST_WORKER_THREAD_PRIORITY) {

View File

@@ -3,44 +3,15 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.4.2</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-maven-plugin</artifactId>
<version>0.3.2</version>
<packaging>maven-plugin</packaging>
<name>testable-maven-plugin</name>
<description>Unit test enhancement toolkit</description>
<url>https://github.com/alibaba/testable-mock</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://github.com/alibaba/testable-mock/blob/master/LICENSE</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<name>金戟</name>
<email>jinji.lf@alibaba-inc.com</email>
</developer>
</developers>
<scm>
<connection>scm:git:git@github.com:alibaba/testable-mock.git</connection>
<url>git@github.com:alibaba/testable-mock.git</url>
<tag>HEAD</tag>
</scm>
<properties>
<testable.version>0.3.2</testable.version>
<java.version>1.6</java.version>
<project.compiler.level>1.6</project.compiler.level>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<plugin.compiler.version>3.8.1</plugin.compiler.version>
<plugin.maven.version>3.6.0</plugin.maven.version>
<plugin.source.version>3.2.0</plugin.source.version>
<plugin.javadoc.version>3.2.0</plugin.javadoc.version>
<plugin.gpg.version>1.6</plugin.gpg.version>
<plugin.staging.version>1.6.8</plugin.staging.version>
</properties>
<dependencies>
<dependency>

View File

@@ -24,13 +24,21 @@ public class TestableMojo extends AbstractMojo
/**
* Maven project.
*/
@Parameter(property = "project", readonly = true)
@Parameter(property = "project", required = true, readonly = true)
private MavenProject project;
/**
* Map of plugin artifacts.
*/
@Parameter(property = "plugin.artifactMap", required = true, readonly = true)
private Map<String, Artifact> pluginArtifactMap;
/**
* JavaAgent log level (mute/debug/verbose)
*/
@Parameter
private String logLevel;
/**
* Name of the Testable Agent artifact.
*/
@@ -48,18 +56,33 @@ public class TestableMojo extends AbstractMojo
*/
private static final String ECLIPSE_TEST_PLUGIN = "eclipse-test-plugin";
@Override
public void execute() throws MojoExecutionException
{
final String testArgsPropertyKey = getEffectivePropertyKey();
final Properties projectProperties = project.getProperties();
if (projectProperties == null) {
getLog().error("failed to fetch project properties");
return;
}
String extraArgs = "";
if (logLevel != null && !logLevel.isEmpty()) {
extraArgs += logLevel;
}
final String oldArgs = projectProperties.getProperty(testArgsPropertyKey);
final String newArgs = (oldArgs == null) ? getAgentJarArgs().trim() : oldArgs + getAgentJarArgs();
String newArgs = (oldArgs == null) ? getAgentJarArgs().trim() : (oldArgs + getAgentJarArgs());
getLog().info(testArgsPropertyKey + " set to " + newArgs);
if (!extraArgs.isEmpty()) {
newArgs += ("=" + extraArgs);
}
projectProperties.setProperty(testArgsPropertyKey, newArgs);
}
private String getAgentJarArgs() {
final Artifact testableAgentArtifact = pluginArtifactMap.get(AGENT_ARTIFACT_NAME);
if (testableAgentArtifact == null) {
getLog().error("failed to find testable agent jar");
}
return " -javaagent:" + testableAgentArtifact.getFile().getAbsolutePath();
}

118
testable-parent/pom.xml Normal file
View File

@@ -0,0 +1,118 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.4.2</version>
<packaging>pom</packaging>
<name>testable-parent</name>
<description>Unit test enhancement toolkit</description>
<url>https://github.com/alibaba/testable-mock</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://github.com/alibaba/testable-mock/blob/master/LICENSE</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<name>金戟</name>
<email>jinji.lf@alibaba-inc.com</email>
</developer>
</developers>
<scm>
<connection>scm:git:git@github.com:alibaba/testable-mock.git</connection>
<url>git@github.com:alibaba/testable-mock.git</url>
<tag>HEAD</tag>
</scm>
<properties>
<java.version>1.6</java.version>
<project.compiler.level>1.6</project.compiler.level>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<plugin.compiler.version>3.8.1</plugin.compiler.version>
<plugin.surefire.version>3.0.0-M5</plugin.surefire.version>
<plugin.jar.version>3.2.0</plugin.jar.version>
<plugin.shade.version>3.2.4</plugin.shade.version>
<plugin.source.version>3.2.0</plugin.source.version>
<plugin.javadoc.version>3.2.0</plugin.javadoc.version>
<plugin.gpg.version>1.6</plugin.gpg.version>
<plugin.staging.version>1.6.8</plugin.staging.version>
<plugin.maven.version>3.6.0</plugin.maven.version>
<testable.version>0.4.2</testable.version>
</properties>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${plugin.source.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${plugin.javadoc.version}</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>${plugin.gpg.version}</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>${plugin.staging.version}</version>
<extensions>true</extensions>
<configuration>
<serverId>oss</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<snapshotRepository>
<id>oss</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
</snapshotRepository>
<repository>
<id>oss</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
</profile>
</profiles>
</project>

View File

@@ -3,52 +3,21 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.4.2</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-processor</artifactId>
<version>0.3.2</version>
<packaging>jar</packaging>
<name>testable-processor</name>
<description>Unit test enhancement toolkit</description>
<url>https://github.com/alibaba/testable-mock</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://github.com/alibaba/testable-mock/blob/master/LICENSE</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<name>金戟</name>
<email>jinji.lf@alibaba-inc.com</email>
</developer>
</developers>
<scm>
<connection>scm:git:git@github.com:alibaba/testable-mock.git</connection>
<url>git@github.com:alibaba/testable-mock.git</url>
<tag>HEAD</tag>
</scm>
<properties>
<java.version>1.6</java.version>
<project.compiler.level>1.6</project.compiler.level>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.6.2</junit.version>
<testable.version>0.3.2</testable.version>
<plugin.compiler.version>3.8.1</plugin.compiler.version>
<plugin.surefire.version>3.0.0-M5</plugin.surefire.version>
<plugin.source.version>3.2.0</plugin.source.version>
<plugin.javadoc.version>3.2.0</plugin.javadoc.version>
<plugin.gpg.version>1.6</plugin.gpg.version>
<plugin.staging.version>1.6.8</plugin.staging.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-core</artifactId>
<version>${testable.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>