mirror of
https://github.com/alibaba/testable-mock.git
synced 2026-08-19 17:53:29 +08:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e370114b01 | ||
|
|
26d3178840 | ||
|
|
55ccc385d5 | ||
|
|
20b346ed34 | ||
|
|
00055a4626 | ||
|
|
27174afa93 | ||
|
|
3d6d3c9dd1 | ||
|
|
df7e641a0c | ||
|
|
3acf341d1f | ||
|
|
52878364ee | ||
|
|
9871a96db0 | ||
|
|
9b56e3d64a | ||
|
|
ea8f305152 | ||
|
|
3feba7fdfa | ||
|
|
9064f7a582 | ||
|
|
b062dbc592 | ||
|
|
a623bd0a33 | ||
|
|
a54bb33c09 | ||
|
|
bbc41f2987 | ||
|
|
7b9a0964bc | ||
|
|
a0dae0dfe1 | ||
|
|
f54a6ce022 | ||
|
|
606760d632 | ||
|
|
206fc5486e | ||
|
|
af3af6b272 | ||
|
|
fe7420c99a | ||
|
|
7857782125 | ||
|
|
f776deff0e | ||
|
|
328296ae3a | ||
|
|
a5038320b4 | ||
|
|
bdd16c0948 | ||
|
|
dce1accafb | ||
|
|
2865093fef | ||
|
|
5f69b21532 |
@@ -9,7 +9,7 @@
|
||||
|
||||
阅读[这里](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)
|
||||
<font size="5">**0.6版本已发布**</font>,从`0.5.x`升级到`0.6.x`版本请参考[版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-guide)
|
||||
|
||||
如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将在24小时内回复并处理
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
|
||||
|
||||
- `0.5` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
|
||||
- `0.6` 实现第四项单元测试增强能力"[快速入参构造器](https://alibaba.github.io/testable-mock/#/zh-cn/doc/parameter-constructor)"
|
||||
- `0.6` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
|
||||
- `0.7` 完善Mock功能的防代码重构机制,支持快速Mock指定类型的所有方法
|
||||
- `1.0` 功能稳定,一个崭新的开始
|
||||
|
||||
## 目录结构
|
||||
|
||||
@@ -7,12 +7,12 @@ Write a mock method, add an `@MockMethod` annotation, everything is done.
|
||||
|
||||
Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
|
||||
|
||||
## Loadmap
|
||||
## Roadmap
|
||||
|
||||
`TestableMock` is still under heavy development, the following version plans may be adjusted during the iteration
|
||||
|
||||
- `v0.5` it's the current version, refer to the [issue](https://github.com/alibaba/testable-mock/issues) list for the work in progress
|
||||
- `v0.6` implement the functionality of "parameter constructor"
|
||||
- `v0.6` it's the current version, refer to the [issue](https://github.com/alibaba/testable-mock/issues) list for the work in progress
|
||||
- `v0.7` better anti-refactor support of mocking, allow quick mock all method in specified class
|
||||
- `v1.0` all functions are stable, a brand-new start
|
||||
|
||||
## Directory Structure
|
||||
|
||||
@@ -13,8 +13,8 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.5.2')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
|
||||
@@ -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.5.2</testable.version>
|
||||
<testable.version>0.6.0</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.demo.basic.model.BlackBox;
|
||||
import com.alibaba.demo.basic.model.Box;
|
||||
import com.alibaba.demo.basic.model.Color;
|
||||
import com.alibaba.demo.basic.model.BlackBox;
|
||||
import com.alibaba.demo.basic.model.Box;
|
||||
import com.alibaba.demo.basic.model.Color;
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox;
|
||||
import com.alibaba.demo.basic.model.mock.Box;
|
||||
import com.alibaba.demo.basic.model.mock.Color;
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
|
||||
import com.alibaba.demo.basic.model.BlackBox;
|
||||
import com.alibaba.demo.basic.model.BlackBox;
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.demo.basic.model.BlackBox;
|
||||
import com.alibaba.demo.basic.model.BlackBox;
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.demo.basic.model;
|
||||
package com.alibaba.demo.basic.model.mock;
|
||||
|
||||
public class BlackBox extends Box implements Color {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.demo.basic.model;
|
||||
package com.alibaba.demo.basic.model.mock;
|
||||
|
||||
abstract public class Box {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.demo.basic.model;
|
||||
package com.alibaba.demo.basic.model.mock;
|
||||
|
||||
public interface Color {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.alibaba.demo.basic.model.omni;
|
||||
|
||||
public class Child {
|
||||
|
||||
/**
|
||||
* 我是一个私有的构造方法
|
||||
* This class have only private constructor
|
||||
*/
|
||||
private Child() {
|
||||
}
|
||||
|
||||
// ---------- 内部成员字段 ----------
|
||||
|
||||
private GrandChild grandChild;
|
||||
|
||||
private InnerChild subChild;
|
||||
|
||||
/* ---------------------------------------
|
||||
以下Getters/Setters方法仅为便于功能演示而添加
|
||||
并非OmniConstructor或OmniAccessor功能所需
|
||||
--------------------------------------- */
|
||||
|
||||
public GrandChild getGrandChild() {
|
||||
return grandChild;
|
||||
}
|
||||
|
||||
public void setGrandChild(GrandChild grandChild) {
|
||||
this.grandChild = grandChild;
|
||||
}
|
||||
|
||||
public InnerChild getSubChild() {
|
||||
return subChild;
|
||||
}
|
||||
|
||||
public void setSubChild(InnerChild subChild) {
|
||||
this.subChild = subChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* 这是一个私有内部类
|
||||
* An private inner class
|
||||
*/
|
||||
private class InnerChild {
|
||||
private String secret;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.alibaba.demo.basic.model.omni;
|
||||
|
||||
public class GrandChild {
|
||||
|
||||
// ---------- 内部成员字段 ----------
|
||||
|
||||
private int value;
|
||||
|
||||
private String content;
|
||||
|
||||
/* ---------------------------------------
|
||||
以下Getters/Setters方法仅为便于功能演示而添加
|
||||
并非OmniConstructor或OmniAccessor功能所需
|
||||
--------------------------------------- */
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.alibaba.demo.basic.model.omni;
|
||||
|
||||
public class Parent {
|
||||
|
||||
/**
|
||||
* 我是一个虽然存在但无法正常使用的构造方法
|
||||
* This class have constructor with exception throw
|
||||
*/
|
||||
public Parent() {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
// ---------- 内部成员字段 ----------
|
||||
|
||||
private Child child;
|
||||
|
||||
private Child[] children;
|
||||
|
||||
/* ---------------------------------------
|
||||
以下Getters/Setters方法仅为便于功能演示而添加
|
||||
并非OmniConstructor或OmniAccessor功能所需
|
||||
--------------------------------------- */
|
||||
|
||||
public Child getChild() {
|
||||
return child;
|
||||
}
|
||||
|
||||
public void setChild(Child child) {
|
||||
this.child = child;
|
||||
}
|
||||
|
||||
public Child[] getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public void setChildren(Child[] children) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.demo.basic.model.BlackBox;
|
||||
import com.alibaba.demo.basic.model.Box;
|
||||
import com.alibaba.demo.basic.model.Color;
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox;
|
||||
import com.alibaba.demo.basic.model.mock.Box;
|
||||
import com.alibaba.demo.basic.model.mock.Color;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.demo.basic.DemoInherit;
|
||||
import com.alibaba.demo.basic.model.BlackBox;
|
||||
import com.alibaba.demo.basic.model.Box;
|
||||
import com.alibaba.demo.basic.model.Color;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
@@ -54,42 +50,42 @@ class DemoInheritTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_sub_object_method_by_parent_object() {
|
||||
void should_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() {
|
||||
void should_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() {
|
||||
void should_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() {
|
||||
void should_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() {
|
||||
void should_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() {
|
||||
void should_mock_call_interface_method_by_sub_class_object() {
|
||||
String color = demoInherit.getColorViaBox();
|
||||
verify("get_color_from_blackbox").withTimes(1);
|
||||
assertEquals("color_from_blackbox", color);
|
||||
|
||||
@@ -19,7 +19,7 @@ class DemoInnerClassTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_invoke_inside_inner_class() throws Exception {
|
||||
void should_mock_invoke_inside_inner_class() throws Exception {
|
||||
DemoInnerClass demo = new DemoInnerClass();
|
||||
assertEquals("MockedCall", demo.callInnerDemo());
|
||||
assertEquals("MockedCall", demo.callAnonymousInner());
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.demo.basic.model.BlackBox;
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.core.error.VerifyFailedError;
|
||||
import com.alibaba.demo.basic.DemoMatcher;
|
||||
import com.alibaba.demo.basic.model.BlackBox;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeMatcher.*;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.demo.basic.model.BlackBox;
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox;
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.demo.basic.model.BlackBox;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -73,20 +72,20 @@ class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_new_object() {
|
||||
void should_mock_new_object() {
|
||||
assertEquals("mock_something", demoMock.newFunc());
|
||||
verify("createBlackBox").with("something");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
void should_mock_member_method() throws Exception {
|
||||
assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoMock.outerFunc("hello"));
|
||||
verify("innerFunc").with("hello");
|
||||
verify("staticFunc").with();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_common_method() {
|
||||
void should_mock_common_method() {
|
||||
assertEquals("trim_string__sub_string__false", demoMock.commonFunc());
|
||||
verify("trim").withTimes(1);
|
||||
verify("sub").withTimes(1);
|
||||
@@ -94,13 +93,13 @@ class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_static_method() {
|
||||
void should_mock_static_method() {
|
||||
Assertions.assertEquals("not_secret_box", demoMock.getBox().get());
|
||||
verify("secretBox").withTimes(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_source_method_name() throws Exception {
|
||||
void should_get_source_method_name() throws Exception {
|
||||
// synchronous
|
||||
assertEquals("mock_one_mock_others", demoMock.callerOne() + "_" + demoMock.callerTwo());
|
||||
// asynchronous
|
||||
@@ -110,7 +109,7 @@ class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_set_mock_context() throws Exception {
|
||||
void should_set_mock_context() throws Exception {
|
||||
MOCK_CONTEXT.put("case", "special_case");
|
||||
// synchronous
|
||||
assertEquals("mock_special", demoMock.callerOne());
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.demo.basic.model.omni.Child;
|
||||
import com.alibaba.demo.basic.model.omni.Parent;
|
||||
import com.alibaba.testable.core.tool.OmniAccessor;
|
||||
import com.alibaba.testable.core.tool.OmniConstructor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* 演示快速创建任意对象和使用路径访问成员
|
||||
* Demonstrate quick object construction and access members by path
|
||||
*/
|
||||
class DemoOmniMethodsTest {
|
||||
|
||||
@Test
|
||||
void should_construct_any_class() {
|
||||
Parent parent = OmniConstructor.newInstance(Parent.class);
|
||||
|
||||
// 任意深度的子孙成员对象都不为空
|
||||
assertNotNull(parent.getChild().getGrandChild().getContent());
|
||||
|
||||
// 所有基础类型初始化为默认数值
|
||||
assertEquals(0, parent.getChild().getGrandChild().getValue());
|
||||
assertEquals("", parent.getChild().getGrandChild().getContent());
|
||||
|
||||
// 所有数组类型初始化为空数组
|
||||
assertEquals(0, parent.getChildren().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_get_any_member() {
|
||||
Parent parent = OmniConstructor.newInstance(Parent.class);
|
||||
parent.setChildren(OmniConstructor.newArray(Child.class, 3));
|
||||
parent.getChild().getGrandChild().setContent("from child");
|
||||
parent.getChildren()[0].getGrandChild().setContent("from 1st children");
|
||||
parent.getChildren()[1].getGrandChild().setContent("from 2nd children");
|
||||
parent.getChildren()[2].getGrandChild().setContent("from 3rd children");
|
||||
|
||||
// 使用成员名称快速读取成员对象
|
||||
List<String> contents = OmniAccessor.get(parent, "content");
|
||||
assertEquals(4, contents.size());
|
||||
assertEquals("from child", contents.get(0));
|
||||
assertEquals("from 1st children", contents.get(1));
|
||||
assertEquals("from 2nd children", contents.get(2));
|
||||
assertEquals("from 3rd children", contents.get(3));
|
||||
|
||||
// 使用成员类型快速读取成员对象
|
||||
contents = OmniAccessor.get(parent, "{Child}/{GrandChild}/content");
|
||||
assertEquals(1, contents.size());
|
||||
assertEquals("from child", contents.get(0));
|
||||
|
||||
// 使用带下标的路径读取成员对象
|
||||
assertEquals("from 2nd children", OmniAccessor.getFirst(parent, "children[1]/{GrandChild}/content"));
|
||||
assertEquals("from 3rd children", OmniAccessor.getFirst(parent, "{Child[]}[2]/{GrandChild}/content"));
|
||||
|
||||
// 使用模糊路径快速读取成员对象
|
||||
assertEquals("from 1st children", OmniAccessor.getFirst(parent, "{C*[]}[0]/*/con*t"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_set_any_member() {
|
||||
Parent parent = OmniConstructor.newInstance(Parent.class);
|
||||
parent.setChildren(OmniConstructor.newArray(Child.class, 3));
|
||||
|
||||
// 使用指定路径快速给成员对象赋值
|
||||
OmniAccessor.set(parent, "child/grandChild/content", "demo child");
|
||||
assertEquals("demo child", parent.getChild().getGrandChild().getContent());
|
||||
|
||||
// 使用带下标的路径给成员对象赋值
|
||||
OmniAccessor.set(parent, "children[1]/grandChild/content", "demo children[1]");
|
||||
assertEquals("demo children[1]", parent.getChildren()[1].getGrandChild().getContent());
|
||||
|
||||
// 使用模糊路径批量给成员对象赋值
|
||||
OmniAccessor.set(parent, "child*/*/content", "demo in batch");
|
||||
assertEquals("demo in batch", parent.getChild().getGrandChild().getContent());
|
||||
assertEquals("demo in batch", parent.getChildren()[0].getGrandChild().getContent());
|
||||
assertEquals("demo in batch", parent.getChildren()[1].getGrandChild().getContent());
|
||||
assertEquals("demo in batch", parent.getChildren()[2].getGrandChild().getContent());
|
||||
|
||||
// 读写私有内部类类型的成员(使用类型名引用内部类时,无需带外部类名)
|
||||
assertEquals("", OmniAccessor.getFirst(parent, "subChild/secret"));
|
||||
OmniAccessor.set(parent, "{InnerChild}/secret", "inner-class secret");
|
||||
assertEquals("inner-class secret", OmniAccessor.getFirst(parent, "subChild/secret"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import com.alibaba.demo.basic.DemoPrivateAccess;
|
||||
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -12,64 +10,46 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
* 演示使用`PrivateAccessor`工具类访问私有成员
|
||||
* Demonstrate access private member via `PrivateAccessor` class
|
||||
*/
|
||||
@EnablePrivateAccess
|
||||
class DemoPrivateAccessTest {
|
||||
class DemoPrivateAccessorTest {
|
||||
|
||||
private DemoPrivateAccess demoPrivateAccess = new DemoPrivateAccess();
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_method() {
|
||||
void should_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);
|
||||
|
||||
void should_access_private_field() {
|
||||
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());
|
||||
void should_access_private_static_method() {
|
||||
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);
|
||||
|
||||
void should_access_private_static_field() {
|
||||
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);
|
||||
|
||||
void should_update_final_field() {
|
||||
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));
|
||||
|
||||
void should_use_null_parameter() {
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", null);
|
||||
assertNull(PrivateAccessor.get(demoPrivateAccess, "pi"));
|
||||
assertEquals("null + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", null, 1));
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 演示使用`@EnablePrivateAccess`注解访问私有成员
|
||||
* Demonstrate access private member via `@EnablePrivateAccess` annotation
|
||||
*/
|
||||
@EnablePrivateAccess(srcClass = DemoPrivateAccess.class)
|
||||
class DemoPrivateProcessorTest {
|
||||
|
||||
private DemoPrivateAccess demoPrivateAccess = new DemoPrivateAccess();
|
||||
|
||||
@Test
|
||||
void should_access_private_method() {
|
||||
List<String> list = new ArrayList<String>() {{ add("a"); add("b"); add("c"); }};
|
||||
assertEquals("member", demoPrivateAccess.privateFunc());
|
||||
assertEquals("abc + hello + 1", demoPrivateAccess.privateFuncWithArgs(list, "hello", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_access_private_field() {
|
||||
demoPrivateAccess.count = 2;
|
||||
assertEquals(Integer.valueOf(2), demoPrivateAccess.count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_access_private_static_method() {
|
||||
assertEquals("static", DemoPrivateAccess.privateStaticFunc());
|
||||
assertEquals("hello + 1", DemoPrivateAccess.privateStaticFuncWithArgs("hello", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_access_private_static_field() {
|
||||
DemoPrivateAccess.staticCount = 2;
|
||||
assertEquals(Integer.valueOf(2), DemoPrivateAccess.staticCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_update_final_field() {
|
||||
demoPrivateAccess.pi = 4.13;
|
||||
assertEquals(Double.valueOf(4.13), demoPrivateAccess.pi);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_use_null_parameter() {
|
||||
demoPrivateAccess.pi = null;
|
||||
assertNull(demoPrivateAccess.pi);
|
||||
assertEquals("null + 1", DemoPrivateAccess.privateStaticFuncWithArgs(null, 1));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -72,19 +72,19 @@ class DemoTemplateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_single_template_method() {
|
||||
void should_mock_single_template_method() {
|
||||
String res = demoTemplate.singleTemplateMethod();
|
||||
assertEquals("demo_mock_list", res);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_double_template_method() {
|
||||
void should_mock_double_template_method() {
|
||||
String res = demoTemplate.doubleTemplateMethod();
|
||||
assertEquals("testable_mock_map", res);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_new_template_method() {
|
||||
void should_mock_new_template_method() {
|
||||
Set<?> res = demoTemplate.newTemplateMethod();
|
||||
assertEquals(2, res.size());
|
||||
Iterator<?> iterator = res.stream().iterator();
|
||||
|
||||
@@ -15,7 +15,7 @@ public class OneToMultiSvcTest {
|
||||
private CSvc cSvc = new CSvc();
|
||||
|
||||
@Test
|
||||
public void should_able_to_test_multi_class_together() {
|
||||
public void should_test_multi_class_together() {
|
||||
assertEquals("a_mock", aSvc.demo("test"));
|
||||
assertEquals("b_mock", bSvc.demo("test"));
|
||||
assertEquals("c_mock", cSvc.demo("test"));
|
||||
|
||||
@@ -16,8 +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-all:0.5.2")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.5.2")
|
||||
testImplementation("com.alibaba.testable:testable-all:0.6.0")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.6.0")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
|
||||
@@ -14,7 +14,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.5.2</testable.version>
|
||||
<testable.version>0.6.0</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.demo.basic.model.BlackBox
|
||||
import com.alibaba.demo.basic.model.Box
|
||||
import com.alibaba.demo.basic.model.Color
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox
|
||||
import com.alibaba.demo.basic.model.mock.Box
|
||||
import com.alibaba.demo.basic.model.mock.Color
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.demo.basic.model.BlackBox
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.demo.basic.model.BlackBox
|
||||
import com.alibaba.demo.basic.model.ColorBox
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox
|
||||
import com.alibaba.demo.basic.model.mock.ColorBox
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class DemoPrivateAccess {
|
||||
/**
|
||||
* a private member field
|
||||
*/
|
||||
private var count = 0
|
||||
var count = 0
|
||||
|
||||
/**
|
||||
* a constant field
|
||||
@@ -19,7 +19,7 @@ class DemoPrivateAccess {
|
||||
/**
|
||||
* private member method
|
||||
*/
|
||||
private fun privateFunc(list: List<String>, str: String, i: Int): String {
|
||||
fun privateFunc(list: List<String>, str: String, i: Int): String {
|
||||
return list.reduce { a: String, s: String -> a + s } + " + " + "$str + $i"
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@ class DemoPrivateAccess {
|
||||
/**
|
||||
* a private static field
|
||||
*/
|
||||
private var staticCount = 0
|
||||
var staticCount = 0
|
||||
|
||||
/**
|
||||
* private static method
|
||||
*/
|
||||
private fun privateStaticFunc(str: String, i: Int): String {
|
||||
fun privateStaticFunc(str: String, i: Int): String {
|
||||
return "$str + $i"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.demo.basic.model
|
||||
package com.alibaba.demo.basic.model.mock
|
||||
|
||||
|
||||
class BlackBox(var input: String) : Box(), Color {
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.demo.basic.model
|
||||
package com.alibaba.demo.basic.model.mock
|
||||
|
||||
abstract class Box {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.demo.basic.model
|
||||
package com.alibaba.demo.basic.model.mock
|
||||
|
||||
interface Color {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.alibaba.demo.basic.model.omni
|
||||
|
||||
/**
|
||||
* 我是一个只有私有构造方法的类
|
||||
* This class have only private constructor
|
||||
*/
|
||||
class Child private constructor() {
|
||||
|
||||
// ---------- 内部成员字段 ----------
|
||||
var grandChild: GrandChild? = null
|
||||
var subChild: InnerChild? = null
|
||||
|
||||
/**
|
||||
* 这是一个私有内部类
|
||||
* An private inner class
|
||||
*/
|
||||
inner class InnerChild {
|
||||
private val secret: String? = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.demo.basic.model.omni
|
||||
|
||||
class GrandChild {
|
||||
|
||||
// ---------- 内部成员字段 ----------
|
||||
var value = 0
|
||||
var content: String? = null
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.alibaba.demo.basic.model.omni
|
||||
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
/**
|
||||
* 我是一个虽有构造方法,但无法正常构造的类
|
||||
* This class have constructor with exception throw
|
||||
*/
|
||||
class Parent {
|
||||
|
||||
init {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
// ---------- 内部成员字段 ----------
|
||||
var child: Child? = null
|
||||
var children: Array<Child>? = null
|
||||
|
||||
}
|
||||
@@ -2,9 +2,9 @@ package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
import com.alibaba.demo.basic.model.BlackBox
|
||||
import com.alibaba.demo.basic.model.Box
|
||||
import com.alibaba.demo.basic.model.Color
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox
|
||||
import com.alibaba.demo.basic.model.mock.Box
|
||||
import com.alibaba.demo.basic.model.mock.Color
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -49,42 +49,42 @@ internal class DemoInheritTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_sub_object_method_by_parent_object() {
|
||||
fun should_mock_call_sub_object_method_by_parent_object() {
|
||||
val box = demoInherit.putIntoBox() as BlackBox
|
||||
InvokeVerifier.verify("put_into_box").withTimes(1)
|
||||
Assertions.assertEquals("put_data_into_box", box.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_sub_object_method_by_sub_object() {
|
||||
fun should_mock_call_sub_object_method_by_sub_object() {
|
||||
val box = demoInherit.putIntoBlackBox()
|
||||
InvokeVerifier.verify("put_into_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("put_data_into_blackbox", box.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_parent_object_method_by_parent_object() {
|
||||
fun should_mock_call_parent_object_method_by_parent_object() {
|
||||
val content = demoInherit.fromBox
|
||||
InvokeVerifier.verify("get_from_box").withTimes(1)
|
||||
Assertions.assertEquals("get_from_box", content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_parent_object_method_by_sub_object() {
|
||||
fun should_mock_call_parent_object_method_by_sub_object() {
|
||||
val content = demoInherit.fromBlackBox
|
||||
InvokeVerifier.verify("get_from_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("get_from_blackbox", content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_interface_method_by_interface_object() {
|
||||
fun should_mock_call_interface_method_by_interface_object() {
|
||||
val color = demoInherit.colorViaColor
|
||||
InvokeVerifier.verify("get_color_from_color").withTimes(1)
|
||||
Assertions.assertEquals("color_from_color", color)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_interface_method_by_sub_class_object() {
|
||||
fun should_mock_call_interface_method_by_sub_class_object() {
|
||||
val color = demoInherit.colorViaBox
|
||||
InvokeVerifier.verify("get_color_from_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("color_from_blackbox", color)
|
||||
|
||||
@@ -19,7 +19,7 @@ internal class DemoInnerClassTest {
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun should_able_to_mock_invoke_inside_inner_class() {
|
||||
fun should_mock_invoke_inside_inner_class() {
|
||||
val demo = DemoInnerClass()
|
||||
Assertions.assertEquals("MockedCall", demo.callInnerDemo())
|
||||
Assertions.assertEquals("MockedCall", demo.callAnonymousInner())
|
||||
|
||||
@@ -4,7 +4,7 @@ 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.demo.basic.model.BlackBox
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ 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.MOCK_CONTEXT
|
||||
import com.alibaba.demo.basic.model.BlackBox
|
||||
import com.alibaba.demo.basic.model.ColorBox
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox
|
||||
import com.alibaba.demo.basic.model.mock.ColorBox
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.concurrent.Executors
|
||||
@@ -64,26 +64,26 @@ internal class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_new_object() {
|
||||
fun should_mock_new_object() {
|
||||
assertEquals("mock_something", demoMock.newFunc())
|
||||
verify("createBlackBox").with("something")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_member_method() {
|
||||
fun should_mock_member_method() {
|
||||
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() {
|
||||
// fun should_mock_method_in_companion_object() {
|
||||
// assertEquals("CALL_MOCK_TAIL", DemoMock.callStaticFunc())
|
||||
// verify("staticFunc").with()
|
||||
// }
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_common_method() {
|
||||
fun should_mock_common_method() {
|
||||
assertEquals("trim_string__sub_string__false", demoMock.commonFunc())
|
||||
verify("trim").withTimes(1)
|
||||
verify("sub").withTimes(1)
|
||||
@@ -91,14 +91,14 @@ internal class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_static_method() {
|
||||
fun should_mock_static_method() {
|
||||
assertEquals("White_not_secret_box", demoMock.getBox().get())
|
||||
verify("secretBox").withTimes(1)
|
||||
verify("createBox").withTimes(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_get_source_method_name() {
|
||||
fun should_get_source_method_name() {
|
||||
// synchronous
|
||||
assertEquals("mock_one_mock_others", demoMock.callerOne() + "_" + demoMock.callerTwo())
|
||||
// asynchronous
|
||||
@@ -109,7 +109,7 @@ internal class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_get_test_case_name() {
|
||||
fun should_get_test_case_name() {
|
||||
MOCK_CONTEXT["case"] = "special_case"
|
||||
// synchronous
|
||||
assertEquals("mock_special", demoMock.callerOne())
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.demo.basic.model.omni.Child
|
||||
import com.alibaba.demo.basic.model.omni.Parent
|
||||
import com.alibaba.testable.core.tool.OmniAccessor
|
||||
import com.alibaba.testable.core.tool.OmniConstructor
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* 演示快速创建任意对象和使用路径访问成员
|
||||
* Demonstrate quick object construction and access members by path
|
||||
*/
|
||||
internal class DemoOmniMethodsTest {
|
||||
|
||||
@Test
|
||||
fun should_construct_any_class() {
|
||||
val parent = OmniConstructor.newInstance(Parent::class.java)
|
||||
|
||||
// 任意深度的子孙成员对象都不为空
|
||||
Assertions.assertNotNull(parent.child?.grandChild?.content)
|
||||
|
||||
// 所有基础类型初始化为默认数值
|
||||
Assertions.assertEquals(0, parent.child?.grandChild?.value)
|
||||
Assertions.assertEquals("", parent.child?.grandChild?.content)
|
||||
|
||||
// 所有数组类型初始化为空数组
|
||||
Assertions.assertEquals(0, parent.children?.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_get_any_member() {
|
||||
val parent = OmniConstructor.newInstance(Parent::class.java)
|
||||
parent.children = OmniConstructor.newArray(Child::class.java, 3)
|
||||
parent.child?.grandChild?.content = "from child"
|
||||
parent.children?.get(0)?.grandChild?.content = "from 1st children"
|
||||
parent.children?.get(1)?.grandChild?.content = "from 2nd children"
|
||||
parent.children?.get(2)?.grandChild?.content = "from 3rd children"
|
||||
|
||||
// 使用成员名称快速读取成员对象
|
||||
var contents = OmniAccessor.get<String?>(parent, "content")
|
||||
Assertions.assertEquals(4, contents.size)
|
||||
Assertions.assertEquals("from child", contents[0])
|
||||
Assertions.assertEquals("from 1st children", contents[1])
|
||||
Assertions.assertEquals("from 2nd children", contents[2])
|
||||
Assertions.assertEquals("from 3rd children", contents[3])
|
||||
|
||||
// 使用成员类型快速读取成员对象
|
||||
contents = OmniAccessor.get(parent, "{Child}/{GrandChild}/content")
|
||||
Assertions.assertEquals(1, contents.size)
|
||||
Assertions.assertEquals("from child", contents[0])
|
||||
|
||||
// 使用带下标的路径读取成员对象
|
||||
Assertions.assertEquals("from 2nd children", OmniAccessor.getFirst(parent, "children[1]/{GrandChild}/content"))
|
||||
Assertions.assertEquals("from 3rd children", OmniAccessor.getFirst(parent, "{Child[]}[2]/{GrandChild}/content"))
|
||||
|
||||
// 使用模糊路径快速读取成员对象
|
||||
Assertions.assertEquals("from 1st children", OmniAccessor.getFirst(parent, "{C*[]}[0]/*/con*t"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_set_any_member() {
|
||||
val parent = OmniConstructor.newInstance(Parent::class.java)
|
||||
parent.children = OmniConstructor.newArray(Child::class.java, 3)
|
||||
|
||||
// 使用指定路径快速给成员对象赋值
|
||||
OmniAccessor.set(parent, "child/grandChild/content", "demo child")
|
||||
Assertions.assertEquals("demo child", parent.child?.grandChild?.content)
|
||||
|
||||
// 使用带下标的路径给成员对象赋值
|
||||
OmniAccessor.set(parent, "children[1]/grandChild/content", "demo children[1]")
|
||||
Assertions.assertEquals("demo children[1]", parent.children?.get(1)?.grandChild?.content)
|
||||
|
||||
// 使用模糊路径批量给成员对象赋值
|
||||
OmniAccessor.set(parent, "child*/*/content", "demo in batch")
|
||||
Assertions.assertEquals("demo in batch", parent.child?.grandChild?.content)
|
||||
Assertions.assertEquals("demo in batch", parent.children?.get(0)?.grandChild?.content)
|
||||
Assertions.assertEquals("demo in batch", parent.children?.get(1)?.grandChild?.content)
|
||||
Assertions.assertEquals("demo in batch", parent.children?.get(2)?.grandChild?.content)
|
||||
|
||||
// 读写私有内部类类型的成员(使用类型名引用内部类时,无需带外部类名)
|
||||
Assertions.assertEquals("", OmniAccessor.getFirst(parent, "subChild/secret"))
|
||||
OmniAccessor.set(parent, "{InnerChild}/secret", "inner-class secret")
|
||||
Assertions.assertEquals("inner-class secret", OmniAccessor.getFirst(parent, "subChild/secret"))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -8,37 +8,37 @@ import org.junit.jupiter.api.Test
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
*/
|
||||
internal class DemoPrivateAccessTest {
|
||||
internal class DemoPrivateAccessorTest {
|
||||
|
||||
private val demoPrivateAccess = DemoPrivateAccess()
|
||||
|
||||
@Test
|
||||
fun should_able_to_access_private_method() {
|
||||
fun should_access_private_method() {
|
||||
val list = listOf("a", "b", "c");
|
||||
assertEquals("abc + hello + 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", list, "hello", 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_access_private_field() {
|
||||
fun should_access_private_field() {
|
||||
PrivateAccessor.set(demoPrivateAccess, "count", 3)
|
||||
assertEquals(3, PrivateAccessor.get(demoPrivateAccess, "count"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_access_private_static_method() {
|
||||
fun should_access_private_static_method() {
|
||||
val list = listOf("a", "b", "c");
|
||||
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateStaticFunc", "hello", 1))
|
||||
assertEquals("abc * hello * 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateJvmStaticFunc", list, "hello", 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_access_private_static_field() {
|
||||
fun should_access_private_static_field() {
|
||||
PrivateAccessor.setStatic(DemoPrivateAccess::class.java, "staticCount", 3)
|
||||
assertEquals(3, PrivateAccessor.getStatic(DemoPrivateAccess::class.java, "staticCount"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_update_final_field() {
|
||||
fun should_update_final_field() {
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", 4.13)
|
||||
assertEquals(4.13, demoPrivateAccess.pi)
|
||||
}
|
||||
@@ -40,19 +40,19 @@ internal class DemoTemplateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_single_template_method() {
|
||||
fun should_mock_single_template_method() {
|
||||
val res = demoTemplate.singleTemplateMethod()
|
||||
Assertions.assertEquals("demo_mock_list", res)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_double_template_method() {
|
||||
fun should_mock_double_template_method() {
|
||||
val res = demoTemplate.doubleTemplateMethod()
|
||||
Assertions.assertEquals("testable_mock_map", res)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_new_template_method() {
|
||||
fun should_mock_new_template_method() {
|
||||
val res = demoTemplate.newTemplateMethod()
|
||||
Assertions.assertEquals(2, res.size)
|
||||
val iterator = res.stream().iterator()
|
||||
|
||||
@@ -41,7 +41,7 @@ class PathUtilTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_java_method_invoke_in_kotlin() {
|
||||
fun should_mock_java_method_invoke_in_kotlin() {
|
||||
PathUtil.deleteRecursively(File("/a/b/"))
|
||||
verify("listFiles").withTimes(2)
|
||||
verify("delete").withTimes(4)
|
||||
|
||||
@@ -13,7 +13,7 @@ class OneToMultiSvcTest {
|
||||
private val cSvc = CSvc()
|
||||
|
||||
@Test
|
||||
fun should_able_to_test_multi_class_together() {
|
||||
fun should_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"))
|
||||
|
||||
@@ -23,4 +23,4 @@ The functionality of `TestabledMock` is basically the same as that of `PowerMock
|
||||
|
||||
The main disadvantage of the current `TestableMock` is that the IDE cannot promptly prompt whether the method parameters are matched correctly when writing the mock method. If the mocking effect does not meet expectation, it has to be verified during runtime through the method provided in the [self-help troubleshooting](en-us/doc/troubleshooting.md) document. This feature needs to be provided by extending IDE plugins in the future.
|
||||
|
||||
In addition, because `TestableMock` uses the mock mechanism based on a single method in a unique way, it decouples the mock method definition from the unit test cases. Thus, the mock methods are by default reusable, and the unit test cases become cleaner and purer. On the other hand, the mock methods become fragmented, and life cycle management is relatively difficult. Therefore, it may take some time for developers get used to it.
|
||||
In addition, because `TestableMock` uses the mechanism based on "each source class binding to its own mock class" in a unique way, it decouples the mock method definition from the unit test cases. Thus, the mock methods are by default reusable, and the unit test cases become cleaner and purer. On the other hand, the mock methods become fragmented, and life cycle management is relatively difficult. Therefore, it may take some time for developers get used to it.
|
||||
|
||||
@@ -29,19 +29,25 @@ See the use case of the `DemoTemplateTest` test class in the Java and Kotlin exa
|
||||
|
||||
> Because JVM has a generic erasure mechanism, you can also directly use the `Object` type to replace generic parameters for Java projects, see the commented out "Second solution" example in the Java version of the `DemoTemplateTest` test class.
|
||||
|
||||
#### 5. Why mocking methods in the `String` class in the Kotlin project does not work?
|
||||
#### 5. How to mock invocation inside an inner class?
|
||||
|
||||
Just put mock methods in the mock container class of its outer class, it works for code in all inner classes.
|
||||
|
||||
See the use case of the `DemoInnerClass` test class in the Java and Kotlin examples.
|
||||
|
||||
#### 6. Why mocking methods in the `String` class in the Kotlin project does not work?
|
||||
|
||||
The `String` type in Kotlin language is actually `kotlin.String` instead of `java.lang.String`. However, when this type is built from bytecode, it will be replaced with Java's `java.lang.String` class, so no matter if the mock target is written as `kotlin.String` or `java.lang.String`, it cannot match the original called method.
|
||||
|
||||
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. Can `TestableMock` be used for testing Android projects?
|
||||
#### 7. Can `TestableMock` be used for testing Android projects?
|
||||
|
||||
It can be used in combination with [Roboelectric](https://github.com/robolectric/robolectric) testing framework.
|
||||
|
||||
The `Dalvik` and `ART` virtual machines of the Android system use a bytecode system different from the standard JVM, which will affect the normal functionality of `TestableMock`. The `Roboelectric` framework can run Android unit tests on a standard JVM virtual machine, which is much faster than running unit tests through the Android virtual machine. Recently, most Android App unit tests are written with the `Roboelectric` framework.
|
||||
|
||||
#### 7. Meet "Command Line is too Long. Shorten command line for ..." error when triggering test in IntelliJ IDE?
|
||||
#### 8. 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`.
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Release Note
|
||||
|
||||
## 0.6.0
|
||||
- add `OmniConstructor` and `OmniAccessor` tool for parameter preparation
|
||||
- fix a `ClassFormatError` caused by incorrect `FRAME FULL` operation
|
||||
- remove support for`diagnose` parameter of `@MockWith` annotation
|
||||
|
||||
## 0.5.2
|
||||
- support use`PrivateAccessor` to access private member in parent class
|
||||
- fix a class format error in jvm 1.8+ cause by incorrect bytecode generation
|
||||
|
||||
@@ -5,8 +5,8 @@ Use TestableMock
|
||||
|
||||
- [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
|
||||
- [Quickly construct complicated parameter object](en-us/doc/omni-constructor.md):generate arbitrarily nested object instances, simplify their internal member assignment methods, solve the problem of long initialization codes for method parameters
|
||||
- [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
|
||||
|
||||
@@ -16,7 +16,7 @@ It is recommended to add a `property` field that identifies the TestableMock ver
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.5.2</testable.version>
|
||||
<testable.version>0.6.0</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -63,8 +63,8 @@ Add dependence of `TestableMock` in `build.gradle` file:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.5.2')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@ Self-Help Troubleshooting
|
||||
|
||||
Compared with `Mockito` and other mock tools where developers have to manually inject mock classes, `TestableMock` uses method name and parameter type matching to automatically find invocations that require mock. While this mechanism brings convenience, it may also cause unexpected mock replacement.
|
||||
|
||||
To troubleshoot mock-related issues, just add the `@MockWith` annotation to the test class, and configure the parameter `diagnose` to `MockDiagnose.ENABLE`, so the detailed mock method replacement process will be printed when the test is run.
|
||||
To troubleshoot mock-related issues, just add the `@MockDiagnose` annotation to the mock class, and set the value `LogLevel.ENABLE`, so the detailed mock method replacement process will be printed when the test is run.
|
||||
|
||||
```java
|
||||
@MockWith(diagnose = MockDiagnose.ENABLE)
|
||||
class DemoTest {
|
||||
...
|
||||
@MockDiagnose(LogLevel.ENABLE)
|
||||
public static class Mock {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -38,15 +40,19 @@ The log shows all the mocked invocation and corresponding code line numbers in t
|
||||
- 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 only the first line of `Handling mock class` is output, please check whether the mock class is created at correct place
|
||||
- If `Handling mock class` and `Handling test class` are output, please check whether the test class is in the same package of the class under test, and the name is "<ClassUnderTest>+Test", otherwise `@MockWith` annotation should be used
|
||||
- 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.
|
||||
For situations where expected mocking is not take effect, you could set the diagnosis level to `LogLevel.VERBOSE` for further investigation information.
|
||||
|
||||
```java
|
||||
@MockWith(diagnose = MockDiagnose.VERBOSE)
|
||||
class DemoTest {
|
||||
...
|
||||
@MockDiagnose(LogLevel.VERBOSE)
|
||||
public static class Mock {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -54,12 +60,12 @@ Executing the unit test again will print out the signatures of all mock methods,
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[VERBOSE] Test case "should_able_to_mock_new_object"
|
||||
[VERBOSE] Test case "should_mock_new_object"
|
||||
... ...
|
||||
[VERBOSE] Test case "should_able_to_set_mock_context"
|
||||
[VERBOSE] Test case "should_set_mock_context"
|
||||
[DIAGNOSE] Found 6 test cases
|
||||
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
|
||||
[VERBOSE] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.BlackBox(java.lang.String)"
|
||||
[VERBOSE] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
|
||||
[VERBOSE] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
|
||||
... ...
|
||||
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
|
||||
@@ -68,10 +74,10 @@ Executing the unit test again will print out the signatures of all mock methods,
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[VERBOSE] Line 13, constructing "java.lang.Object()"
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[VERBOSE] Line 19, constructing "com.alibaba.demo.basic.model.BlackBox(java.lang.String)"
|
||||
[VERBOSE] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[VERBOSE] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.BlackBox"
|
||||
[VERBOSE] Line 20, invoking "com.alibaba.demo.basic.model.BlackBox::get() : java.lang.String"
|
||||
[VERBOSE] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
|
||||
[VERBOSE] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[VERBOSE] Line 27, constructing "java.lang.StringBuilder()"
|
||||
[VERBOSE] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
Upgrade To Version 0.5
|
||||
Upgrade Guide
|
||||
---
|
||||
|
||||
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:
|
||||
### Upgrade to v0.6
|
||||
|
||||
In version `0.6`,`TestableMock` provided a new [quick complicated class construction](en-us/doc/omni-constructor.md) capability. Meanwhile, it includes a modification that is incompatible with the `0.5` version: class `PrivateAccessor` have been moved from `com.alibaba.testable.core.accessor` package to `com.alibaba.testable.core.tool` package.
|
||||
|
||||
If you are using `PrivateAccessor` and having import statement like this:
|
||||
|
||||
```java
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
```
|
||||
|
||||
When upgrade `TestableMock` to `0.6` version, please replace it as:
|
||||
|
||||
```java
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor;
|
||||
```
|
||||
|
||||
### Upgrade to v0.5
|
||||
|
||||
The `0.5` 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 {}`.
|
||||
In order to better realize the reuse of Mock methods, the version `0.5` 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:
|
||||
|
||||
@@ -19,7 +37,7 @@ public class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
void should_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
@@ -39,11 +57,11 @@ public class DemoMockTest {
|
||||
} // Add this line
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
void should_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.
|
||||
Finally, upgrade the `TestableMock` dependency in `pom.xml` or `build.gradle` file to the new version.
|
||||
@@ -9,7 +9,7 @@ Compared with the class-granularity mocking practices of existing mock tools, `T
|
||||
|
||||
The detail mock method definition convention is as follows.
|
||||
|
||||
#### 0. Pre-step, prepare the mock class
|
||||
### 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:
|
||||
|
||||
@@ -23,7 +23,7 @@ public class DemoTest {
|
||||
}
|
||||
```
|
||||
|
||||
#### 1. Mock method calls of any class
|
||||
### 1.1 Mock method calls of any class
|
||||
|
||||
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.
|
||||
|
||||
@@ -67,9 +67,9 @@ private String substring(String self, int i, int 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)
|
||||
For complete code examples, see the `should_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
|
||||
### 1.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.
|
||||
|
||||
@@ -87,9 +87,9 @@ private String innerFunc(String 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.
|
||||
For complete code examples, see the `should_mock_member_method()` test case in the `java-demo` and `kotlin-demo` sample projects.
|
||||
|
||||
#### 3. Mock static methods of any class
|
||||
### 1.3 Mock static methods of any class
|
||||
|
||||
Mock for static methods is the same as for any ordinary methods.
|
||||
|
||||
@@ -102,9 +102,9 @@ private BlackBox secretBox() {
|
||||
}
|
||||
```
|
||||
|
||||
For complete code examples, see the `should_able_to_mock_static_method()` test case in the `java-demo` and `kotlin-demo` sample projects.
|
||||
For complete code examples, see the `should_mock_static_method()` test case in the `java-demo` and `kotlin-demo` sample projects.
|
||||
|
||||
#### 4. Mock `new` operation of any type
|
||||
### 1.4 Mock `new` operation of any type
|
||||
|
||||
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.
|
||||
|
||||
@@ -123,9 +123,9 @@ private BlackBox createBlackBox(String text) {
|
||||
|
||||
> You can still use the `@MockMethod` annotation, and configure the `targetMethod` parameter value to `"<init>"`, and the rest is the same as above. The effect is the same as using the `@MockContructor` annotation
|
||||
|
||||
For complete code examples, see the `should_able_to_mock_new_object()` test case in the `java-demo` and `kotlin-demo` sample projects.
|
||||
For complete code examples, see the `should_mock_new_object()` test case in the `java-demo` and `kotlin-demo` sample projects.
|
||||
|
||||
#### 5. Identify different invocation source in mock method
|
||||
### 2. Identify different invocation source in mock method
|
||||
|
||||
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.
|
||||
|
||||
@@ -157,9 +157,9 @@ private Data mockDemo() {
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
For complete code examples, see the `should_get_source_method_name()` and `should_get_test_case_name()` test cases in the `java-demo` and `kotlin-demo` sample projects.
|
||||
|
||||
#### 6. Verify the sequence and parameters of the mock method being invoked
|
||||
### 3. Verify the sequence and parameters of the mock method being invoked
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
- [Use TestableMock](en-us/doc/setup.md)
|
||||
- [Fast Mocking](en-us/doc/use-mock.md)
|
||||
- [Private Accessor](en-us/doc/private-accessor.md)
|
||||
- [Omni Constructor](en-us/doc/omni-constructor.md)
|
||||
- [Test Void Method](en-us/doc/test-void-method.md)
|
||||
- [Parameter constructor](en-us/doc/parameter-constructor.md)
|
||||
|
||||
- Usage Guide
|
||||
- [Verify Mock Invocation](en-us/doc/invoke-matcher.md)
|
||||
@@ -16,7 +16,7 @@
|
||||
- [Testable Maven Plugin](en-us/doc/use-maven-plugin.md)
|
||||
|
||||
- Technical Reference
|
||||
- [Upgrade To Version 0.5](en-us/doc/upgrade-to-v05.md)
|
||||
- [Upgrade Guide](en-us/doc/upgrade-guide.md)
|
||||
- [Available Global Arguments](zh-cn/doc/javaagent-args.md)
|
||||
- [Available Annotations](zh-cn/doc/annotations.md)
|
||||
- [How Mock Works](zh-cn/doc/design-and-mechanism.md)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
TestableMock简介
|
||||
---
|
||||
|
||||
单元测试中的Mock方法,通常是为了绕开那些依赖外部资源或无关功能的方法调用,使得测试重点能够集中在需要验证和保障的代码逻辑上。
|
||||
单元测试中的Mock方法,通常是为了绕开那些依赖外部资源或无关功能的方法调用,使得测试重点能够集中在需要验证和保障的代码逻辑上。某个调用需要被Mock,往往只与其自身特征有关,而与调用的来源无关。
|
||||
|
||||
在定义Mock方法时,开发者真正关心的只有一件事:"<u>这个调用,在测试的时候要换成那个假的Mock方法</u>"。
|
||||
|
||||
然而当下主流的Mock框架在实现Mock功能时,需要开发者操心的事情实在太多:Mock框架如何初始化、与所用的服务框架是否兼容、要被Mock的方法是不是私有的、是不是静态的、被Mock对象是new出来的还是注入的、怎样把被测对象送回被测类里...这些非关键的额外工作极大分散了使用Mock工具应有的乐趣。
|
||||
当下主流的Mock框架在实现Mock功能时,需要开发者操心的事情实在太多:Mock框架如何初始化、与所用的服务框架是否兼容、要被Mock的方法是不是私有的、是不是静态的、被Mock对象是new出来的还是注入的、怎样把被测对象送回被测类里...这些非关键的额外工作极大分散了使用Mock工具应有的乐趣。
|
||||
|
||||
于是,我们开发了`TestableMock`,**一款特立独行的轻量Mock工具**。
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| N/A | Class | 否 | NullType.class | 指定使用的Mock容器类 |
|
||||
| treatAs | ClassType | 否 | ClassType.GuessByName | 指定当前类是测试类或被测类 |
|
||||
| diagnose | LogLevel | 否 | N/A | (**deprecated**)指定Mock诊断日志级别 |
|
||||
|
||||
#### @MockDiagnose
|
||||
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
|
||||
相比之下,`TestabledMock`的功能与`PowerMock`基本平齐,且极易上手,只需掌握`@MockMethod`注解就可以完成绝大多数任务。
|
||||
|
||||
当前`TestableMock`的主要不足在于,编写Mock方法时IDE尚无法即时提示方法参数是否正确匹配。若发现匹配效果不符合预期,需要通过[自助问题排查](zh-cn/doc/troubleshooting.md)文档提供的方法在运行期进行校验。这个功能未来需要通过扩展主流IDE插件来提供。
|
||||
当前`TestableMock`的主要不足在于,编写Mock方法时IDE无法即时提示方法参数是否正确匹配。若发现匹配效果不符合预期,需要通过[自助问题排查](zh-cn/doc/troubleshooting.md)文档提供的方法在运行期进行校验。这个功能理论上能够通过扩展主流IDE插件来补充,但目前暂无相关开发计划,参见[Issue-104](https://github.com/alibaba/testable-mock/issues/104)。
|
||||
|
||||
此外,由于`TestableMock`独辟蹊径的采用基于单个方法的Mock机制,将Mock方法定义与单元测试用例解耦,一方面使得Mock方法具有默认可复用性,单元测试用例也因此变得更干净纯粹,另一方面也导致Mock方法定义变得零散,生命周期管理起来相对困难,对现有开发者的Mock编写习惯会带来一定改变。
|
||||
此外,由于`TestableMock`独辟蹊径的采用“每个业务类拥有一个专属Mock容器类”的思维方式,将Mock方法定义与单元测试用例解耦,一方面使得Mock方法具有默认可复用性,单元测试用例也因此变得更干净纯粹,另一方面也导致Mock方法定义变得零散,生命周期管理起来相对困难,对现有开发者的Mock编写习惯会带来一定改变。
|
||||
|
||||
@@ -33,19 +33,25 @@
|
||||
|
||||
> 由于JVM存在泛型擦除机制,对于Java项目也可以直接使用`Object`类型替代泛型参数,见Java版`DemoTemplateTest`测试类中被注释掉的"第二种写法"示例。
|
||||
|
||||
#### 5. 在Kotlin项目对`String`类中的方法进行Mock不生效?
|
||||
#### 5. 如何Mock在内部类代码里的调用?
|
||||
|
||||
在其所在外部类对应的Mock容器中定义所需的Mock方法即可。
|
||||
|
||||
参见Java和Kotlin示例中`DemoInnerClass`测试类的用例。
|
||||
|
||||
#### 6. 在Kotlin项目对`String`类中的方法进行Mock不生效?
|
||||
|
||||
Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.String`。但在构建生成自字节码的时候又会被替换为Java的`java.lang.String`类,因此无论将Mock目标写为`kotlin.String`或`java.lang.String`均无法正常匹配到原始的被调用方法。
|
||||
|
||||
实际场景中需要对`String`类中的方法进行Mock的场景很少,`TestableMock`暂未对这种情况做特别处理。
|
||||
|
||||
#### 6. `TestableMock`能否用于Android项目的测试?
|
||||
#### 7. `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 ..."错误?
|
||||
#### 8. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
|
||||
|
||||
这个问题是由于系统ClassPath包含太多路径所致,与是否使用`TestableMock`无关。但需要注意的是,IntelliJ提供了两种辅助解决机制:`JAR manifest`和`classpath file`,若测试中使用了`TestableMock`,请选择`JAR manifest`。
|
||||
|
||||
|
||||
111
docs/zh-cn/doc/omni-constructor.md
Normal file
111
docs/zh-cn/doc/omni-constructor.md
Normal file
@@ -0,0 +1,111 @@
|
||||
快速构造复杂的参数对象
|
||||
---
|
||||
|
||||
在单元测试中,测试数据的准备和构造是一件既必须又繁琐的任务,尤其遇到被测函数的入参类型结构复杂、没有合适的构造方法、成员对象使用私有内部类的时候,常规方法往往无处下手。为此`TestableMock`提供了`OmniConstructor`和`OmniAccessor`两个极简的工具类,从此让一切对象构造不再困难。
|
||||
|
||||
### 1. 一行代码构造任何对象
|
||||
|
||||
万能的对象构造器`OmniConstructor`有两个静态方法:
|
||||
|
||||
- `newInstance(任意类型)` ➜ 指定任意类型,返回一个该类型的对象
|
||||
- `newArray(任意类型, 数组大小)` ➜ 指定任意类型,返回一个该类型的数组
|
||||
|
||||
例如:
|
||||
|
||||
```java
|
||||
// 构造一个ComplicatedClass类型的对象
|
||||
ComplicatedClass obj = OmniConstructor.newInstance(ComplicatedClass.class);
|
||||
// 构造一个ComplicatedClass[]类型,容量为5的数组
|
||||
ComplicatedClass[] arr = OmniConstructor.newArray(ComplicatedClass.class, 5);
|
||||
```
|
||||
|
||||
值得一提的是,使用`OmniConstructor`构造出来的并非是一个所有成员值为`null`的简单空对象。该对象的所有成员,以及所有成员的所有子成员,都会在构造时被依次递归赋值。相比直接用`new`构造的对象,使用`OmniConstructor`能够确保对象完全初始化,无需担心测试过程中发生`NullPointerException`问题。
|
||||
|
||||
> **注意**:在`0.6.0`版本中,类型为接口或抽象类的成员字段依然会被初始化为`null`,此问题将在近期版本修复
|
||||
|
||||
```java
|
||||
// 使用构造函数创建对象
|
||||
Parent parent = new Parent();
|
||||
// 内部成员未初始化,直接访问报NullPointerException异常
|
||||
parent.getChild().getGrandChild();
|
||||
|
||||
// 使用OmniConstructor创建对象
|
||||
Parent parent = OmniConstructor.newInstance(Parent.class);
|
||||
// 无需顾虑,安心访问任意子成员
|
||||
parent.getChild().getGrandChild().getContent();
|
||||
```
|
||||
|
||||
除了用于构造方法的入参,`OmniConstructor`也可以用于快速构造Mock方法的返回值,相比将`null`作为Mock方法的返回值,使用完全初始化的对象能够更好保障测试的可靠性。
|
||||
|
||||
详见`java-demo`和`kotlin-demo`示例项目`DemoOmniMethodsTest`测试类中的用例。
|
||||
|
||||
### 2. 一行代码访问任意深度成员
|
||||
|
||||
在单元测试中,有时会遇到一些结构复杂的参数对象,但与特定测试用例有关的仅仅是该对象结构深处的个别几个属性和状态。`OmniAccessor`的灵感来自于`XML`语言中的`xpath`节点选择器,它有`get`、`set`两个主要的静态方法:
|
||||
|
||||
- `get(任意对象, "访问路径")` ➜ 返回根据路径匹配搜索到的所有成员对象
|
||||
- `set(任意对象, "访问路径", 新的值)` ➜ 根据路径匹配为指定位置的对象赋值
|
||||
|
||||
还有一个用于精确路径匹配时直接获取唯一目标对象的`getFirst()`辅助方法,其作用等效于`OmniAccessor.get(...).get(0)`:
|
||||
|
||||
- `getFirst(任意对象, "访问路径")` ➜ 返回根据路径匹配搜索到的第一个成员对象
|
||||
|
||||
只需书写符合规则的访问路径,不论什么类型和深度的成员,都可以一键直达:
|
||||
|
||||
```java
|
||||
// 返回parent对象中,所有符合类型是GrandChild的子对象中叫做content的成员对象
|
||||
OmniAccessor.get(parent, "{GrandChild}/content");
|
||||
// 将parent对象中,符合名称为children的数组第3位的任意子成员的value字段赋值为100
|
||||
OmniAccessor.set(parent, "children[2]/*/value", 100);
|
||||
```
|
||||
|
||||
具体路径规则如下:
|
||||
|
||||
**1. 匹配成员名**
|
||||
|
||||
不带额外修饰的路径名将匹配与之同名的任意成员对象
|
||||
|
||||
- `child`: 匹配任意名字为`child`的子孙成员
|
||||
- `child/grandChild`: 匹配名字为`child`的子孙成员里,名为`grandChild`的子成员
|
||||
|
||||
**2. 匹配成员类型**
|
||||
|
||||
使用花括号匹配类型名称,通常用于批量获取或赋值同类的多个成员对象
|
||||
|
||||
- `{Child}`: 匹配所有类型是`Child`的子孙成员
|
||||
- `{Children[]}`: 匹配所有类型是`Children`数组的子孙成员
|
||||
- `{Child}/{GrandChild}`: 匹配所有类型是`Child`的子孙成员里,所有类型是`GrandChild`子成员
|
||||
|
||||
成员名和类型可以在路径上混用,但暂不支持在同一级路径既指定成员名称又指定类型的写法
|
||||
|
||||
- `child/{GrandChild}`: 匹配名字为`child`的子孙成员里,所有类型是`GrandChild`的子成员
|
||||
- `{Child}/grandChild/content`: 匹配所有类型是`Child`的子孙成员里,名为`grandChild`子成员里的,名为`content`的子成员
|
||||
|
||||
**3. 使用下标访问数组成员**
|
||||
|
||||
使用带数值的方括号表示匹配该位置为数组类型,且取指定下标的对象(不带下标时,当匹配对象为数组类型,默认匹配数组中的所有对象)
|
||||
|
||||
- `children[1]/content`: 匹配名称为`children`的数组类型子孙成员,取其中第`2`个对象中名为`content`的子成员
|
||||
- `parent/children[1]`: 匹配名称为`parent`的子孙成员里,名为`children`的数组类型子成员,取其中第`2`个对象
|
||||
|
||||
**4. 使用通配符**
|
||||
|
||||
通配符可以用于成员名或类型名的匹配
|
||||
|
||||
- `child*`: 匹配名称以`child`开头的所有子孙成员
|
||||
- `{*Child}`: 匹配类型以`Child`结尾的所有子孙成员
|
||||
- `c*ld/{Grand*ld}`: 匹配名称以`c`开头`ld`结尾的子孙成员里,类型以`Grand`开头`ld`结尾的成员
|
||||
- `child/*/content`: 此时`*`将匹配任意成员,即`child`对象任意子成员中,包含的`content`子成员
|
||||
|
||||
详见`java-demo`和`kotlin-demo`示例项目`DemoOmniMethodsTest`测试类中的用例。
|
||||
|
||||
### 3. 特别说明
|
||||
|
||||
> **你真的需要用到`OmniAccessor`吗?**
|
||||
>
|
||||
> `OmniAccessor`具有基于Fail-Fast机制的防代码重构能力,当用户提供的访问路径无法匹配到任何成员时,`OmniAccessor`将立即抛出`NoSuchMemberError`错误,使单元测试提前终止。然而相比常规的成员访问方式,`OmniAccessor`在IDE重构方面的支持依然偏弱。
|
||||
>
|
||||
> 对于复杂对象的内容赋值,大多数情况下,我们更推荐使用[构造者模式](https://developer.aliyun.com/article/705058),或者暴露Getter/Setter方法实现。这些常规手段虽然稍显笨拙(尤其在需要为许多相似的成员批量赋值的时候),但对业务逻辑的封装和重构都更加友好。
|
||||
> 仅当原类型不适合改造,且没有其它可访问目标成员的方法时,`OmniAccessor`才是最后的终极手段。
|
||||
>
|
||||
> 出于相同的原因,我们并不推荐在除单元测试之外的场景使用`OmniAccessor`方式来读写业务类的成员字段(虽然技术上可行)。
|
||||
@@ -1,6 +0,0 @@
|
||||
快速构造复杂入参
|
||||
---
|
||||
|
||||
不论被测方法所需的参数结构多么错综复杂、甚至没有合适的构造方法、甚至需要私有内部类对象... 呼唤TestableMock,马上递给您~
|
||||
|
||||
计划在`0.6`版本中推出。
|
||||
@@ -5,31 +5,9 @@
|
||||
|
||||
此外,在单元测试中时常会需要对被测对象进行特定的成员字段初始化,但有时由于被测类的构造方法限制,使得无法便捷的对这些字段进行赋值。那么,能否在不破坏被测类型封装的情况下,允许单元测试用例内的代码直接访问被测类的私有方法和成员字段呢?`TestableMock`提供了两种简单的解决方案。
|
||||
|
||||
### 方法一:使用`@EnablePrivateAccess`注解
|
||||
### 1.1 使用`PrivateAccessor`工具类
|
||||
|
||||
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
|
||||
|
||||
- 调用**被测类**的私有方法(包括静态方法)
|
||||
- 读取**被测类**的私有字段(包括静态字段)
|
||||
- 修改**被测类**的私有字段(包括静态字段)
|
||||
- 修改**被测类**的常量字段(使用final修饰的字段,包括静态字段)
|
||||
|
||||
访问和修改私有、常量成员时,IDE可能会提示语法有误,但编译器将能够正常运行测试。(使用编译期代码增强,目前仅实现了Java语言的适配)
|
||||
|
||||
效果见`java-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
|
||||
|
||||
> 此功能默认假设测试类与被测类同包,且名称为`被测类+Test`。当不符合此约定时,可在测试类的`@EnablePrivateAccess`注解上使用`srcClass`参数指定实际的被测类。例如:
|
||||
>
|
||||
> ```java
|
||||
> @EnablePrivateAccess(srcClass = DemoServiceImpl.class)
|
||||
> class DemoServiceTest() { ... }
|
||||
> ```
|
||||
|
||||
### 方法二:使用`PrivateAccessor`工具类
|
||||
|
||||
若不希望看到IDE的语法错误提醒,或是在非Java语言的JVM工程(譬如Kotlin语言)里,可以借助`PrivateAccessor`工具类来直接访问私有成员。
|
||||
|
||||
这个类提供了7个静态方法:
|
||||
第一种方法是借助`PrivateAccessor`工具类来直接访问私有成员。这个类提供7个静态方法:
|
||||
|
||||
- `PrivateAccessor.get(任意对象, "私有字段名")` ➜ 读取任意类的私有字段
|
||||
- `PrivateAccessor.set(任意对象, "私有字段名", 新的值)` ➜ 修改任意类的私有字段(或常量字段)
|
||||
@@ -39,12 +17,38 @@
|
||||
- `PrivateAccessor.invokeStatic(任意类型, "私有静态方法名", 调用参数...)` ➜ 调用任意类的**静态**私有方法
|
||||
- `PrivateAccessor.construct(任意类型, 构造方法参数...)` ➜ 调用任意类的私有构造方法
|
||||
|
||||
> 使用`PrivateAccessor`工具类并不需要测试类具有`@EnablePrivateAccess`注解,但加上此注解将开启被测类私有成员的编译期校验功能。
|
||||
详见`java-demo`和`kotlin-demo`示例项目`DemoPrivateAccessorTest`测试类中的用例。
|
||||
|
||||
详见`java-demo`和`kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
|
||||
### 1.2 防代码重构机制
|
||||
|
||||
### 私有成员编译期校验
|
||||
本质上来说,`PrivateAccessor`只是JVM反射机制的“易用型”封装,因此会存在与JVM反射相同的“对代码重构不友好”问题。当被测类中的私有方法名称、参数进行了修改,由于IDE无法自动订正反射访问的代码,往往相关错误要在单元测试运行时才能被发现。
|
||||
|
||||
上述两种私有成员访问功能的本质都是利用了JVM的反射机制,但JVM编译器不会检查反射目标的存在性,当代码重构时,如果对源类型中的私有方法名称、参数进行了修改,就会导致错误要在单元测试运行时才能被发现。为此,`@EnablePrivateAccess`注解的另一项功能是为**被测类**的私有成员访问进行额外的编译期校验。
|
||||
为此,`TestableMock`对`PrivateAccessor`进行了增强,赋予其编译期私有成员校验能力。这项功能默认关闭,需要通过`@EnablePrivateAccess`注解开启。(实际上是通过该注解的`verifyTargetOnCompile`参数控制,由于此参数默认值为`true`,因此只需在被测类上添加该注解即可启用私有成员校验)
|
||||
|
||||
**注意**:当私有成员校验功能开启时,`PrivateAccessor`工具类将只能用于访问**被测类**的私有成员,这会有助于限制将`PrivateAccessor`工具类用于与当前测试无关的“越权”操作。如果确实需要访问其他类的私有成员,可将`@EnablePrivateAccess`注解移除,或将注解的`verifyTargetOnCompile`参数设为`false`,手工关闭校验功能。
|
||||
**注意 1.**:当私有成员校验功能开启时,`PrivateAccessor`工具类将只能用于访问**被测类**的私有成员,从某种角度而言,这也有助于限制将`PrivateAccessor`工具类用于与当前测试无关的“越权”操作。
|
||||
|
||||
**注意 2.**:`TestableMock`默认约定测试类与**被测类**的包路径相同,且名称为`被测类+Test`。若测试类名称不符合此约定时,在使用`@EnablePrivateAccess`注解时,需用`srcClass`参数显式指明实际的**被测类**位置。
|
||||
|
||||
**注意 3**: 此机制目前只针对`Java`语言实现,对于`Kotlin`以及其他JVM方言均无效。
|
||||
|
||||
> 将`DemoPrivateAccessorTest`示例代码稍加修改,添加`@EnablePrivateAccess`注解(注意此时测试类名不符合约定,需加`srcClass`参数):
|
||||
>
|
||||
> ```java
|
||||
> @EnablePrivateAccess(srcClass = DemoPrivateAccess.class) // <- 添加此行
|
||||
> class DemoPrivateAccessorTest() { ... }
|
||||
> ```
|
||||
>
|
||||
> 然后将任意一处通过`PrivateAccessor`访问的目标名称改为实际不存在的成员名,再次编译时即可发现该行有编译错误,提示信息为访问目标不存在。
|
||||
|
||||
### 2.1 直接访问私有成员
|
||||
|
||||
第二种方法,除了借助`PrivateAccessor`工具类以外,凡是使用了`@EnablePrivateAccess`注解的测试类还会被自动赋予以下“特殊能力”:
|
||||
|
||||
- 调用**被测类**的私有方法(包括静态方法)
|
||||
- 读取**被测类**的私有字段(包括静态字段)
|
||||
- 修改**被测类**的私有字段(包括静态字段)
|
||||
- 修改**被测类**的常量字段(使用`final`修饰的字段,包括静态常量字段)
|
||||
|
||||
访问和修改私有、常量成员时,IDE可能会提示语法有误,但编译器将能够正常运行测试。(使用编译期代码增强,目前仅实现了`Java`语言的适配)
|
||||
|
||||
效果见`java-demo`示例项目`DemoPrivateProcessorTest`测试类中的用例。
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Release Note
|
||||
|
||||
## 0.6.0
|
||||
- 增加`OmniConstructor`和`OmniAccessor`,支持快速构建方法入参
|
||||
- 修复一处`FRAME FULL`字节码处理的异常 (issue-117)
|
||||
- 移除`@MockWith`的`diagnose`参数支持
|
||||
|
||||
## 0.5.2
|
||||
- 支持使用`PrivateAccessor`访问父类中的私有成员 (issue-91)
|
||||
- 修复在较高版本JVM下的非法类型错误 (issue-112)
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
- [快速Mock任意调用](zh-cn/doc/use-mock.md):使被测类的任意方法调用快速替换为Mock方法,实现"指哪换哪",解决传统Mock工具使用繁琐的问题
|
||||
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
|
||||
- [快速构造参数对象](zh-cn/doc/omni-constructor.md):生成任意复杂嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题
|
||||
- [辅助测试void方法](zh-cn/doc/test-void-method.md):利用Mock校验器对方法的内部逻辑进行检查,解决无返回值方法难以实施单元测试的问题
|
||||
- [快速构造参数对象](zh-cn/doc/parameter-constructor.md):生成任意多层嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题
|
||||
|
||||
## 在Maven项目中使用
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.5.2</testable.version>
|
||||
<testable.version>0.6.0</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.5.2')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -59,12 +59,12 @@ class DemoTest {
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[VERBOSE] Test case "should_able_to_mock_new_object"
|
||||
[VERBOSE] Test case "should_mock_new_object"
|
||||
... ...
|
||||
[VERBOSE] Test case "should_able_to_set_mock_context"
|
||||
[VERBOSE] Test case "should_set_mock_context"
|
||||
[DIAGNOSE] Found 6 test cases
|
||||
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
|
||||
[VERBOSE] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.BlackBox(java.lang.String)"
|
||||
[VERBOSE] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
|
||||
[VERBOSE] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
|
||||
... ...
|
||||
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
|
||||
@@ -73,10 +73,10 @@ class DemoTest {
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[VERBOSE] Line 13, constructing "java.lang.Object()"
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[VERBOSE] Line 19, constructing "com.alibaba.demo.basic.model.BlackBox(java.lang.String)"
|
||||
[VERBOSE] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[VERBOSE] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.BlackBox"
|
||||
[VERBOSE] Line 20, invoking "com.alibaba.demo.basic.model.BlackBox::get() : java.lang.String"
|
||||
[VERBOSE] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
|
||||
[VERBOSE] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[VERBOSE] Line 27, constructing "java.lang.StringBuilder()"
|
||||
[VERBOSE] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
|
||||
@@ -91,5 +91,3 @@ class DemoTest {
|
||||
- `Mock method "<Mock方法名>" as "<方法签名>"` 在测试类中扫描到的**普通Mock方法**及其签名
|
||||
- `Line XX, constructing "<类型>" as "<方法签名>"` 在被测类中扫描掉的**构造方法调用**及其签名
|
||||
- `Line XX, invoking "<方法名>" as "<方法签名>"` 在被测类中扫描到的**成员方法调用**及其签名
|
||||
|
||||
> 在`0.4.x`版本使用测试类添加`@MockWith`注解的`diagnose`参数来启用诊断信息的方法在`0.5`版本中依然可用,但将在未来版本中移除,请优先使用`@MockDiagnose`注解替代。
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
升级到0.5版本
|
||||
版本升级说明
|
||||
---
|
||||
|
||||
经过近一个月的设计和开发,TestableMock的`0.5`版本终于和大家见面了。相比`0.4`版本,新版解决了此前遗留的三大历史问题:
|
||||
### 升级到0.6.x版本
|
||||
|
||||
在`0.6`版本中,`TestableMock`提供了[快速构造复杂参数对象](zh-cn/doc/omni-constructor.md)的能力,同时包含一处与`0.5`版本不兼容的修改,`PrivateAccessor`类型的包路径从`com.alibaba.testable.core.accessor`移到了`com.alibaba.testable.core.tool`。
|
||||
|
||||
例如在此前的测试代码中若使用了如下类型引用语句:
|
||||
|
||||
```java
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
```
|
||||
|
||||
当升级`TestableMock`为`0.6`版本时,请将其修改为:
|
||||
|
||||
```java
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor;
|
||||
```
|
||||
|
||||
### 升级到0.5.x版本
|
||||
|
||||
在`0.5`版本中,`TestableMock`解决了此前遗留的三大历史问题:
|
||||
|
||||
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 { }`包裹起来。
|
||||
在使用方式上,`0.5`版本延续了简洁轻量原则,同时为了更好的实现Mock方法复用,新版本的Mock类与测试类之间有了明确的边界。从`0.4`版本升级到`0.5`时,唯一需要的改变是将测试类中的所有Mock方法使用一个`public static class Mock { }`包裹起来。
|
||||
|
||||
例如,原先有如下测试类定义:
|
||||
|
||||
@@ -19,7 +37,7 @@ public class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
void should_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
@@ -39,11 +57,11 @@ public class DemoMockTest {
|
||||
} // 增加此行
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
void should_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后将`pom.xml`或`build.gradle`文件中的TestableMock依赖升级到`0.5.0`或以上版本即可。
|
||||
然后将`pom.xml`或`build.gradle`文件中的`TestableMock`依赖升级到对应的新版本即可。
|
||||
@@ -41,4 +41,4 @@
|
||||
|
||||
最后点击运行单元测试,如图中位置③
|
||||
|
||||
总体来说,使用`testable-maven-plugin`带来的额外复杂性依然高于其对配置的简化作用。目前而言,直接在`pom.xml`文件中修改`maven-surefire-plugin`插件配置还是相对推荐的实用方案。
|
||||
总体来说,由于当下的IDE支持问题,使用`testable-maven-plugin`带来的额外复杂性依然高于其对配置的简化作用。目前直接在`pom.xml`文件中修改`maven-surefire-plugin`插件配置还是相对推荐的实用方案。
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
相比以往Mock工具以类为粒度的Mock方式,`TestableMock`允许用户直接定义需要Mock的单个方法,并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测方法中的指定方法调用。
|
||||
|
||||
> 归纳起来就两条:
|
||||
> 规则归纳起来就两条:
|
||||
> - Mock非构造方法,拷贝原方法定义到Mock容器类,加`@MockMethod`注解
|
||||
> - Mock构造方法,拷贝原方法定义到Mock容器类,返回值换成构造的类型,方法名随意,加`@MockContructor`注解
|
||||
|
||||
具体的Mock方法定义约定如下。
|
||||
|
||||
#### 0. 前置步骤,准备Mock容器
|
||||
### 0. 前置步骤,准备Mock容器
|
||||
|
||||
首先为测试类添加一个关联的Mock类型,作为承载其Mock方法的容器,最简单的做法是在测试类里添加一个名称为`Mock`的静态内部类。例如:
|
||||
|
||||
@@ -23,7 +23,7 @@ public class DemoTest {
|
||||
}
|
||||
```
|
||||
|
||||
#### 1. 覆写任意类的方法调用
|
||||
### 1.1 覆写任意类的方法调用
|
||||
|
||||
在Mock容器类中定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,并在注解的`targetClass`参数指定该方法原本所属对象类型。
|
||||
|
||||
@@ -67,9 +67,9 @@ private String substring(String self, int i, int j) {
|
||||
}
|
||||
```
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_common_method()`测试用例。(由于Kotlin对String类型进行了魔改,故Kotlin示例中将被测方法在`BlackBox`类里加了一层封装)
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_mock_common_method()`测试用例。(由于Kotlin对String类型进行了魔改,故Kotlin示例中将被测方法在`BlackBox`类里加了一层封装)
|
||||
|
||||
#### 2. 覆写被测类自身的成员方法
|
||||
### 1.2 覆写被测类自身的成员方法
|
||||
|
||||
有时候,在对某些方法进行测试时,希望将被测类自身的另外一些成员方法Mock掉(比如这个方法里有许多外部依赖或耗时操作)。
|
||||
|
||||
@@ -87,9 +87,9 @@ private String innerFunc(String text) {
|
||||
|
||||
同样的,上述示例中的方法如需访问发起调用的原始被测对象,也可不使用`targetClass`参数,而是在定义Mock方法时,在方法参数列表首位加一个类型为`DemoMock`的参数(名字随意)。
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_member_method()`测试用例。
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_mock_member_method()`测试用例。
|
||||
|
||||
#### 3. 覆写任意类的静态方法
|
||||
### 1.3 覆写任意类的静态方法
|
||||
|
||||
对于静态方法的Mock与普通方法相同。
|
||||
|
||||
@@ -104,9 +104,9 @@ private BlackBox secretBox() {
|
||||
|
||||
对于静态方法的Mock,通常不使用方法参数列表的首位加参数来表示目标类型。但这种方法也依然适用,只是实际传入的第一个参数值将始终是`null`。
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_static_method()`测试用例。
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_mock_static_method()`测试用例。
|
||||
|
||||
#### 4. 覆写任意类的new操作
|
||||
### 1.4 覆写任意类的new操作
|
||||
|
||||
在Mock容器类里定义一个返回值类型为要被创建的对象类型,且方法参数与要Mock的构造函数参数完全一致的方法,名称随意,然后加上`@MockContructor`注解。
|
||||
|
||||
@@ -123,9 +123,9 @@ private BlackBox createBlackBox(String text) {
|
||||
}
|
||||
```
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_new_object()`测试用例。
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_mock_new_object()`测试用例。
|
||||
|
||||
#### 5. 在Mock方法中区分调用来源
|
||||
### 2. 在Mock方法中区分调用来源
|
||||
|
||||
在Mock方法中通过`TestableTool.SOURCE_METHOD`变量可以识别**进入该Mock方法前的被测类方法名称**;此外,还可以借助`TestableTool.MOCK_CONTEXT`变量为Mock方法注入“**额外的上下文参数**”,从而区分处理不同的调用场景。
|
||||
|
||||
@@ -157,15 +157,16 @@ private Data mockDemo() {
|
||||
}
|
||||
```
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_get_source_method_name()`和`should_able_to_get_test_case_name()`测试用例。
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_get_source_method_name()`和`should_get_test_case_name()`测试用例。
|
||||
|
||||
#### 6. 验证Mock方法被调用的顺序和参数
|
||||
### 3. 验证Mock方法被调用的顺序和参数
|
||||
|
||||
在测试用例中可用通过`TestableTool.verify()`方法,配合`with()`、`withInOrder()`、`without()`、`withTimes()`等方法实现对Mock调用情况的验证。
|
||||
|
||||
详见[校验Mock调用](zh-cn/doc/matcher.md)文档。
|
||||
|
||||
#### 特别说明
|
||||
|
||||
### 4. 特别说明
|
||||
|
||||
> **Mock只对被测类的代码有效**
|
||||
>
|
||||
@@ -173,7 +174,7 @@ private Data mockDemo() {
|
||||
>
|
||||
> 除去这种情况,若Mock未生效,请参考[自助问题排查](zh-cn/doc/troubleshooting.md)提供的方法对比<u>Mock方法签名</u>和<u>目标位置的调用方法签名</u>。若依然无法定位原因,欢迎提交Issues告诉我们。
|
||||
|
||||
> **测试类和Mock容器的命名约定**:
|
||||
> **测试类和Mock容器的命名约定**
|
||||
>
|
||||
> 默认情况下,`TestableMock`假设测试类与被测类的<u>包路径相同,且名称为`被测类名+Test`</u>(通常采用`Maven`或`Gradle`构建的Java项目均符合这种惯例)。
|
||||
> 同时约定测试类关联的Mock容器为<u>在其内部且名为`Mock`的静态类</u>,或<u>相同包路径下名为`被测类名+Mock`的独立类</u>。
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
- [使用TestableMock](zh-cn/doc/setup.md)
|
||||
- [快速Mock任意方法](zh-cn/doc/use-mock.md)
|
||||
- [直接访问私有成员](zh-cn/doc/private-accessor.md)
|
||||
- [快速构造复杂入参](zh-cn/doc/omni-constructor.md)
|
||||
- [测试无返回值的方法](zh-cn/doc/test-void-method.md)
|
||||
- [快速构造复杂入参](zh-cn/doc/parameter-constructor.md)
|
||||
|
||||
- 使用指南
|
||||
- [校验Mock调用](zh-cn/doc/invoke-matcher.md)
|
||||
@@ -16,7 +16,7 @@
|
||||
- [使用Maven插件](zh-cn/doc/use-maven-plugin.md)
|
||||
|
||||
- 技术参考
|
||||
- [升级到0.5版本](zh-cn/doc/upgrade-to-v05.md)
|
||||
- [版本升级说明](zh-cn/doc/upgrade-guide.md)
|
||||
- [全局运行参数](zh-cn/doc/javaagent-args.md)
|
||||
- [注解参数清单](zh-cn/doc/annotations.md)
|
||||
- [Mock的设计和原理](zh-cn/doc/design-and-mechanism.md)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.5.2</version>
|
||||
<version>0.6.0</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
|
||||
@@ -5,16 +5,12 @@ package com.alibaba.testable.agent.constant;
|
||||
*/
|
||||
public class ConstPool {
|
||||
|
||||
public static final String DOT = ".";
|
||||
public static final String SLASH = "/";
|
||||
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 DUMP_TO = "com.alibaba.testable.core.annotation.DumpTo";
|
||||
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";
|
||||
|
||||
@@ -10,13 +10,10 @@ import org.objectweb.asm.tree.ClassNode;
|
||||
*/
|
||||
abstract public class BaseClassHandler implements Opcodes {
|
||||
|
||||
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 String mockClassName;
|
||||
|
||||
public byte[] getBytes(byte[] classFileBuffer) {
|
||||
ClassReader cr = new ClassReader(classFileBuffer);
|
||||
ClassNode cn = new ClassNode();
|
||||
|
||||
@@ -3,10 +3,7 @@ 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.agent.util.*;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.Type;
|
||||
@@ -33,6 +30,10 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
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";
|
||||
private static final String SELF_REF = "__self";
|
||||
private static final String TESTABLE_REF = "__testable";
|
||||
|
||||
private final String mockClassName;
|
||||
|
||||
public MockClassHandler(String className) {
|
||||
this.mockClassName = className;
|
||||
@@ -43,9 +44,7 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
injectRefFieldAndGetInstanceMethod(cn);
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (isMockMethod(mn)) {
|
||||
mn.access &= ~ACC_PRIVATE;
|
||||
mn.access &= ~ACC_PROTECTED;
|
||||
mn.access |= ACC_PUBLIC;
|
||||
mn.access = BytecodeUtil.toPublicAccess(mn.access);
|
||||
// firstly, unfold target class from annotation to parameter
|
||||
unfoldTargetClass(mn);
|
||||
// secondly, add invoke recorder at the beginning of mock method
|
||||
@@ -103,7 +102,7 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
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,
|
||||
mn.localVariables.add(parameterOffset, new LocalVariableNode(SELF_REF, targetClassName, null,
|
||||
labels.left, labels.right, parameterOffset));
|
||||
for (int i = parameterOffset + 1; i < mn.localVariables.size(); i++) {
|
||||
mn.localVariables.get(i).index++;
|
||||
@@ -114,7 +113,7 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
} 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);
|
||||
((FrameNode)in).local.add(parameterOffset, ClassUtil.toSlashSeparateJavaStyleName(targetClassName));
|
||||
}
|
||||
}
|
||||
mn.maxLocals++;
|
||||
@@ -161,7 +160,7 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
il.add(new JumpInsnNode(IFNE, firstLine));
|
||||
il.add(invokeOriginalMethod(mn));
|
||||
il.add(firstLine);
|
||||
il.add( new FrameNode(F_SAME, 0, null, 0, null));
|
||||
il.add(new FrameNode(F_SAME, 0, null, 0, null));
|
||||
mn.instructions.insertBefore(mn.instructions.getFirst(), il);
|
||||
}
|
||||
|
||||
@@ -177,7 +176,7 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
if (VOID_RES.equals(returnType)) {
|
||||
il.add(new InsnNode(POP));
|
||||
il.add(new InsnNode(RETURN));
|
||||
} else if (returnType.charAt(0) == TYPE_ARRAY ||returnType.charAt(0) == TYPE_CLASS) {
|
||||
} else if (returnType.charAt(0) == TYPE_ARRAY || returnType.charAt(0) == TYPE_CLASS) {
|
||||
il.add(new TypeInsnNode(CHECKCAST, ClassUtil.toSlashSeparateJavaStyleName(returnType)));
|
||||
il.add(new InsnNode(ARETURN));
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.alibaba.testable.agent.handler;
|
||||
|
||||
import com.alibaba.testable.agent.handler.test.JUnit4Framework;
|
||||
import com.alibaba.testable.agent.handler.test.JUnit5Framework;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.agent.util.CollectionUtil;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.CLASS_OBJECT;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.CONSTRUCTOR;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.THIS_REF;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class OmniClassHandler extends BaseClassHandler {
|
||||
|
||||
private static final String TESTABLE_NULL_TYPE = "com.alibaba.testable.core.model.TestableNull";
|
||||
private static final String NULL_TYPE = "javax.lang.model.type.NullType";
|
||||
private static final String IGNORE = "ignore";
|
||||
private static final String METHOD_START = "(";
|
||||
private static final String VOID_METHOD_END = ")V";
|
||||
private static final String VOID_METHOD = "()V";
|
||||
|
||||
private static final String[] JUNIT_TEST_ANNOTATIONS = new String[] {
|
||||
JUnit4Framework.ANNOTATION_TEST, JUnit5Framework.ANNOTATION_TEST, JUnit5Framework.ANNOTATION_PARAMETERIZED_TEST
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
if ((cn.access & ACC_INTERFACE) != 0 || cn.superName == null) {
|
||||
return;
|
||||
}
|
||||
if (cn.superName.equals(CLASS_OBJECT)) {
|
||||
// JUnit require test class contains only one constructor
|
||||
if (isJUnitTestClass(cn)) {
|
||||
return;
|
||||
}
|
||||
MethodNode constructor = new MethodNode(ACC_PUBLIC, CONSTRUCTOR,
|
||||
METHOD_START + ClassUtil.toByteCodeClassName(NULL_TYPE) + VOID_METHOD_END, null, null);
|
||||
LabelNode start = new LabelNode(new Label());
|
||||
LabelNode end = new LabelNode(new Label());
|
||||
constructor.instructions = invokeSuperWithoutParameter(start, end);
|
||||
constructor.localVariables = createLocalVariables(cn, start, end);
|
||||
constructor.maxStack = 1;
|
||||
constructor.maxLocals = 2;
|
||||
cn.methods.add(constructor);
|
||||
} else {
|
||||
MethodNode constructor = new MethodNode(ACC_PUBLIC, CONSTRUCTOR,
|
||||
METHOD_START + ClassUtil.toByteCodeClassName(NULL_TYPE) + VOID_METHOD_END, null, null);
|
||||
LabelNode start = new LabelNode(new Label());
|
||||
LabelNode end = new LabelNode(new Label());
|
||||
constructor.instructions = invokeSuperWithTestableNullParameter(cn, start, end);
|
||||
constructor.localVariables = createLocalVariables(cn, start, end);
|
||||
constructor.maxStack = 3;
|
||||
constructor.maxLocals = 2;
|
||||
cn.methods.add(constructor);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isJUnitTestClass(ClassNode cn) {
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (mn.visibleAnnotations == null) {
|
||||
continue;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
for (String annotation : JUNIT_TEST_ANNOTATIONS) {
|
||||
if (an.desc.equals(annotation)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private InsnList invokeSuperWithoutParameter(LabelNode start, LabelNode end) {
|
||||
InsnList il = new InsnList();
|
||||
il.add(start);
|
||||
il.add(new VarInsnNode(ALOAD, 0));
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, CLASS_OBJECT, CONSTRUCTOR, VOID_METHOD, false));
|
||||
il.add(new InsnNode(RETURN));
|
||||
il.add(end);
|
||||
return il;
|
||||
}
|
||||
|
||||
private InsnList invokeSuperWithTestableNullParameter(ClassNode cn, LabelNode start, LabelNode end) {
|
||||
InsnList il = new InsnList();
|
||||
il.add(start);
|
||||
il.add(new VarInsnNode(ALOAD, 0));
|
||||
il.add(new TypeInsnNode(NEW, ClassUtil.toSlashSeparatedName(TESTABLE_NULL_TYPE)));
|
||||
il.add(new InsnNode(DUP));
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, ClassUtil.toSlashSeparatedName(TESTABLE_NULL_TYPE), CONSTRUCTOR,
|
||||
VOID_METHOD, false));
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, cn.superName, CONSTRUCTOR,
|
||||
METHOD_START + ClassUtil.toByteCodeClassName(NULL_TYPE) + VOID_METHOD_END, false));
|
||||
il.add(new InsnNode(RETURN));
|
||||
il.add(end);
|
||||
return il;
|
||||
}
|
||||
|
||||
private List<LocalVariableNode> createLocalVariables(ClassNode cn, LabelNode start, LabelNode end) {
|
||||
return CollectionUtil.listOf(
|
||||
new LocalVariableNode(THIS_REF, ClassUtil.toByteCodeClassName(cn.name), null, start, end, 0),
|
||||
new LocalVariableNode(IGNORE, ClassUtil.toByteCodeClassName(NULL_TYPE), null, start, end, 1)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import static com.alibaba.testable.core.constant.ConstPool.CONSTRUCTOR;
|
||||
*/
|
||||
public class SourceClassHandler extends BaseClassHandler {
|
||||
|
||||
private final String mockClassName;
|
||||
private final List<MethodInfo> injectMethods;
|
||||
private final Set<Integer> invokeOps = new HashSet<Integer>() {{
|
||||
add(Opcodes.INVOKEVIRTUAL);
|
||||
|
||||
@@ -11,6 +11,8 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.alibaba.testable.core.constant.ConstPool.THIS_REF;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
@@ -20,7 +22,6 @@ public class TestClassHandler extends BaseClassWithContextHandler {
|
||||
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;
|
||||
|
||||
@@ -31,10 +32,6 @@ public class TestClassHandler extends BaseClassWithContextHandler {
|
||||
new TestNgOnClassFramework()
|
||||
};
|
||||
|
||||
public TestClassHandler(String mockClassName) {
|
||||
this.mockClassName = mockClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bytecode of test class
|
||||
* @param cn original class node
|
||||
@@ -89,8 +86,8 @@ public class TestClassHandler extends BaseClassWithContextHandler {
|
||||
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.localVariables = Collections.singletonList(new LocalVariableNode(THIS_REF,
|
||||
ClassUtil.toByteCodeClassName(cn.name), null, startLabel, endLabel, 0));
|
||||
afterTestMethod.maxLocals = 1;
|
||||
afterTestMethod.maxStack = 0;
|
||||
cn.methods.add(afterTestMethod);
|
||||
|
||||
@@ -5,7 +5,7 @@ import java.util.List;
|
||||
|
||||
public class JUnit4Framework extends Framework {
|
||||
|
||||
private static final String ANNOTATION_TEST = "Lorg/junit/Test;";
|
||||
public static final String ANNOTATION_TEST = "Lorg/junit/Test;";
|
||||
private static final String ANNOTATION_AFTER_TEST = "Lorg/junit/After;";
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,8 +5,8 @@ import java.util.List;
|
||||
|
||||
public class JUnit5Framework extends Framework {
|
||||
|
||||
private static final String ANNOTATION_TEST = "Lorg/junit/jupiter/api/Test;";
|
||||
private static final String ANNOTATION_PARAMETERIZED_TEST = "Lorg/junit/jupiter/params/ParameterizedTest;";
|
||||
public static final String ANNOTATION_TEST = "Lorg/junit/jupiter/api/Test;";
|
||||
public static final String ANNOTATION_PARAMETERIZED_TEST = "Lorg/junit/jupiter/params/ParameterizedTest;";
|
||||
private static final String ANNOTATION_AFTER_TEST = "Lorg/junit/jupiter/api/AfterEach;";
|
||||
|
||||
@Override
|
||||
|
||||
@@ -45,11 +45,10 @@ public class MockClassParser {
|
||||
/**
|
||||
* Check whether any method in specified class has mock-related annotation
|
||||
*
|
||||
* @param className class that need to explore
|
||||
* @param cn class that need to explore
|
||||
* @return found annotation or not
|
||||
*/
|
||||
public boolean isMockClass(String className) {
|
||||
ClassNode cn = ClassUtil.getClassNode(className);
|
||||
public boolean isMockClass(ClassNode cn) {
|
||||
if (cn == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.alibaba.testable.agent.transformer;
|
||||
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.handler.MockClassHandler;
|
||||
import com.alibaba.testable.agent.handler.OmniClassHandler;
|
||||
import com.alibaba.testable.agent.handler.SourceClassHandler;
|
||||
import com.alibaba.testable.agent.handler.TestClassHandler;
|
||||
import com.alibaba.testable.agent.model.MethodInfo;
|
||||
@@ -15,18 +16,17 @@ import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.InnerClassNode;
|
||||
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.*;
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.KOTLIN_POSTFIX_COMPANION;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.*;
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toJavaStyleClassName;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.TEST_POSTFIX;
|
||||
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
|
||||
import static org.objectweb.asm.Opcodes.ACC_STATIC;
|
||||
import static org.objectweb.asm.Opcodes.*;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
@@ -35,14 +35,14 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
|
||||
private static final String FIELD_VALUE = "value";
|
||||
private static final String FIELD_TREAT_AS = "treatAs";
|
||||
private static final String FIELD_PATH = "path";
|
||||
private static final String COMMA = ",";
|
||||
private static final String CLASS_NAME_MOCK = "Mock";
|
||||
|
||||
/**
|
||||
* Just avoid spend time to scan those surely non-user classes Should keep these lists as tiny as possible
|
||||
* Just avoid spend time to scan those surely non-user classes, should keep these lists as tiny as possible
|
||||
*/
|
||||
private final String[] BLACKLIST_PREFIXES = new String[] {"jdk/", "java/", "javax/", "sun/", "com/sun/",
|
||||
"org/apache/maven/", "com/alibaba/testable/", "junit/", "org/junit/", "org/testng/"};
|
||||
private final String[] BLACKLIST_PREFIXES = new String[] { "sun/", "com/sun/" };
|
||||
|
||||
public MockClassParser mockClassParser = new MockClassParser();
|
||||
|
||||
@@ -54,48 +54,57 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
return null;
|
||||
}
|
||||
LogUtil.verbose("Handle class: " + className);
|
||||
byte[] bytes = null;
|
||||
byte[] bytes = new OmniClassHandler().getBytes(classFileBuffer);
|
||||
ClassNode cn = ClassUtil.getClassNode(className);
|
||||
if (cn != null) {
|
||||
return transformMock(bytes, cn);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private byte[] transformMock(byte[] bytes, ClassNode cn) {
|
||||
String className = cn.name;
|
||||
try {
|
||||
if (mockClassParser.isMockClass(className)) {
|
||||
if (mockClassParser.isMockClass(cn)) {
|
||||
// it's a mock class
|
||||
LogUtil.diagnose("Handling mock class %s", className);
|
||||
bytes = new MockClassHandler(className).getBytes(classFileBuffer);
|
||||
dumpByte(className, bytes);
|
||||
bytes = new MockClassHandler(className).getBytes(bytes);
|
||||
dumpByte(className, GlobalConfig.getDumpPath(), bytes);
|
||||
} else {
|
||||
String mockClass = foundMockForTestClass(className);
|
||||
if (mockClass != null) {
|
||||
// it's a test class with testable enabled
|
||||
LogUtil.diagnose("Handling test class %s", className);
|
||||
bytes = new TestClassHandler(mockClass).getBytes(classFileBuffer);
|
||||
dumpByte(className, bytes);
|
||||
bytes = new TestClassHandler().getBytes(bytes);
|
||||
dumpByte(className, GlobalConfig.getDumpPath(), bytes);
|
||||
} else {
|
||||
mockClass = foundMockForSourceClass(className);
|
||||
if (mockClass != null) {
|
||||
// it's a source class with testable enabled
|
||||
List<MethodInfo> injectMethods = mockClassParser.getTestableMockMethods(mockClass);
|
||||
LogUtil.diagnose("Handling source class %s", className);
|
||||
bytes = new SourceClassHandler(injectMethods, mockClass).getBytes(classFileBuffer);
|
||||
dumpByte(className, bytes);
|
||||
bytes = new SourceClassHandler(injectMethods, mockClass).getBytes(bytes);
|
||||
dumpByte(className, GlobalConfig.getDumpPath(), bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
LogUtil.warn("Failed to transform class " + className);
|
||||
LogUtil.diagnose(t.toString());
|
||||
LogUtil.diagnose(ThreadUtil.getFirstRelatedStackLine(t));
|
||||
LogUtil.warn(t.toString());
|
||||
LogUtil.warn(ThreadUtil.getFirstRelatedStackLine(t));
|
||||
} finally {
|
||||
LogUtil.resetLogLevel();
|
||||
}
|
||||
dumpByte(className, getDumpPathByAnnotation(cn), bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private void dumpByte(String className, byte[] bytes) {
|
||||
String dumpDir = GlobalConfig.getDumpPath();
|
||||
if (dumpDir == null || dumpDir.isEmpty() || !new File(dumpDir).isDirectory()) {
|
||||
private void dumpByte(String className, String dumpPath, byte[] bytes) {
|
||||
if (dumpPath == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String dumpFile = StringUtil.joinPath(dumpDir,
|
||||
String dumpFile = StringUtil.joinPath(dumpPath,
|
||||
className.replace(SLASH, DOT).replace(DOLLAR, UNDERLINE) + ".class");
|
||||
LogUtil.verbose("Dump class: " + dumpFile);
|
||||
FileOutputStream stream = new FileOutputStream(dumpFile);
|
||||
@@ -139,10 +148,9 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
}
|
||||
|
||||
private String lookForOuterMockClass(String className) {
|
||||
String mockClass;
|
||||
mockClass = ClassUtil.getMockClassName(ClassUtil.getSourceClassName(className));
|
||||
if (mockClassParser.isMockClass(mockClass)) {
|
||||
return mockClass;
|
||||
String mockClassName = ClassUtil.getMockClassName(ClassUtil.getSourceClassName(className));
|
||||
if (mockClassParser.isMockClass(ClassUtil.getClassNode(mockClassName))) {
|
||||
return mockClassName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -193,12 +201,13 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
*/
|
||||
private String lookForInnerMockClass(ClassNode cn) {
|
||||
for (InnerClassNode ic : cn.innerClasses) {
|
||||
if ((ic.access & ACC_PUBLIC) != 0 && ic.name.equals(getInnerMockClassName(cn.name)) &&
|
||||
mockClassParser.isMockClass(ic.name)) {
|
||||
if ((ic.access & ACC_STATIC) != 0) {
|
||||
return ic.name;
|
||||
} else {
|
||||
ClassNode innerClassNode = ClassUtil.getClassNode(ic.name);
|
||||
if (ic.name.equals(getInnerMockClassName(cn.name)) && mockClassParser.isMockClass(innerClassNode)) {
|
||||
if ((ic.access & ACC_STATIC) == 0) {
|
||||
LogUtil.warn(String.format("Mock class in \"%s\" is not declared as static", cn.name));
|
||||
} else {
|
||||
ic.access = BytecodeUtil.toPublicAccess(ic.access);
|
||||
return ic.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +238,6 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
private String parseMockWithAnnotation(ClassNode cn, ClassType expectedType) {
|
||||
if (cn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
DiagnoseUtil.setupByAnnotation(an);
|
||||
if (toJavaStyleClassName(an.desc).equals(ConstPool.MOCK_WITH)) {
|
||||
ClassType type = AnnotationUtil.getAnnotationParameter(an, FIELD_TREAT_AS, ClassType.GuessByName,
|
||||
ClassType.class);
|
||||
@@ -245,6 +253,17 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getDumpPathByAnnotation(ClassNode cn) {
|
||||
if (cn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
if (toJavaStyleClassName(an.desc).equals(ConstPool.DUMP_TO)) {
|
||||
return AnnotationUtil.getAnnotationParameter(an, FIELD_PATH, null, String.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isExpectedType(String className, ClassType type, ClassType expectedType) {
|
||||
if (type.equals(ClassType.GuessByName)) {
|
||||
return expectedType.equals(ClassType.TestClass) == className.endsWith(TEST_POSTFIX);
|
||||
|
||||
@@ -173,8 +173,24 @@ public class BytecodeUtil {
|
||||
put(IFNONNULL, -1);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Get stack impact of a specified ops code
|
||||
* @param bytecode ops code to check
|
||||
* @return stack change
|
||||
*/
|
||||
public static int stackEffect(int bytecode) {
|
||||
return bytecodeStackEffect.get(bytecode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure method has public access
|
||||
* @param access original access mark
|
||||
* @return access mark with public flag
|
||||
*/
|
||||
public static int toPublicAccess(int access) {
|
||||
access &= ~ACC_PRIVATE;
|
||||
access &= ~ACC_PROTECTED;
|
||||
access |= ACC_PUBLIC;
|
||||
return access;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ByteCodeConst.*;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.MOCK_POSTFIX;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.TEST_POSTFIX;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.*;
|
||||
import static org.objectweb.asm.Opcodes.*;
|
||||
|
||||
/**
|
||||
@@ -80,7 +79,7 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* fit kotlin companion class name to original name
|
||||
* Fit kotlin companion class name to original name
|
||||
* @param name a class name (which could be a companion class)
|
||||
* @return is companion class or not
|
||||
*/
|
||||
@@ -89,7 +88,7 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* fit kotlin companion class name to original name
|
||||
* Fit kotlin companion class name to original name
|
||||
* @param name a class name (which could be a companion class)
|
||||
* @return original name
|
||||
*/
|
||||
@@ -99,7 +98,7 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* fit kotlin accessor method name to original name
|
||||
* Fit kotlin accessor method name to original name
|
||||
* @param name a accessor name (which could be a common kotlin method)
|
||||
* @return original name
|
||||
*/
|
||||
@@ -109,7 +108,7 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* get mock class name from source class name
|
||||
* Get mock class name from source class name
|
||||
* @param sourceClassName source class name
|
||||
* @return mock class name
|
||||
*/
|
||||
@@ -118,7 +117,7 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* get test class name from source class name
|
||||
* Get test class name from source class name
|
||||
* @param sourceClassName source class name
|
||||
* @return test class name
|
||||
*/
|
||||
@@ -127,7 +126,7 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* get source class name from test class name
|
||||
* Get source class name from test class name
|
||||
* @param testClassName test class name
|
||||
* @return source class name
|
||||
*/
|
||||
@@ -136,7 +135,7 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* get wrapper class of specified private type
|
||||
* Get wrapper class of specified private type
|
||||
* @param primaryType byte code of private type
|
||||
* @return byte code of wrapper class
|
||||
*/
|
||||
@@ -145,7 +144,7 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* get method name and descriptor to convert wrapper type to primary type
|
||||
* Get method name and descriptor to convert wrapper type to primary type
|
||||
* @param primaryType byte code of private type
|
||||
* @return pair of [method-name, method-descriptor]
|
||||
*/
|
||||
@@ -154,7 +153,7 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* get byte code for return specified private type
|
||||
* Get byte code for return specified private type
|
||||
* @param type class type
|
||||
* @return byte code of return operation
|
||||
*/
|
||||
@@ -164,7 +163,7 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* get method node to convert primary type to wrapper type
|
||||
* Get method node to convert primary type to wrapper type
|
||||
* @param type primary type to convert
|
||||
* @return converter method node
|
||||
*/
|
||||
@@ -175,25 +174,25 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* convert slash separated name to dot separated name
|
||||
* Convert slash separated name to dot separated name
|
||||
* @param name original name
|
||||
* @return converted name
|
||||
*/
|
||||
public static String toDotSeparatedName(String name) {
|
||||
return name.replace(ConstPool.SLASH, ConstPool.DOT);
|
||||
return name.replace(SLASH, DOT);
|
||||
}
|
||||
|
||||
/**
|
||||
* convert dot separated name to slash separated name
|
||||
* Convert dot separated name to slash separated name
|
||||
* @param name original name
|
||||
* @return converted name
|
||||
*/
|
||||
public static String toSlashSeparatedName(String name) {
|
||||
return name.replace(ConstPool.DOT, ConstPool.SLASH);
|
||||
return name.replace(DOT, SLASH);
|
||||
}
|
||||
|
||||
/**
|
||||
* convert dot separated name to byte code class name
|
||||
* Convert dot separated name to byte code class name
|
||||
* @param className original name
|
||||
* @return converted name
|
||||
*/
|
||||
@@ -202,7 +201,7 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* convert byte code class name to slash separated human readable name
|
||||
* Convert byte code class name to slash separated human readable name
|
||||
* @param className original name
|
||||
* @return converted name
|
||||
*/
|
||||
@@ -211,7 +210,7 @@ public class ClassUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* convert byte code class name to dot separated human readable name
|
||||
* Convert byte code class name to dot separated human readable name
|
||||
* @param className original name
|
||||
* @return converted name
|
||||
*/
|
||||
|
||||
@@ -6,13 +6,11 @@ import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.MOCK_WITH;
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toJavaStyleClassName;
|
||||
|
||||
public class DiagnoseUtil {
|
||||
|
||||
private static final String FIELD_VALUE = "value";
|
||||
private static final String FIELD_DIAGNOSE = "diagnose";
|
||||
|
||||
public static void setupByClass(ClassNode cn) {
|
||||
if (cn == null || cn.visibleAnnotations == null) {
|
||||
@@ -25,13 +23,6 @@ public class DiagnoseUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static void setupByAnnotation(AnnotationNode an) {
|
||||
// to be remove in v0.6
|
||||
if (toJavaStyleClassName(an.desc).equals(MOCK_WITH)) {
|
||||
setupDiagnose(an, FIELD_DIAGNOSE);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setupDiagnose(AnnotationNode an, String fieldDiagnose) {
|
||||
LogLevel level = AnnotationUtil.getAnnotationParameter(an, fieldDiagnose, null, LogLevel.class);
|
||||
if (level != null) {
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.alibaba.testable.agent.util;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
@@ -31,7 +33,7 @@ public class GlobalConfig {
|
||||
}
|
||||
|
||||
public static String getDumpPath() {
|
||||
return dumpPath;
|
||||
return (dumpPath == null || dumpPath.isEmpty() || !new File(dumpPath).isDirectory()) ? null : dumpPath;
|
||||
}
|
||||
|
||||
public static void setDumpPath(String path) {
|
||||
|
||||
@@ -22,7 +22,7 @@ public class MethodUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* parse method desc, fetch parameter types
|
||||
* Parse method desc, fetch parameter types
|
||||
* @param desc method description
|
||||
* @return list of parameter types
|
||||
*/
|
||||
@@ -55,7 +55,7 @@ public class MethodUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* extract parameter part of method desc
|
||||
* Extract parameter part of method desc
|
||||
* @param desc method description
|
||||
* @return parameter value
|
||||
*/
|
||||
@@ -65,7 +65,7 @@ public class MethodUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* parse method desc, fetch return value types
|
||||
* Parse method desc, fetch return value types
|
||||
* @param desc method description
|
||||
* @return types of return value
|
||||
*/
|
||||
@@ -75,7 +75,7 @@ public class MethodUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* parse method desc, fetch first parameter type (assume first parameter is an object type)
|
||||
* Parse method desc, fetch first parameter type (assume first parameter is an object type)
|
||||
* @param desc method description
|
||||
* @return types of first parameter
|
||||
*/
|
||||
@@ -85,7 +85,7 @@ public class MethodUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* remove first parameter from method descriptor
|
||||
* Remove first parameter from method descriptor
|
||||
* @param desc original descriptor
|
||||
* @return descriptor without first parameter
|
||||
*/
|
||||
@@ -94,7 +94,7 @@ public class MethodUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* add extra parameter to the beginning of method descriptor
|
||||
* Add extra parameter to the beginning of method descriptor
|
||||
* @param desc original descriptor
|
||||
* @param type byte code class name
|
||||
* @return descriptor with specified parameter at begin
|
||||
@@ -109,7 +109,7 @@ public class MethodUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* format to java style constructor descriptor
|
||||
* Format to java style constructor descriptor
|
||||
* @param owner class of method belongs to
|
||||
* @param desc method constructor in bytecode format
|
||||
* @return java style constructor descriptor
|
||||
@@ -121,7 +121,7 @@ public class MethodUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* format to java style method descriptor
|
||||
* Format to java style method descriptor
|
||||
* @param owner class of method belongs to
|
||||
* @param name method name
|
||||
* @param desc method descriptor in bytecode format
|
||||
@@ -135,7 +135,7 @@ public class MethodUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* convert bytecode style parameter descriptor to java style descriptor
|
||||
* Convert bytecode style parameter descriptor to java style descriptor
|
||||
* @param desc bytecode style descriptor
|
||||
* @return java style descriptor
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.alibaba.testable.agent.handler;
|
||||
|
||||
import com.alibaba.testable.agent.model.MethodInfo;
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.InsnNode;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.alibaba.testable.agent.transformer;
|
||||
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@@ -7,12 +7,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
class ClassUtilTest {
|
||||
|
||||
@Test
|
||||
void should_able_to_convert_class_name() {
|
||||
void should_convert_class_name() {
|
||||
assertEquals("Ljava/lang/String;", ClassUtil.toByteCodeClassName("java.lang.String"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_fit_companion_class_name() {
|
||||
void should_fit_companion_class_name() {
|
||||
assertEquals("com/intellij/rt/debugger/agent/CaptureAgent$ParamKeyProvider",
|
||||
ClassUtil.fitCompanionClassName("com/intellij/rt/debugger/agent/CaptureAgent$ParamKeyProvider"));
|
||||
assertEquals("com/alibaba/testable/demo/BlackBox",
|
||||
|
||||
@@ -7,7 +7,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
class CollectionUtilTest {
|
||||
|
||||
@Test
|
||||
void should_able_to_check_collection_contains_any_element() {
|
||||
void should_check_collection_contains_any_element() {
|
||||
assertTrue(CollectionUtil.containsAny(
|
||||
CollectionUtil.listOf("a", "b"), CollectionUtil.listOf("b", "c")
|
||||
));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -8,7 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
class MethodUtilTest {
|
||||
|
||||
@Test
|
||||
void should_able_to_get_parameter_count() {
|
||||
void should_get_parameter_count() {
|
||||
assertEquals(0, MethodUtil.getParameterTypes("()V").size());
|
||||
assertEquals(1, MethodUtil.getParameterTypes("(Ljava/lang/String;)V").size());
|
||||
assertEquals(6, MethodUtil.getParameterTypes("(Ljava/lang/String;IDLjava/lang/String;ZLjava/net/URL;)V").size());
|
||||
@@ -17,13 +17,13 @@ class MethodUtilTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_extract_parameter() {
|
||||
void should_extract_parameter() {
|
||||
assertEquals("", MethodUtil.extractParameters("()I"));
|
||||
assertEquals("Ljava/lang/String;", MethodUtil.extractParameters("(Ljava/lang/String;)I"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_return_type() {
|
||||
void should_get_return_type() {
|
||||
assertEquals("V", MethodUtil.getReturnType("(Ljava/lang/String;)V"));
|
||||
assertEquals("I", MethodUtil.getReturnType("(Ljava/lang/String;)I"));
|
||||
assertEquals("[I", MethodUtil.getReturnType("(Ljava/lang/String;)[I"));
|
||||
@@ -32,14 +32,14 @@ class MethodUtilTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_first_parameter() {
|
||||
void should_get_first_parameter() {
|
||||
assertEquals("Ljava/lang/String;", MethodUtil.getFirstParameter("(Ljava/lang/String;Ljava/lang/Object;I)V"));
|
||||
assertEquals("Ljava/lang/String;", MethodUtil.getFirstParameter("(Ljava/lang/String;)V"));
|
||||
assertEquals("", MethodUtil.getFirstParameter("()V"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_convert_bytecode_parameters() {
|
||||
void should_convert_bytecode_parameters() {
|
||||
assertEquals("", PrivateAccessor.invokeStatic(MethodUtil.class, "toJavaParameterDesc", ""));
|
||||
assertEquals("void", PrivateAccessor.invokeStatic(MethodUtil.class, "toJavaParameterDesc", "V"));
|
||||
assertEquals("int, long", PrivateAccessor.invokeStatic(MethodUtil.class, "toJavaParameterDesc", "IJ"));
|
||||
|
||||
@@ -7,7 +7,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
class StringUtilTest {
|
||||
|
||||
@Test
|
||||
void should_able_to_repeat_text() {
|
||||
void should_repeat_text() {
|
||||
assertEquals("", StringUtil.repeat("abc", 0));
|
||||
assertEquals("abc", StringUtil.repeat("abc", 1));
|
||||
assertEquals("abcabcabc", StringUtil.repeat("abc", 3));
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.5.2</version>
|
||||
<version>0.6.0</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-all</artifactId>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.5.2</version>
|
||||
<version>0.6.0</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-core</artifactId>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.alibaba.testable.core.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Dump byte code for single class
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
public @interface DumpTo {
|
||||
|
||||
/**
|
||||
* dump class byte code to specified folder
|
||||
* @return an exist folder
|
||||
*/
|
||||
String path() default "";
|
||||
|
||||
}
|
||||
@@ -28,11 +28,4 @@ public @interface MockWith {
|
||||
*/
|
||||
ClassType treatAs() default ClassType.GuessByName;
|
||||
|
||||
/**
|
||||
* switch of mock diagnose information of current test class
|
||||
* @deprecated to be removed in v0.6, use @MockDiagnose annotation instead
|
||||
* @return enable or disable
|
||||
*/
|
||||
LogLevel diagnose() default LogLevel.DISABLE;
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ public class ConstPool {
|
||||
* Name of the constructor method
|
||||
*/
|
||||
public static final String CONSTRUCTOR = "<init>";
|
||||
public static final String THIS_REF = "this";
|
||||
|
||||
/**
|
||||
* Postfix or test and mock file
|
||||
@@ -13,4 +14,12 @@ public class ConstPool {
|
||||
public static final String TEST_POSTFIX = "Test";
|
||||
public static final String MOCK_POSTFIX = "Mock";
|
||||
|
||||
/**
|
||||
* Common symbols
|
||||
*/
|
||||
public static final String DOT = ".";
|
||||
public static final String SLASH = "/";
|
||||
public static final String DOLLAR = "$";
|
||||
public static final String UNDERLINE = "_";
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.core.error;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class NoSuchMemberError extends Error {
|
||||
|
||||
public NoSuchMemberError(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.alibaba.testable.core.exception;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class ClassConstructionException extends RuntimeException {
|
||||
|
||||
public ClassConstructionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ClassConstructionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
private static Throwable getRootCause(Throwable cause) {
|
||||
if (cause instanceof InvocationTargetException) {
|
||||
return ((InvocationTargetException)cause).getTargetException();
|
||||
} else {
|
||||
return cause;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.alibaba.testable.core.model;
|
||||
|
||||
import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.type.NullType;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.lang.model.type.TypeVisitor;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class TestableNull implements NullType {
|
||||
|
||||
@Override
|
||||
public TypeKind getKind() {
|
||||
return TypeKind.NULL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R, P> R accept(TypeVisitor<R, P> v, P p) {
|
||||
return v.visitNull(this, p);
|
||||
}
|
||||
|
||||
public List<? extends AnnotationMirror> getAnnotationMirrors() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public <A extends Annotation> A getAnnotation(Class<A> annotationType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationType) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.alibaba.testable.core.tool;
|
||||
|
||||
import com.alibaba.testable.core.error.NoSuchMemberError;
|
||||
import com.alibaba.testable.core.util.CollectionUtil;
|
||||
import com.alibaba.testable.core.util.FixSizeMap;
|
||||
import com.alibaba.testable.core.util.TypeUtil;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
|
||||
import static com.alibaba.testable.core.constant.ConstPool.SLASH;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class OmniAccessor {
|
||||
|
||||
private static final FixSizeMap<Class<?>, List<String>> MEMBER_INDEXES = new FixSizeMap<Class<?>, List<String>>(30);
|
||||
private static final String THIS_REF_PREFIX = "this$";
|
||||
private static final String PATTERN_PREFIX = ".*/";
|
||||
private static final String REGEX_ANY_NAME = "[^/]+";
|
||||
private static final String REGEX_ANY_FIELD_NAME = "[^{]*";
|
||||
private static final String REGEX_ANY_CLASS_NAME = "[^}]*";
|
||||
private static final String REGEX_ANY_FIELD = "[^{]+";
|
||||
private static final String REGEX_ANY_CLASS = "\\{[^}]+\\}";
|
||||
private static final String BRACE_START = "{";
|
||||
private static final String ESCAPE = "\\";
|
||||
private static final String BRACE_END = "}";
|
||||
private static final String BRACKET_START = "[";
|
||||
private static final String BRACKET_END = "]";
|
||||
private static final String STAR = "*";
|
||||
|
||||
private OmniAccessor() {}
|
||||
|
||||
/**
|
||||
* 获取第一个符合搜索路径的成员
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @param queryPath 搜索路径
|
||||
* @return 返回目标成员,若不存在则返回null
|
||||
*/
|
||||
public static <T> T getFirst(Object target, String queryPath) {
|
||||
List<T> values = get(target, queryPath);
|
||||
return values.isEmpty() ? null : values.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有符合搜索路径的成员
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @param queryPath 搜索路径
|
||||
* @return 返回所有匹配的成员
|
||||
*/
|
||||
public static <T> List<T> get(Object target, String queryPath) {
|
||||
List<T> values = new ArrayList<T>();
|
||||
for (String memberPath : MEMBER_INDEXES.getOrElse(target.getClass(), generateMemberIndex(target.getClass()))) {
|
||||
if (memberPath.matches(toPattern(queryPath))) {
|
||||
try {
|
||||
List<T> elements = getByPath(target, memberPath, queryPath);
|
||||
if (!elements.isEmpty()) {
|
||||
values.addAll(elements);
|
||||
}
|
||||
} catch (NoSuchFieldException e) {
|
||||
// continue
|
||||
} catch (IllegalAccessException e) {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if (values.isEmpty()) {
|
||||
throw new NoSuchMemberError("Query \"" + queryPath + "\"" + " does not match any member!");
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为符合搜索路径的成员赋值
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @param queryPath 搜索路径
|
||||
* @param value 新的值
|
||||
* @return 实际影响的成员个数
|
||||
*/
|
||||
public static int set(Object target, String queryPath, Object value) {
|
||||
int count = 0;
|
||||
for (String memberPath : MEMBER_INDEXES.getOrElse(target.getClass(), generateMemberIndex(target.getClass()))) {
|
||||
if (memberPath.matches(toPattern(queryPath))) {
|
||||
try {
|
||||
List<Object> parent = getByPath(target, toParent(memberPath), toParent(queryPath));
|
||||
if (!parent.isEmpty()) {
|
||||
for (Object p : parent) {
|
||||
if (setByPathSegment(p, toChild(memberPath), toChild(queryPath), value)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (NoSuchFieldException e) {
|
||||
// continue
|
||||
} catch (IllegalAccessException e) {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
throw new NoSuchMemberError("Query \"" + queryPath + "\"" + " does not match any member!");
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static List<String> generateMemberIndex(Class<?> clazz) {
|
||||
return generateMemberIndex(clazz, "", new HashSet<Class<?>>(6));
|
||||
}
|
||||
|
||||
private static List<String> generateMemberIndex(Class<?> clazz, String basePath, Set<Class<?>> classPool) {
|
||||
if (TypeUtil.isBasicType(clazz)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
classPool.add(clazz);
|
||||
List<String> paths = new ArrayList<String>();
|
||||
for (Field f : TypeUtil.getAllFields(clazz)) {
|
||||
if (!classPool.contains(f.getType()) && !f.getName().startsWith(THIS_REF_PREFIX)) {
|
||||
String fullPath = basePath + SLASH + toPath(f);
|
||||
paths.add(fullPath);
|
||||
paths.addAll(generateMemberIndex(f.getType(), fullPath, classPool));
|
||||
}
|
||||
}
|
||||
classPool.remove(clazz);
|
||||
return paths;
|
||||
}
|
||||
|
||||
private static String toPath(Field field) {
|
||||
return field.getName() + BRACE_START + field.getType().getSimpleName() + BRACE_END;
|
||||
}
|
||||
|
||||
private static String toPattern(String queryPath) {
|
||||
String[] querySegments = queryPath.split(SLASH);
|
||||
String[] patternSegments = new String[querySegments.length];
|
||||
for (int i = 0; i < querySegments.length; i++) {
|
||||
patternSegments[i] = toSinglePattern(querySegments[i]);
|
||||
}
|
||||
return PATTERN_PREFIX + CollectionUtil.join(Arrays.asList(patternSegments), SLASH);
|
||||
}
|
||||
|
||||
private static String toSinglePattern(String querySegment) {
|
||||
if (querySegment.endsWith(BRACKET_END)) {
|
||||
querySegment = querySegment.substring(0, querySegment.lastIndexOf(BRACKET_START));
|
||||
}
|
||||
if (querySegment.isEmpty()) {
|
||||
return "";
|
||||
} else if (querySegment.equals(STAR)) {
|
||||
return REGEX_ANY_NAME;
|
||||
} else if (querySegment.startsWith(BRACE_START)) {
|
||||
return REGEX_ANY_FIELD + querySegment.replace(BRACE_START, ESCAPE + BRACE_START)
|
||||
.replace(BRACE_END, ESCAPE + BRACE_END)
|
||||
.replace(BRACKET_START, ESCAPE + BRACKET_START)
|
||||
.replace(BRACKET_END, ESCAPE + BRACKET_END)
|
||||
.replace(STAR, REGEX_ANY_CLASS_NAME);
|
||||
} else {
|
||||
return querySegment.replace(STAR, REGEX_ANY_FIELD_NAME) + REGEX_ANY_CLASS;
|
||||
}
|
||||
}
|
||||
|
||||
private static String toChild(String memberPath) {
|
||||
return memberPath.contains(SLASH) ? memberPath.substring(memberPath.lastIndexOf(SLASH) + 1) : memberPath;
|
||||
}
|
||||
|
||||
private static String toParent(String memberPath) {
|
||||
return memberPath.contains(SLASH) ? memberPath.substring(0, memberPath.lastIndexOf(SLASH)) : "";
|
||||
}
|
||||
|
||||
private static String extraNameFromMemberRecord(String memberSegment) {
|
||||
return memberSegment.substring(0, memberSegment.indexOf(BRACE_START));
|
||||
}
|
||||
|
||||
private static int extraIndexFromQuery(String query) {
|
||||
return query.endsWith(BRACKET_END)
|
||||
? Integer.parseInt(query.substring(query.lastIndexOf(BRACKET_START) + 1, query.length() - 1))
|
||||
: -1;
|
||||
}
|
||||
|
||||
private static <T> List<T> getByPath(Object target, String memberPath, String queryPath)
|
||||
throws NoSuchFieldException, IllegalAccessException {
|
||||
String[] memberSegments = memberPath.substring(1).split(SLASH);
|
||||
String[] querySegments = queryPath.split(SLASH);
|
||||
if (memberSegments.length < querySegments.length) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String[] querySegmentsWithPadding = calculateFullQueryPath(querySegments, memberSegments);
|
||||
return getBySegment(target, memberSegments, querySegmentsWithPadding, 0);
|
||||
}
|
||||
|
||||
private static <T> List<T> getBySegment(Object target, String[] memberSegments, String[] querySegments, int n)
|
||||
throws IllegalAccessException {
|
||||
if (target == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (memberSegments.length == n) {
|
||||
int nth = extraIndexFromQuery(querySegments[n]);
|
||||
return Collections.singletonList((T)(nth > 0 ? Array.get(target, nth) : target));
|
||||
}
|
||||
int nth = extraIndexFromQuery(querySegments[n]);
|
||||
String fieldName = extraNameFromMemberRecord(memberSegments[n]);
|
||||
List<T> nexts = new ArrayList<T>();
|
||||
if (target.getClass().isArray()) {
|
||||
if (nth < 0) {
|
||||
for (int i = 0; i < Array.getLength(target); i++) {
|
||||
List<T> all = getBySegment(getFieldValue(Array.get(target, i), fieldName, i),
|
||||
memberSegments, querySegments, n + 1);
|
||||
nexts.addAll(all);
|
||||
}
|
||||
} else {
|
||||
List <T> all = getBySegment(getFieldValue(Array.get(target, nth), fieldName, nth),
|
||||
memberSegments, querySegments, n + 1);
|
||||
nexts.addAll(all);
|
||||
}
|
||||
} else {
|
||||
List <T> all = getBySegment(getFieldValue(target, fieldName, nth), memberSegments, querySegments, n + 1);
|
||||
nexts.addAll(all);
|
||||
}
|
||||
return nexts;
|
||||
}
|
||||
|
||||
private static Object getFieldValue(Object obj, String name, int nth) throws IllegalAccessException {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
Field field = TypeUtil.getFieldByName(obj.getClass(), name);
|
||||
field.setAccessible(true);
|
||||
if (field.getType().isArray() && nth >= 0) {
|
||||
Object f = field.get(obj);
|
||||
return Array.get(f, nth);
|
||||
} else {
|
||||
return field.get(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] calculateFullQueryPath(String[] querySegments, String[] memberSegments) {
|
||||
String[] fullQuerySegments = new String[memberSegments.length + 1];
|
||||
for (int i = 0; i <= memberSegments.length - querySegments.length; i++) {
|
||||
fullQuerySegments[i] = "";
|
||||
}
|
||||
System.arraycopy(querySegments, 0, fullQuerySegments, memberSegments.length - querySegments.length + 1,
|
||||
querySegments.length);
|
||||
return fullQuerySegments;
|
||||
}
|
||||
|
||||
private static boolean setByPathSegment(Object target, String memberSegment, String querySegment, Object value)
|
||||
throws IllegalAccessException {
|
||||
String name = extraNameFromMemberRecord(memberSegment);
|
||||
int nth = extraIndexFromQuery(querySegment);
|
||||
boolean isFieldMatch = false;
|
||||
if (target.getClass().isArray()) {
|
||||
for (int i = 0; i < Array.getLength(target); i++) {
|
||||
isFieldMatch |= setFieldByName(Array.get(target, i), name, nth, value);
|
||||
}
|
||||
} else {
|
||||
isFieldMatch = setFieldByName(target, name, nth, value);
|
||||
}
|
||||
return isFieldMatch;
|
||||
}
|
||||
|
||||
private static boolean setFieldByName(Object target, String name, int nth, Object value) throws IllegalAccessException {
|
||||
Field field = TypeUtil.getFieldByName(target.getClass(), name);
|
||||
if (field == null) {
|
||||
return false;
|
||||
}
|
||||
field.setAccessible(true);
|
||||
if (field.getType().isArray()) {
|
||||
Object f = field.get(target);
|
||||
if (nth >= 0) {
|
||||
Array.set(f, nth, value);
|
||||
} else {
|
||||
for (int i = 0; i < Array.getLength(f); i++) {
|
||||
Array.set(f, i, value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
field.set(target, value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package com.alibaba.testable.core.tool;
|
||||
|
||||
import com.alibaba.testable.core.exception.ClassConstructionException;
|
||||
import com.alibaba.testable.core.model.TestableNull;
|
||||
import com.alibaba.testable.core.util.TypeUtil;
|
||||
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.lang.reflect.*;
|
||||
import java.util.*;
|
||||
|
||||
import static com.alibaba.testable.core.constant.ConstPool.DOLLAR;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class OmniConstructor {
|
||||
|
||||
private static final int INITIAL_CAPACITY = 6;
|
||||
|
||||
private OmniConstructor() {}
|
||||
|
||||
/**
|
||||
* 快速创建任意指定类型的测试对象
|
||||
*
|
||||
* @param clazz 期望的对象类型
|
||||
* @return 返回新创建的对象
|
||||
*/
|
||||
public static <T> T newInstance(Class<T> clazz) {
|
||||
return handleCircleReference(newInstance(clazz, new HashSet<Class<?>>(INITIAL_CAPACITY)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速创建任意指定类型的对象数组
|
||||
*
|
||||
* @param clazz 期望的对象类型
|
||||
* @param size 数组大小
|
||||
* @return 返回新创建的对象数组
|
||||
*/
|
||||
public static <T> T[] newArray(Class<T> clazz, int size) {
|
||||
return (T[])handleCircleReference(newArray(clazz, size, new HashSet<Class<?>>(INITIAL_CAPACITY)));
|
||||
}
|
||||
|
||||
private static <T> T newInstance(Class<T> clazz, Set<Class<?>> classPool) {
|
||||
if (classPool.contains(clazz)) {
|
||||
return null;
|
||||
}
|
||||
classPool.add(clazz);
|
||||
try {
|
||||
if (clazz.isPrimitive()) {
|
||||
return newPrimitive(clazz);
|
||||
} else if (clazz.isArray()) {
|
||||
return (T)newArray(clazz.getComponentType(), 0, classPool);
|
||||
} else if (clazz.isEnum()) {
|
||||
return newEnum(clazz);
|
||||
} else if (clazz.isInterface()) {
|
||||
return newInterface(clazz);
|
||||
} else if (Modifier.isAbstract(clazz.getModifiers())) {
|
||||
return newAbstractClass(clazz);
|
||||
}
|
||||
return newObject(clazz, classPool);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new ClassConstructionException("Failed to find constructor", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new ClassConstructionException("Failed to access constructor", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new ClassConstructionException("Failed to invoke constructor", e);
|
||||
} catch (InstantiationException e) {
|
||||
throw new ClassConstructionException("Failed to complete construction", e);
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassConstructionException("Unexpected type", e);
|
||||
} finally {
|
||||
classPool.remove(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T newObject(Class<T> clazz, Set<Class<?>> classPool)
|
||||
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
||||
Object ins = createInstance(getBestConstructor(clazz), classPool);
|
||||
if (!TypeUtil.isBasicType(clazz)) {
|
||||
for (Field f : TypeUtil.getAllFields(clazz)) {
|
||||
f.setAccessible(true);
|
||||
// skip "$jacocoData" field added by jacoco
|
||||
if (f.get(ins) == null && !f.getName().startsWith(DOLLAR)) {
|
||||
f.set(ins, newInstance(f.getType(), classPool));
|
||||
}
|
||||
}
|
||||
}
|
||||
return (T)ins;
|
||||
}
|
||||
|
||||
private static Object newArray(Class<?> clazz, int size, Set<Class<?>> classPool) {
|
||||
// primary[] cannot be cast to Object[], have to use Object instead of T[]
|
||||
Object array = Array.newInstance(clazz, size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
Array.set(array, i, newInstance(clazz, classPool));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private static <T> T newAbstractClass(Class<T> clazz) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static <T> T newInterface(Class<T> clazz) {
|
||||
if (clazz.equals(List.class)) {
|
||||
return (T)Collections.emptyList();
|
||||
} else if (clazz.equals(Map.class)) {
|
||||
return (T)Collections.emptyMap();
|
||||
} else if (clazz.equals(Set.class)) {
|
||||
return (T)Collections.emptySet();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static <T> T newEnum(Class<T> clazz)
|
||||
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
|
||||
T[] constants = clazz.getEnumConstants();
|
||||
return constants.length > 0 ? constants[0] : null;
|
||||
}
|
||||
|
||||
private static <T> T newPrimitive(Class<T> clazz) {
|
||||
if (clazz.equals(int.class)) {
|
||||
return (T)Integer.valueOf(0);
|
||||
} else if (clazz.equals(char.class)) {
|
||||
return (T)Character.valueOf((char)0);
|
||||
} else if (clazz.equals(short.class)) {
|
||||
return (T)Short.valueOf((short)0);
|
||||
} else if (clazz.equals(long.class)) {
|
||||
return (T)Long.valueOf(0L);
|
||||
} else if (clazz.equals(double.class)) {
|
||||
return (T)Double.valueOf(0);
|
||||
} else if (clazz.equals(float.class)) {
|
||||
return (T)Float.valueOf(0F);
|
||||
} else if (clazz.equals(byte.class)) {
|
||||
return (T)Byte.valueOf((byte)0);
|
||||
} else if (clazz.equals(boolean.class)) {
|
||||
return (T)Boolean.valueOf(false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static <T> T handleCircleReference(T instance) {
|
||||
try {
|
||||
if (instance.getClass().isArray()) {
|
||||
for (int i = 0; i < Array.getLength(instance); i++) {
|
||||
handleCircleReference(Array.get(instance, i), new HashMap<Class<?>, Object>(INITIAL_CAPACITY));
|
||||
}
|
||||
} else {
|
||||
handleCircleReference(instance, new HashMap<Class<?>, Object>(INITIAL_CAPACITY));
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new ClassConstructionException("Failed to access field", e);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static <T> void handleCircleReference(T instance, Map<Class<?>, Object> classPool)
|
||||
throws IllegalAccessException {
|
||||
classPool.put(instance.getClass(), instance);
|
||||
for (Field f : TypeUtil.getAllFields(instance.getClass())) {
|
||||
f.setAccessible(true);
|
||||
Object fieldIns = f.get(instance);
|
||||
Class<?> fieldType = f.getType();
|
||||
if (fieldType.isArray()) {
|
||||
if (fieldIns != null) {
|
||||
for (int i = 0; i < Array.getLength(fieldIns); i++) {
|
||||
handleCircleReference(Array.get(fieldIns, i), classPool);
|
||||
}
|
||||
}
|
||||
} else if (!fieldType.isPrimitive() && !TypeUtil.isBasicType(fieldType)) {
|
||||
if (fieldIns == null && classPool.containsKey(fieldType)) {
|
||||
f.set(instance, classPool.get(fieldType));
|
||||
} else if (fieldIns != null && !classPool.containsKey(fieldType)) {
|
||||
handleCircleReference(fieldIns, classPool);
|
||||
}
|
||||
}
|
||||
}
|
||||
classPool.remove(instance.getClass());
|
||||
}
|
||||
|
||||
private static Object createInstance(Constructor<?> constructor, Set<Class<?>> classPool)
|
||||
throws InstantiationException, IllegalAccessException, InvocationTargetException {
|
||||
constructor.setAccessible(true);
|
||||
Class<?>[] types = constructor.getParameterTypes();
|
||||
if (types.length == 1 && types[0].equals(NullType.class)) {
|
||||
return constructor.newInstance(new TestableNull());
|
||||
} else {
|
||||
Object[] args = new Object[types.length];
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
args[i] = newInstance(types[i], classPool);
|
||||
}
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
}
|
||||
|
||||
private static Constructor<?> getBestConstructor(Class<?> clazz) {
|
||||
Constructor<?> bestConstructor = null;
|
||||
int minimalParametersSize = 999;
|
||||
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
|
||||
Class<?>[] types = constructor.getParameterTypes();
|
||||
if (types.length == 1 && types[0].equals(NullType.class)) {
|
||||
return constructor;
|
||||
} else if (types.length < minimalParametersSize) {
|
||||
minimalParametersSize = types.length;
|
||||
bestConstructor = constructor;
|
||||
}
|
||||
}
|
||||
return bestConstructor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.core.accessor;
|
||||
package com.alibaba.testable.core.tool;
|
||||
|
||||
import com.alibaba.testable.core.exception.MemberAccessException;
|
||||
import com.alibaba.testable.core.util.TypeUtil;
|
||||
@@ -15,6 +15,8 @@ public class PrivateAccessor {
|
||||
|
||||
private static final String KOTLIN_COMPANION_FIELD = "Companion";
|
||||
|
||||
private PrivateAccessor() {}
|
||||
|
||||
/**
|
||||
* 读取任意类的私有字段
|
||||
* @param ref 目标对象
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class CollectionUtil {
|
||||
|
||||
public static Object[] slice(Object[] args, int pos) {
|
||||
@@ -12,4 +15,14 @@ public class CollectionUtil {
|
||||
return slicedArgs;
|
||||
}
|
||||
|
||||
public static String join(Collection<?> collection, String joinSymbol) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(Iterator<?> i = collection.iterator(); i.hasNext(); sb.append((String)i.next())) {
|
||||
if (sb.length() != 0) {
|
||||
sb.append(joinSymbol);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user