mirror of
https://github.com/alibaba/testable-mock.git
synced 2026-08-19 17:53:29 +08:00
Compare commits
133 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 | ||
|
|
250a1beaf9 | ||
|
|
323ab398b2 | ||
|
|
edd4c24d88 | ||
|
|
4995bbd9a9 | ||
|
|
121536a337 | ||
|
|
b163152c2f | ||
|
|
5897e087c8 | ||
|
|
6f079db3f3 | ||
|
|
40073ad784 | ||
|
|
e46ec3d98a | ||
|
|
1849cf85c5 | ||
|
|
f8450d7047 | ||
|
|
196dffc73d | ||
|
|
1ff6ec5961 | ||
|
|
e50633b884 | ||
|
|
2976ca0211 | ||
|
|
24c6a9cc5c | ||
|
|
9e7ceb2dc1 | ||
|
|
8dfd0b4516 | ||
|
|
190cf4cc3c | ||
|
|
d49f526376 | ||
|
|
f01ff10fba | ||
|
|
f64b07e9da | ||
|
|
f96d272f97 | ||
|
|
b1e6ad58cc | ||
|
|
48d6ef328b | ||
|
|
84df2a0fd8 | ||
|
|
f9621a6f2e | ||
|
|
55ccabbb39 | ||
|
|
cfc28467bb | ||
|
|
23a0a0cd90 | ||
|
|
a1086ff402 | ||
|
|
6237e19a32 | ||
|
|
df73dac54f | ||
|
|
f660ccb88f | ||
|
|
82d87f3447 | ||
|
|
65cc631d39 | ||
|
|
bdd99577c4 | ||
|
|
8be5550331 | ||
|
|
6a5c817a3a | ||
|
|
c76ed2d84a | ||
|
|
14549040d9 | ||
|
|
03ec857cd0 | ||
|
|
b4a29e7e8e | ||
|
|
02c74299f4 | ||
|
|
4c6fc5b228 | ||
|
|
6d6e2ecb7f | ||
|
|
c364e96f77 | ||
|
|
dab1d36a81 | ||
|
|
01bd676df7 | ||
|
|
02d7fb6e91 | ||
|
|
d8ffcacdaf | ||
|
|
be53ea2d9c | ||
|
|
9eb8682ec0 | ||
|
|
2a54fe70dd | ||
|
|
790409a336 | ||
|
|
189ef43565 | ||
|
|
34f1dc8f73 | ||
|
|
b60ebf2f2c | ||
|
|
d95cba6d37 | ||
|
|
5ef06c4bde | ||
|
|
8814276d63 | ||
|
|
374a4d5442 | ||
|
|
9a22f361f3 | ||
|
|
94c5aa8621 | ||
|
|
74e3cf16a4 | ||
|
|
19f28c32db | ||
|
|
affa56d057 | ||
|
|
f1edf92626 | ||
|
|
6f9accf319 | ||
|
|
cefc5fb1df | ||
|
|
948878687c | ||
|
|
d626906d28 | ||
|
|
705ba796f1 | ||
|
|
7e44a66205 | ||
|
|
d57dedb6fc | ||
|
|
d860684e91 | ||
|
|
7bbdf82a41 | ||
|
|
126c7257d8 | ||
|
|
adfba4bac0 | ||
|
|
2c18ea12ed | ||
|
|
d4bce13918 | ||
|
|
b23b8b98b3 | ||
|
|
f228ed5002 | ||
|
|
2303ac188e | ||
|
|
290d0db400 | ||
|
|
77b955a5ce | ||
|
|
ab8a0b32e5 | ||
|
|
39c788cd02 | ||
|
|
7029579534 | ||
|
|
ba951ce744 | ||
|
|
2bbe2eba93 | ||
|
|
4c96f4cc0e | ||
|
|
3a2e3032df | ||
|
|
530f3295bd | ||
|
|
650b385785 | ||
|
|
f9be92e9d0 | ||
|
|
019e407a4f | ||
|
|
453908cd46 |
16
README.md
16
README.md
@@ -2,26 +2,26 @@
|
||||
|
||||
换种思路写Mock,让单元测试更简单。
|
||||
|
||||
无需初始化,不挑测试框架,甭管要换的是私有方法、静态方法、构造方法还是其他任何类的任何方法,也甭管要换的对象是怎么创建的。写好Mock定义,加个`@MockMethod`注解,一切统统搞定。
|
||||
无需初始化,不挑服务框架,甭管要换的是私有方法、静态方法、构造方法还是其他任何类的任何方法,也甭管要换的对象是怎么创建的。写好Mock定义,加个`@MockMethod`注解,一切统统搞定。
|
||||
|
||||
- 文档:https://alibaba.github.io/testable-mock/
|
||||
- 国内文档镜像:http://freyrlin.gitee.io/testable-mock/
|
||||
|
||||
阅读[这里](https://mp.weixin.qq.com/s/KyU6Eu7mDkZU8FspfSqfMw)了解更多故事。
|
||||
|
||||
> 特别说明
|
||||
> 1. 如遇到"Attempt to access none-static member in mock method"错误,参见[常见问题](https://alibaba.github.io/testable-mock/#/zh-cn/doc/frequently-asked-questions)第8条
|
||||
> 2. 如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将在24小时内回复并处理
|
||||
<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小时内回复并处理
|
||||
|
||||
-----
|
||||
|
||||
## 版本计划
|
||||
|
||||
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整,请以最新内容为准
|
||||
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
|
||||
|
||||
- `0.4.x` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
|
||||
- `0.5` 实现以"Mock方法集"为单元的Mock方法复用机制,让测试类之间可以方便复用相同的Mock方法
|
||||
- `0.5.?` 实现第四项单元测试增强能力"[快速入参构造器]()"
|
||||
- `0.6` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
|
||||
- `0.7` 完善Mock功能的防代码重构机制,支持快速Mock指定类型的所有方法
|
||||
- `1.0` 功能稳定,一个崭新的开始
|
||||
|
||||
## 目录结构
|
||||
|
||||
|
||||
@@ -7,6 +7,14 @@ Write a mock method, add an `@MockMethod` annotation, everything is done.
|
||||
|
||||
Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
|
||||
|
||||
## Roadmap
|
||||
|
||||
`TestableMock` is still under heavy development, the following version plans may be adjusted during the iteration
|
||||
|
||||
- `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
|
||||
|
||||
```bash
|
||||
|
||||
@@ -13,8 +13,8 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.10')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.10')
|
||||
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.4.10</testable.version>
|
||||
<testable.version>0.6.0</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -32,15 +32,14 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<configuration>
|
||||
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<configuration>
|
||||
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- <plugin>-->
|
||||
<!-- <groupId>com.alibaba.testable</groupId>-->
|
||||
<!-- <artifactId>testable-maven-plugin</artifactId>-->
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.model.Box;
|
||||
import com.alibaba.testable.demo.model.Color;
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox;
|
||||
import com.alibaba.demo.basic.model.mock.Box;
|
||||
import com.alibaba.demo.basic.model.mock.Color;
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* 演示对内部类的Mock支持
|
||||
* Demonstrate support for mocking invocation inside a inner class
|
||||
*/
|
||||
public class DemoInnerClass {
|
||||
|
||||
public static class StaticInner {
|
||||
/**
|
||||
* invocation inside a static inner class
|
||||
*/
|
||||
public String demo() {
|
||||
return methodToBeMock();
|
||||
}
|
||||
}
|
||||
|
||||
public class Inner {
|
||||
/**
|
||||
* invocation inside a non-static inner class
|
||||
*/
|
||||
public String demo() {
|
||||
return methodToBeMock();
|
||||
}
|
||||
}
|
||||
|
||||
private ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
|
||||
public String callAnonymousInner() throws ExecutionException, InterruptedException {
|
||||
/**
|
||||
* invocation inside a anonymous inner class
|
||||
*/
|
||||
Future<String> future = executorService.submit(new Callable<String>() {
|
||||
@Override
|
||||
public String call() throws Exception {
|
||||
return methodToBeMock();
|
||||
}
|
||||
});
|
||||
return future.get();
|
||||
}
|
||||
|
||||
public String callLambdaInner() throws ExecutionException, InterruptedException {
|
||||
/**
|
||||
* invocation inside a lambda inner class
|
||||
*/
|
||||
Future<String> future = executorService.submit(() -> methodToBeMock());
|
||||
return future.get();
|
||||
}
|
||||
|
||||
public String callInnerDemo() {
|
||||
return new Inner().demo();
|
||||
}
|
||||
|
||||
public static String methodToBeMock() {
|
||||
return "RealCall";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.testable.demo.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.testable.demo;
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
package com.alibaba.demo.basic.model.mock;
|
||||
|
||||
public class BlackBox extends Box implements Color {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
package com.alibaba.demo.basic.model.mock;
|
||||
|
||||
abstract public class Box {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.demo.one2multi;
|
||||
|
||||
public class ASvc {
|
||||
|
||||
public String demo(String name) {
|
||||
return String.format("a_%s", name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.demo.one2multi;
|
||||
|
||||
public class BSvc {
|
||||
|
||||
public String demo(String name) {
|
||||
return String.format("b_%s", name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.demo.one2multi;
|
||||
|
||||
public class CSvc {
|
||||
|
||||
public String demo(String name) {
|
||||
return String.format("c_%s", name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
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.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.model.Box;
|
||||
import com.alibaba.testable.demo.model.Color;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
@@ -17,74 +17,75 @@ class DemoInheritTest {
|
||||
|
||||
private DemoInherit demoInherit = new DemoInherit();
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private void put_into_box(Box self, String something) {
|
||||
self.put("put_" + something + "_into_box");
|
||||
}
|
||||
public static class Mock {
|
||||
@MockMethod(targetMethod = "put")
|
||||
private void put_into_box(Box self, String something) {
|
||||
self.put("put_" + something + "_into_box");
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private void put_into_blackbox(BlackBox self, String something) {
|
||||
self.put("put_" + something + "_into_blackbox");
|
||||
}
|
||||
@MockMethod(targetMethod = "put")
|
||||
private void put_into_blackbox(BlackBox self, String something) {
|
||||
self.put("put_" + something + "_into_blackbox");
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private String get_from_box(Box self) {
|
||||
return "get_from_box";
|
||||
}
|
||||
@MockMethod(targetMethod = "get")
|
||||
private String get_from_box(Box self) {
|
||||
return "get_from_box";
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private String get_from_blackbox(BlackBox self) {
|
||||
return "get_from_blackbox";
|
||||
}
|
||||
@MockMethod(targetMethod = "get")
|
||||
private String get_from_blackbox(BlackBox self) {
|
||||
return "get_from_blackbox";
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private String get_color_from_color(Color self) {
|
||||
return "color_from_color";
|
||||
}
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private String get_color_from_color(Color self) {
|
||||
return "color_from_color";
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private String get_color_from_blackbox(BlackBox self) {
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private String get_color_from_blackbox(BlackBox self) {
|
||||
return "color_from_blackbox";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_sub_object_method_by_parent_object() {
|
||||
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);
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示对内部类的Mock支持
|
||||
* Demonstrate support for mocking invocation inside a inner class
|
||||
*/
|
||||
class DemoInnerClassTest {
|
||||
|
||||
public static class Mock {
|
||||
@MockMethod(targetClass = DemoInnerClass.class)
|
||||
String methodToBeMock() {
|
||||
return "MockedCall";
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_mock_invoke_inside_inner_class() throws Exception {
|
||||
DemoInnerClass demo = new DemoInnerClass();
|
||||
assertEquals("MockedCall", demo.callInnerDemo());
|
||||
assertEquals("MockedCall", demo.callAnonymousInner());
|
||||
assertEquals("MockedCall", demo.callLambdaInner());
|
||||
assertEquals("MockedCall", new DemoInnerClass.StaticInner().demo());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
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.testable.demo.model.BlackBox;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeMatcher.*;
|
||||
@@ -17,15 +17,16 @@ class DemoMatcherTest {
|
||||
|
||||
private DemoMatcher demoMatcher = new DemoMatcher();
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithoutArgument(DemoMatcher self) {}
|
||||
public static class Mock {
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithoutArgument(DemoMatcher self) {}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArguments(DemoMatcher self, Object a1, Object a2) {}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArrayArgument(DemoMatcher self, Object[] a) {}
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArguments(DemoMatcher self, Object a1, Object a2) {}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArrayArgument(DemoMatcher self, Object[] a) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_match_no_argument() {
|
||||
@@ -1,15 +1,16 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
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.testable.demo.model.BlackBox;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
@@ -20,68 +21,71 @@ class DemoMockTest {
|
||||
|
||||
private DemoMock demoMock = new DemoMock();
|
||||
|
||||
@MockConstructor
|
||||
private BlackBox createBlackBox(String text) {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String staticFunc() {
|
||||
return "_MOCK_TAIL";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class)
|
||||
private String trim() {
|
||||
return "trim_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "substring")
|
||||
private String sub(int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class)
|
||||
private boolean startsWith(String s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = BlackBox.class)
|
||||
private BlackBox secretBox() {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String callFromDifferentMethod() {
|
||||
if ("special_case".equals(MOCK_CONTEXT.get("case"))) {
|
||||
return "mock_special";
|
||||
public static class Mock {
|
||||
@MockConstructor
|
||||
private BlackBox createBlackBox(String text) {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
switch (SOURCE_METHOD) {
|
||||
case "callerOne": return "mock_one";
|
||||
default: return "mock_others";
|
||||
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String staticFunc() {
|
||||
return "_MOCK_TAIL";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class)
|
||||
private String trim() {
|
||||
return "trim_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "substring")
|
||||
private String sub(int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = String.class)
|
||||
private boolean startsWith(String s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = BlackBox.class)
|
||||
private BlackBox secretBox() {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String callFromDifferentMethod() {
|
||||
if ("special_case".equals(MOCK_CONTEXT.get("case"))) {
|
||||
return "mock_special";
|
||||
}
|
||||
switch (SOURCE_METHOD) {
|
||||
case "callerOne":
|
||||
return "mock_one";
|
||||
default:
|
||||
return "mock_others";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_new_object() {
|
||||
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);
|
||||
@@ -89,13 +93,13 @@ class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_static_method() {
|
||||
assertEquals("not_secret_box", demoMock.getBox().get());
|
||||
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
|
||||
@@ -105,14 +109,13 @@ class DemoMockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_test_case_name() throws Exception {
|
||||
void should_set_mock_context() throws Exception {
|
||||
MOCK_CONTEXT.put("case", "special_case");
|
||||
// synchronous
|
||||
assertEquals("mock_special", demoMock.callerOne());
|
||||
// asynchronous
|
||||
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoMock.callerOne()).get());
|
||||
verify("callFromDifferentMethod").withTimes(2);
|
||||
MOCK_CONTEXT.clear();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,7 +1,6 @@
|
||||
package com.alibaba.testable.demo;
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
|
||||
import com.alibaba.testable.core.tool.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -11,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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.alibaba.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.demo.basic.DemoTemplate;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示模板方法的Mock场景
|
||||
* Demonstrate scenario of mocking template method
|
||||
*/
|
||||
class DemoTemplateTest {
|
||||
|
||||
private DemoTemplate demoTemplate = new DemoTemplate();
|
||||
|
||||
public static class Mock {
|
||||
/* 第一种写法:使用泛型定义 */
|
||||
/* First solution: use generics type */
|
||||
|
||||
@MockMethod
|
||||
private <T> List<T> getList(DemoTemplate self, T value) {
|
||||
return new ArrayList<T>() {{ add((T)(value.toString() + "_mock_list")); }};
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private <K, V> Map<K, V> getMap(DemoTemplate self, K key, V value) {
|
||||
return new HashMap<K, V>() {{ put(key, (V)(value.toString() + "_mock_map")); }};
|
||||
}
|
||||
|
||||
@MockConstructor
|
||||
private <T> HashSet<T> newHashSet() {
|
||||
HashSet<T> set = new HashSet<>();
|
||||
set.add((T)"insert_mock");
|
||||
return set;
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private <E> boolean add(Set s, E e) {
|
||||
s.add(e.toString() + "_mocked");
|
||||
return true;
|
||||
}
|
||||
|
||||
/* 第二种写法:使用Object类型 */
|
||||
/* Second solution: use object type */
|
||||
|
||||
//@MockMethod
|
||||
//private List<Object> getList(DemoTemplate self, Object value) {
|
||||
// return new ArrayList<Object>() {{ add(value.toString() + "_mock_list"); }};
|
||||
//}
|
||||
//
|
||||
//@MockMethod
|
||||
//private Map<Object, Object> getMap(DemoTemplate self, Object key, Object value) {
|
||||
// return new HashMap<Object, Object>() {{ put(key, value.toString() + "_mock_map"); }};
|
||||
//}
|
||||
//
|
||||
//@MockConstructor
|
||||
//private HashSet newHashSet() {
|
||||
// HashSet<Object> set = new HashSet<>();
|
||||
// set.add("insert_mock");
|
||||
// return set;
|
||||
//}
|
||||
//
|
||||
//@MockMethod
|
||||
//private boolean add(Set s, Object e) {
|
||||
// s.add(e.toString() + "_mocked");
|
||||
// return true;
|
||||
//}
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_mock_single_template_method() {
|
||||
String res = demoTemplate.singleTemplateMethod();
|
||||
assertEquals("demo_mock_list", res);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_mock_double_template_method() {
|
||||
String res = demoTemplate.doubleTemplateMethod();
|
||||
assertEquals("testable_mock_map", res);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_mock_new_template_method() {
|
||||
Set<?> res = demoTemplate.newTemplateMethod();
|
||||
assertEquals(2, res.size());
|
||||
Iterator<?> iterator = res.stream().iterator();
|
||||
assertEquals("insert_mock", iterator.next());
|
||||
assertEquals("world_mocked", iterator.next());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.demo.one2multi;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
public class ASvcMock {
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "format")
|
||||
public String a_format(String format, Object... args) {
|
||||
return "a_mock";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.demo.one2multi;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
public class BSvcMock {
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "format")
|
||||
public String b_format(String format, Object... args) {
|
||||
return "b_mock";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.demo.one2multi;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
public class CSvcMock {
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "format")
|
||||
public String c_format(String format, Object... args) {
|
||||
return "c_mock";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.alibaba.demo.one2multi;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockWith;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@MockWith
|
||||
public class OneToMultiSvcTest {
|
||||
|
||||
private ASvc aSvc = new ASvc();
|
||||
private BSvc bSvc = new BSvc();
|
||||
private CSvc cSvc = new CSvc();
|
||||
|
||||
@Test
|
||||
public void should_test_multi_class_together() {
|
||||
assertEquals("a_mock", aSvc.demo("test"));
|
||||
assertEquals("b_mock", bSvc.demo("test"));
|
||||
assertEquals("c_mock", cSvc.demo("test"));
|
||||
verify("a_format").withTimes(1);
|
||||
verify("b_format").withTimes(1);
|
||||
verify("c_format").withTimes(1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示模板方法的Mock场景
|
||||
* Demonstrate scenario of mocking template method
|
||||
*/
|
||||
class DemoTemplateTest {
|
||||
|
||||
private DemoTemplate demoTemplate = new DemoTemplate();
|
||||
|
||||
/* 第一种写法:使用泛型定义 */
|
||||
/* First solution: use generics type */
|
||||
|
||||
@MockMethod
|
||||
private static <T> List<T> getList(DemoTemplate self, T value) {
|
||||
return new ArrayList<T>() {{ add((T)(value.toString() + "_mock_list")); }};
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private static <K, V> Map<K, V> getMap(DemoTemplate self, K key, V value) {
|
||||
return new HashMap<K, V>() {{ put(key, (V)(value.toString() + "_mock_map")); }};
|
||||
}
|
||||
|
||||
@MockConstructor
|
||||
private <T> HashSet<T> newHashSet() {
|
||||
HashSet<T> set = new HashSet<>();
|
||||
set.add((T)"insert_mock");
|
||||
return set;
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private <E> boolean add(Set s, E e) {
|
||||
s.add(e.toString() + "_mocked");
|
||||
return true;
|
||||
}
|
||||
|
||||
/* 第二种写法:使用Object类型 */
|
||||
/* Second solution: use object type */
|
||||
|
||||
//@MockMethod
|
||||
//private static List<Object> getList(DemoTemplate self, Object value) {
|
||||
// return new ArrayList<Object>() {{ add(value.toString() + "_mock_list"); }};
|
||||
//}
|
||||
//
|
||||
//@MockMethod
|
||||
//private static Map<Object, Object> getMap(DemoTemplate self, Object key, Object value) {
|
||||
// return new HashMap<Object, Object>() {{ put(key, value.toString() + "_mock_map"); }};
|
||||
//}
|
||||
//
|
||||
//@MockConstructor
|
||||
//private HashSet newHashSet() {
|
||||
// HashSet<Object> set = new HashSet<>();
|
||||
// set.add("insert_mock");
|
||||
// return set;
|
||||
//}
|
||||
//
|
||||
//@MockMethod
|
||||
//private boolean add(Set s, Object e) {
|
||||
// s.add(e.toString() + "_mocked");
|
||||
// return true;
|
||||
//}
|
||||
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_single_template_method() {
|
||||
String res = demoTemplate.singleTemplateMethod();
|
||||
assertEquals("demo_mock_list", res);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_double_template_method() {
|
||||
String res = demoTemplate.doubleTemplateMethod();
|
||||
assertEquals("testable_mock_map", res);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_new_template_method() {
|
||||
Set<?> res = demoTemplate.newTemplateMethod();
|
||||
assertEquals(2, res.size());
|
||||
Iterator<?> iterator = res.stream().iterator();
|
||||
assertEquals("insert_mock", iterator.next());
|
||||
assertEquals("world_mocked", iterator.next());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "1.4.10"
|
||||
kotlin("jvm") version "1.4.10"
|
||||
}
|
||||
|
||||
group = "com.alibaba.testable"
|
||||
@@ -9,25 +9,25 @@ version = "1.0.0-SNAPSHOT"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
|
||||
testImplementation("com.alibaba.testable:testable-all:0.4.10")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.10")
|
||||
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.6.0")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.6.0")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
jvmArgs("-javaagent:${classpath.find { it.name.contains("testable-agent") }!!.absolutePath}")
|
||||
useJUnitPlatform()
|
||||
jvmArgs("-javaagent:${classpath.find { it.name.contains("testable-agent") }!!.absolutePath}")
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
@@ -1,119 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>kotlin-demo</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>kotlin-demo</name>
|
||||
<description>Demo project for TestableMock</description>
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>kotlin-demo</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>kotlin-demo</name>
|
||||
<description>Demo project for TestableMock</description>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<kotlin.version>1.3.72</kotlin.version>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.4.10</testable.version>
|
||||
</properties>
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<kotlin.version>1.3.72</kotlin.version>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.6.0</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<!-- <configuration>-->
|
||||
<!-- <argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>-->
|
||||
<!-- </configuration>-->
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare</id>
|
||||
<goals>
|
||||
<goal>prepare</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<dataFile>target/jacoco.exec</dataFile>
|
||||
<outputDirectory>target/jacoco-ut</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<build>
|
||||
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<!-- <configuration>-->
|
||||
<!-- <argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>-->
|
||||
<!-- </configuration>-->
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare</id>
|
||||
<goals>
|
||||
<goal>prepare</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<dataFile>target/jacoco.exec</dataFile>
|
||||
<outputDirectory>target/jacoco-ut</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.Box
|
||||
import com.alibaba.testable.demo.model.Color
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox
|
||||
import com.alibaba.demo.basic.model.mock.Box
|
||||
import com.alibaba.demo.basic.model.mock.Color
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* 演示对内部类的Mock支持
|
||||
* Demonstrate support for mocking invocation inside a inner class
|
||||
*/
|
||||
class DemoInnerClass {
|
||||
|
||||
class StaticInner {
|
||||
/**
|
||||
* invocation inside a static inner class
|
||||
*/
|
||||
fun demo(): String {
|
||||
return methodToBeMock()
|
||||
}
|
||||
}
|
||||
|
||||
inner class Inner {
|
||||
/**
|
||||
* invocation inside a non-static inner class
|
||||
*/
|
||||
fun demo(): String {
|
||||
return methodToBeMock()
|
||||
}
|
||||
}
|
||||
|
||||
private val executorService = Executors.newSingleThreadExecutor()
|
||||
@Throws(ExecutionException::class, InterruptedException::class)
|
||||
fun callAnonymousInner(): String {
|
||||
/**
|
||||
* invocation inside a anonymous inner class
|
||||
*/
|
||||
val future = executorService.submit<String> { methodToBeMock() }
|
||||
return future.get()
|
||||
}
|
||||
|
||||
fun callInnerDemo(): String {
|
||||
return Inner().demo()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun methodToBeMock(): String {
|
||||
return "RealCall"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
@@ -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.testable.demo
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import java.util.ArrayList
|
||||
import java.util.HashMap
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.model
|
||||
package com.alibaba.demo.basic.model.mock
|
||||
|
||||
|
||||
class BlackBox(var input: String) : Box(), Color {
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.model
|
||||
package com.alibaba.demo.basic.model.mock
|
||||
|
||||
abstract class Box {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.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
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.util
|
||||
package com.alibaba.demo.java2kotlin
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.demo.one2multi
|
||||
|
||||
class ASvc {
|
||||
|
||||
fun demo(name: String): String {
|
||||
return String.format("a_%s", name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.demo.one2multi
|
||||
|
||||
class BSvc {
|
||||
|
||||
fun demo(name: String): String {
|
||||
return String.format("b_%s", name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.demo.one2multi
|
||||
|
||||
class CSvc {
|
||||
|
||||
fun demo(name: String): String {
|
||||
return String.format("c_%s", name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
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.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
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
|
||||
*/
|
||||
internal class DemoInheritTest {
|
||||
|
||||
private val demoInherit = com.alibaba.demo.basic.DemoInherit()
|
||||
|
||||
class Mock {
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_box(self: Box, something: String) {
|
||||
self.put("put_" + something + "_into_box")
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_blackbox(self: BlackBox, something: String) {
|
||||
self.put("put_" + something + "_into_blackbox")
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_box(self: Box): String {
|
||||
return "get_from_box"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_blackbox(self: BlackBox): String {
|
||||
return "get_from_blackbox"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_color(self: Color): String {
|
||||
return "color_from_color"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_blackbox(self: BlackBox): String {
|
||||
return "color_from_blackbox"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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_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_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_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_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_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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* 演示对内部类的Mock支持
|
||||
* Demonstrate support for mocking invocation inside a inner class
|
||||
*/
|
||||
internal class DemoInnerClassTest {
|
||||
|
||||
class Mock {
|
||||
@MockMethod(targetClass = DemoInnerClass::class)
|
||||
fun methodToBeMock(): String {
|
||||
return "MockedCall"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun should_mock_invoke_inside_inner_class() {
|
||||
val demo = DemoInnerClass()
|
||||
Assertions.assertEquals("MockedCall", demo.callInnerDemo())
|
||||
Assertions.assertEquals("MockedCall", demo.callAnonymousInner())
|
||||
Assertions.assertEquals("MockedCall", DemoInnerClass.StaticInner().demo())
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.error.VerifyFailedError
|
||||
import com.alibaba.testable.core.matcher.InvokeMatcher
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.demo.basic.model.mock.BlackBox
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -14,21 +14,22 @@ import org.junit.jupiter.api.Test
|
||||
*/
|
||||
internal class DemoMatcherTest {
|
||||
|
||||
private val demoMatcher = DemoMatcher()
|
||||
private val demoMatcher = com.alibaba.demo.basic.DemoMatcher()
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithoutArgument(self: DemoMatcher) {
|
||||
class Mock {
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithoutArgument(self: com.alibaba.demo.basic.DemoMatcher) {
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArguments(self: com.alibaba.demo.basic.DemoMatcher, a1: Any, a2: Any) {
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArrayArgument(self: com.alibaba.demo.basic.DemoMatcher, a: Array<Any>) {
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArguments(self: DemoMatcher, a1: Any, a2: Any) {
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArrayArgument(self: DemoMatcher, a: Array<Any>) {
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun should_match_no_argument() {
|
||||
demoMatcher.callMethodWithoutArgument()
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD
|
||||
import com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.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
|
||||
@@ -19,70 +19,71 @@ internal class DemoMockTest {
|
||||
|
||||
private val demoMock = DemoMock()
|
||||
|
||||
@MockConstructor
|
||||
private fun createBlackBox(text: String) = BlackBox("mock_$text")
|
||||
class Mock {
|
||||
@MockConstructor
|
||||
private fun createBlackBox(text: String) = BlackBox("mock_$text")
|
||||
|
||||
@MockMethod(targetClass = DemoMock::class)
|
||||
private fun innerFunc(text: String) = "mock_$text"
|
||||
@MockMethod(targetClass = DemoMock::class)
|
||||
private fun innerFunc(text: String) = "mock_$text"
|
||||
|
||||
@MockMethod(targetClass = DemoMock::class)
|
||||
private fun staticFunc(): String {
|
||||
return "_MOCK_TAIL";
|
||||
}
|
||||
@MockMethod(targetClass = DemoMock::class)
|
||||
private fun staticFunc(): String {
|
||||
return "_MOCK_TAIL";
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = BlackBox::class)
|
||||
private fun trim() = "trim_string"
|
||||
@MockMethod(targetClass = BlackBox::class)
|
||||
private fun trim() = "trim_string"
|
||||
|
||||
@MockMethod(targetClass = BlackBox::class, targetMethod = "substring")
|
||||
private fun sub(i: Int, j: Int) = "sub_string"
|
||||
@MockMethod(targetClass = BlackBox::class, targetMethod = "substring")
|
||||
private fun sub(i: Int, j: Int) = "sub_string"
|
||||
|
||||
@MockMethod(targetClass = BlackBox::class)
|
||||
private fun startsWith(s: String) = false
|
||||
@MockMethod(targetClass = BlackBox::class)
|
||||
private fun startsWith(s: String) = false
|
||||
|
||||
@MockMethod(targetClass = BlackBox::class)
|
||||
private fun secretBox(): BlackBox {
|
||||
return BlackBox("not_secret_box")
|
||||
}
|
||||
@MockMethod(targetClass = BlackBox::class)
|
||||
private fun secretBox(): BlackBox {
|
||||
return BlackBox("not_secret_box")
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = ColorBox::class)
|
||||
private fun createBox(color: String, box: BlackBox): BlackBox {
|
||||
return BlackBox("White_${box.get()}")
|
||||
}
|
||||
@MockMethod(targetClass = ColorBox::class)
|
||||
private fun createBox(color: String, box: BlackBox): BlackBox {
|
||||
return BlackBox("White_${box.get()}")
|
||||
}
|
||||
|
||||
@MockMethod(targetClass = DemoMock::class)
|
||||
private fun callFromDifferentMethod(): String {
|
||||
return if (MOCK_CONTEXT["case"] == "special_case") {
|
||||
"mock_special"
|
||||
} else {
|
||||
when (SOURCE_METHOD) {
|
||||
"callerOne" -> "mock_one"
|
||||
else -> "mock_others"
|
||||
@MockMethod(targetClass = DemoMock::class)
|
||||
private fun callFromDifferentMethod(): String {
|
||||
return if (MOCK_CONTEXT["case"] == "special_case") {
|
||||
"mock_special"
|
||||
} else {
|
||||
when (SOURCE_METHOD) {
|
||||
"callerOne" -> "mock_one"
|
||||
else -> "mock_others"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_new_object() {
|
||||
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)
|
||||
@@ -90,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
|
||||
@@ -108,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.testable.demo
|
||||
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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo
|
||||
package com.alibaba.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
@@ -14,44 +14,45 @@ internal class DemoTemplateTest {
|
||||
|
||||
private val demoTemplate = DemoTemplate()
|
||||
|
||||
@MockMethod
|
||||
private fun <T> getList(self: DemoTemplate, value: T): List<T> {
|
||||
return mutableListOf((value.toString() + "_mock_list") as T)
|
||||
}
|
||||
class Mock {
|
||||
@MockMethod
|
||||
private fun <T> getList(self: DemoTemplate, value: T): List<T> {
|
||||
return mutableListOf((value.toString() + "_mock_list") as T)
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private fun <K, V> getMap(self: DemoTemplate, key: K, value: V): Map<K, V> {
|
||||
return mutableMapOf(key to (value.toString() + "_mock_map") as V)
|
||||
}
|
||||
@MockMethod
|
||||
private fun <K, V> getMap(self: DemoTemplate, key: K, value: V): Map<K, V> {
|
||||
return mutableMapOf(key to (value.toString() + "_mock_map") as V)
|
||||
}
|
||||
|
||||
@MockConstructor
|
||||
private fun newHashSet(): HashSet<*> {
|
||||
val set = HashSet<Any>()
|
||||
set.add("insert_mock")
|
||||
return set
|
||||
}
|
||||
@MockConstructor
|
||||
private fun newHashSet(): HashSet<*> {
|
||||
val set = HashSet<Any>()
|
||||
set.add("insert_mock")
|
||||
return set
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private fun <E> add(s: MutableSet<E>, e: E): Boolean {
|
||||
s.add((e.toString() + "_mocked") as E)
|
||||
return true
|
||||
@MockMethod
|
||||
private fun <E> add(s: MutableSet<E>, e: E): Boolean {
|
||||
s.add((e.toString() + "_mocked") as E)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_single_template_method() {
|
||||
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()
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.alibaba.demo.java2kotlin
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
|
||||
class PathUtilTest {
|
||||
|
||||
class Mock {
|
||||
@MockMethod
|
||||
fun exists(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> true
|
||||
"/a/b/c" -> true
|
||||
else -> f.exists()
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun isDirectory(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b/c" -> true
|
||||
else -> f.isDirectory
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun delete(f: File): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun listFiles(f: File): Array<File>? {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> arrayOf(File("/a/b/c"), File("/a/b/d"))
|
||||
"/a/b/c" -> arrayOf(File("/a/b/c/e"))
|
||||
else -> f.listFiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_mock_java_method_invoke_in_kotlin() {
|
||||
PathUtil.deleteRecursively(File("/a/b/"))
|
||||
verify("listFiles").withTimes(2)
|
||||
verify("delete").withTimes(4)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
|
||||
class ASvcMock {
|
||||
|
||||
@MockMethod(targetClass = String::class, targetMethod = "format")
|
||||
fun a_format(format: String, vararg args: Any?): String {
|
||||
return "a_mock"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
|
||||
class BSvcMock {
|
||||
|
||||
@MockMethod(targetClass = String::class, targetMethod = "format")
|
||||
fun b_format(format: String, vararg args: Any?): String {
|
||||
return "b_mock"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
|
||||
class CSvcMock {
|
||||
|
||||
@MockMethod(targetClass = String::class, targetMethod = "format")
|
||||
fun c_format(format: String, vararg args: Any?): String {
|
||||
return "c_mock"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.alibaba.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockWith
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@MockWith
|
||||
class OneToMultiSvcTest {
|
||||
|
||||
private val aSvc = ASvc()
|
||||
private val bSvc = BSvc()
|
||||
private val cSvc = CSvc()
|
||||
|
||||
@Test
|
||||
fun should_test_multi_class_together() {
|
||||
Assertions.assertEquals("a_mock", aSvc.demo("test"))
|
||||
Assertions.assertEquals("b_mock", bSvc.demo("test"))
|
||||
Assertions.assertEquals("c_mock", cSvc.demo("test"))
|
||||
verify("a_format").withTimes(1)
|
||||
verify("b_format").withTimes(1)
|
||||
verify("c_format").withTimes(1)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.Box
|
||||
import com.alibaba.testable.demo.model.Color
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
|
||||
*/
|
||||
internal class DemoInheritTest {
|
||||
|
||||
private val demoInherit = DemoInherit()
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_box(self: Box, something: String) {
|
||||
self.put("put_" + something + "_into_box")
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_blackbox(self: BlackBox, something: String) {
|
||||
self.put("put_" + something + "_into_blackbox")
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_box(self: Box): String {
|
||||
return "get_from_box"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_blackbox(self: BlackBox): String {
|
||||
return "get_from_blackbox"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_color(self: Color): String {
|
||||
return "color_from_color"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_blackbox(self: BlackBox): String {
|
||||
return "color_from_blackbox"
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun should_able_to_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() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
val color = demoInherit.colorViaBox
|
||||
InvokeVerifier.verify("get_color_from_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("color_from_blackbox", color)
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.alibaba.testable.demo.util
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
|
||||
class PathUtilTest {
|
||||
|
||||
@MockMethod
|
||||
fun exists(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> true
|
||||
"/a/b/c" -> true
|
||||
else -> f.exists()
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun isDirectory(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b/c" -> true
|
||||
else -> f.isDirectory
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun delete(f: File): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun listFiles(f: File): Array<File>? {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> arrayOf(File("/a/b/c"), File("/a/b/d"))
|
||||
"/a/b/c" -> arrayOf(File("/a/b/c/e"))
|
||||
else -> f.listFiles()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_java_method_invoke_in_kotlin() {
|
||||
PathUtil.deleteRecursively(File("/a/b/"))
|
||||
verify("listFiles").withTimes(2)
|
||||
verify("delete").withTimes(4)
|
||||
}
|
||||
|
||||
}
|
||||
4
docs/en-us/doc/annotations.md
Normal file
4
docs/en-us/doc/annotations.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Available Annotations
|
||||
---
|
||||
|
||||
TO BE TRANSLATED
|
||||
@@ -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.
|
||||
|
||||
4
docs/en-us/doc/design-and-mechanism.md
Normal file
4
docs/en-us/doc/design-and-mechanism.md
Normal file
@@ -0,0 +1,4 @@
|
||||
How Mock Works
|
||||
---
|
||||
|
||||
TO BE TRANSLATED
|
||||
@@ -29,45 +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. Will the mock definition still be valid when it is **indirectly called** from other test classes?
|
||||
|
||||
Equally effective, the scope of mock is the entire test runtime process.
|
||||
|
||||
For example, some private methods and external invocation in the `Aaa` class are mocked, mock method are defined in the test class `AaaTest`. When testing the `Bbb` class in another test class `BbbTest`, some mocked methods are invoked indirectly in the `Aaa` class, the actual call will also be routed to the mock method defined in the `AaaTest` class.
|
||||
|
||||
#### 7. Can `TestableMock` be used for testing Android projects?
|
||||
|
||||
It can be used in combination with [Roboelectric](https://github.com/robolectric/robolectric) testing framework.
|
||||
|
||||
The `Dalvik` and `ART` virtual machines of the Android system use a bytecode system different from the standard JVM, which will affect the normal functionality of `TestableMock`. The `Roboelectric` framework can run Android unit tests on a standard JVM virtual machine, which is much faster than running unit tests through the Android virtual machine. Recently, most Android App unit tests are written with the `Roboelectric` framework.
|
||||
|
||||
#### 8. Meet "Attempt to access none-static member in mock method" error during mocking?
|
||||
|
||||
The current design of `TestableMock` does not allow access to the non-`static` members of the test class in the mock method (because the mock method itself will be dynamically modified to the `static` type during runtime). However, some Java statements include building blocks (like `new ArrayList<String>() {{ append("data"); }}`), lambda expression (like `list.stream().map(i -> i. get)`) and so on, will generate additional member method invocations during compilation, causing mock method execution report above error.
|
||||
|
||||
The simplest solution is to declare the mock method itself as a `static` type (so that dynamically generated invocation will also be `static` to avoid the errors), for example, the original method is defined as:
|
||||
|
||||
```java
|
||||
@MockMethod
|
||||
private int getXxx(Demo self) {}
|
||||
```
|
||||
|
||||
Modify it to:
|
||||
|
||||
```java
|
||||
@MockMethod
|
||||
private static int getXxx(Demo self) {}
|
||||
```
|
||||
|
||||
In the next major iteration (**i.e. `v0.5`**), the mock implementation mechanism will be modified while maintaining the current mock experience. Then, it will be no longer necessary to modify the mock method to a static method, and completely solving this problem.
|
||||
|
||||
#### 9. Meet "Command Line is too Long. Shorten command line for ..." error when triggering test in IntelliJ IDE?
|
||||
#### 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`.
|
||||
|
||||
|
||||
4
docs/en-us/doc/javaagent-args.md
Normal file
4
docs/en-us/doc/javaagent-args.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Available Global Arguments
|
||||
---
|
||||
|
||||
TO BE TRANSLATED
|
||||
4
docs/en-us/doc/mock-method-reusing.md
Normal file
4
docs/en-us/doc/mock-method-reusing.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Reusing Mock Methods
|
||||
---
|
||||
|
||||
TO BE TRANSLATED
|
||||
@@ -3,4 +3,4 @@ Parameter constructor
|
||||
|
||||
No matter how intricate the parameter structure required by the method under test is, even there is no suitable construction method, or even there are private internal class objects... Call `TestableMock`, the parameter object will be handed to you immediately~
|
||||
|
||||
This feature is planned to be released in the `0.5.x` version.
|
||||
This feature is planned to be released in the `0.6` version.
|
||||
@@ -9,26 +9,42 @@ In addition, before unit testing begin, it is often necessary to initialize spec
|
||||
|
||||
Just add `@EnablePrivateAccess` annotation to the test class, then you have got the following enhancements in the test case:
|
||||
|
||||
- Invoke private methods (including static methods) of the class under test
|
||||
- Read private fields (including static fields) of the class under test
|
||||
- Modify private fields (including static fields) of the class under test
|
||||
- Modify the constant fields of the class under test (fields modified with final, including static fields)
|
||||
- Invoke private methods (including static methods) of the **class under test**
|
||||
- Read private fields (including static fields) of the **class under test**
|
||||
- Modify private fields (including static fields) of the **class under test**
|
||||
- Modify the constant fields of the **class under test** (fields modified with final, including static fields)
|
||||
|
||||
When accessing and modifying private and constant members, the IDE may prompt some syntax errors, but the compiler will be able to run the test normally.
|
||||
|
||||
For the effect, see the use case in the test class of the `java-demo` sample project `DemoPrivateAccessTest`. (Using compile-time code enhancement, currently only the adaptation of the Java language is implemented)
|
||||
|
||||
> This function assumes that the test class is in the same package as the class under test, and the name is `<ClassUnderTest>+Test`. When this convention is not met, you can use the `srcClass` parameter on the `@EnablePrivateAccess` annotation to specify the actual class under test. E.g:
|
||||
>
|
||||
> ```java
|
||||
> @EnablePrivateAccess(srcClass = DemoServiceImpl.class)
|
||||
> class DemoServiceTest() { ... }
|
||||
> ```
|
||||
|
||||
### Solution 2: Use the `PrivateAccessor` tool class
|
||||
|
||||
If you don't want to see the IDE's syntax error reminder, or in a non-Java language JVM project (such as Kotlin language), you can also use the `PrivateAccessor` tool class to directly access private members.
|
||||
|
||||
This class provides 6 static methods:
|
||||
This class provides 7 static methods:
|
||||
|
||||
- `PrivateAccessor.get(<ObjectUnderTest>, "<private-field-name>")` ➜ read the private field of the class under test
|
||||
- `PrivateAccessor.set(<ObjectUnderTest>, "<private-field-name>", <new-value>)` ➜ modify the private field (or constant field) of the class under test
|
||||
- `PrivateAccessor.invoke(<ObjectUnderTest>, "<private-method-name>", <call-parameters>..)` ➜ call the private method of the class under test
|
||||
- `PrivateAccessor.getStatic(<ClassUnderTest>, "<private-static-field-name>")` ➜ read the **static** private field of the class under test
|
||||
- `PrivateAccessor.setStatic(<ClassUnderTest>, "<private-static-field-name>", <new-value>)` ➜ modify the **static** private field (or **static** constant field) of the class under test
|
||||
- `PrivateAccessor.invokeStatic(<ClassUnderTest>, "<private-static-method-name>", <call-parameters>..)` ➜ call the **static** private method of the class under test
|
||||
- `PrivateAccessor.get(<AnyObject>, "<private-field-name>")` ➜ read the private field of any object
|
||||
- `PrivateAccessor.set(<AnyObject>, "<private-field-name>", <new-value>)` ➜ modify the private field (or constant field) of any object
|
||||
- `PrivateAccessor.invoke(<AnyObject>, "<private-method-name>", <call-parameters>...)` ➜ call the private method of any object
|
||||
- `PrivateAccessor.getStatic(<AnyClass>, "<private-static-field-name>")` ➜ read the **static** private field of any class
|
||||
- `PrivateAccessor.setStatic(<AnyClass>, "<private-static-field-name>", <new-value>)` ➜ modify the **static** private field (or **static** constant field) of any class
|
||||
- `PrivateAccessor.invokeStatic(<AnyClass>, "<private-static-method-name>", <call-parameters>...)` ➜ call the **static** private method of any class
|
||||
- `PrivateAccessor.construct(<AnyClass>, <constructor-parameters>...)` ➜ create a new object by the private constructor of any class
|
||||
|
||||
> Using the `PrivateAccessor` class does not require the test class to have `@EnablePrivateAccess` annotation, but adding this annotation will enable the compile-time verification for the private members of the class under test.
|
||||
|
||||
For details, see the use cases in the test classes of the `java-demo` and `kotlin-demo` sample projects `DemoPrivateAccessTest`.
|
||||
|
||||
### Compile-time verification of private members
|
||||
|
||||
Both of the above two methods essentially use JVM reflection mechanism to achieve private member access, but the JVM compiler will not check the existence of the reflection target. When the code is refactored, if the private method names and parameters in the source class are modified, it would cause exceptions to be discovered only when the unit test is triggered. For this reason, another function of the `@EnablePrivateAccess` annotation is to perform additional compile-time checks for existence of private member of the **class under test**.
|
||||
|
||||
**Note**: When the private member verification function is enabled, the `PrivateAccessor` class can only be used to access the private members of the **class under test**, which will help limit the using of the `PrivateAccessor` tool class for "unauthorized" operations unrelated to the current test. If you really need to access private members of other classes, you can remove the `@EnablePrivateAccess` annotation, or set the `verifyTargetOnCompile` parameter of the annotation to `false` to manually turn off the verification function.
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
# 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
|
||||
- fix illegal field modifiers error when add mock class to an interface
|
||||
|
||||
## 0.5.1
|
||||
- use kotlin-style method descriptor in `VERBOSE` diagnose logs
|
||||
- support parameterized test of JUnit 5
|
||||
- fix an exception caused by method parameter with ternary operator
|
||||
- fix a bug cause log message lost when `@MockWith` annotation used
|
||||
|
||||
## 0.4.12
|
||||
- support verbose diagnose log for better self-troubleshooting
|
||||
- support disable private access target existence check
|
||||
- support specify mock scanning packages
|
||||
- fix an ArrayIndexOutOfBoundsException issue when transforming native method
|
||||
|
||||
## 0.4.11
|
||||
- support accessing private members of class under test in different package path
|
||||
- validate the number of private method parameters accessed by `PrivateAccessor`
|
||||
- fix a bug which may cause errors when the mock method contains array parameters
|
||||
- fix an issue which cause some private members not be found in the IntelliJ build
|
||||
|
||||
## 0.4.10
|
||||
- fix an issue of using mock in lambda expression
|
||||
- fix the NullPointerException when invoke private method with parameter value `null`
|
||||
|
||||
4
docs/en-us/doc/scope-of-mock.md
Normal file
4
docs/en-us/doc/scope-of-mock.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Effective Scope Of Mocking
|
||||
---
|
||||
|
||||
TO BE TRANSLATED
|
||||
@@ -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.4.10</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.4.10')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.10')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -76,11 +76,13 @@ After executing the void type method under test, use `InvokeVerifier.verify()` t
|
||||
class DemoTest {
|
||||
private Demo demo = new Demo();
|
||||
|
||||
// Intercept `System.out.println` invocation
|
||||
@MockMethod
|
||||
public void println(PrintStream ps, String msg) {
|
||||
// Execute the original call
|
||||
ps.println(msg);
|
||||
public static class Mock {
|
||||
// Intercept `System.out.println` invocation
|
||||
@MockMethod
|
||||
public void println(PrintStream ps, String msg) {
|
||||
// Execute the original call
|
||||
ps.println(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -3,38 +3,92 @@ 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 {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The output log example is as follows:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
|
||||
[DIAGNOSE] Found 7 mock methods
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[DIAGNOSE] Found 6 test cases
|
||||
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[DIAGNOSE] Line 14, mock method createBlackBox used
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[DIAGNOSE] Line 22, mock method innerFunc used
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
[DIAGNOSE] Line 27, mock method "staticFunc" used
|
||||
[DIAGNOSE] Handling method commonFunc
|
||||
[DIAGNOSE] Line 29, mock method trim used
|
||||
[DIAGNOSE] Line 29, mock method sub used
|
||||
[DIAGNOSE] Line 29, mock method startsWith used
|
||||
[DIAGNOSE] Handling method getBox
|
||||
[DIAGNOSE] Line 36, mock method secretBox used
|
||||
[DIAGNOSE] Handling method callerOne
|
||||
[DIAGNOSE] Line 43, mock method callFromDifferentMethod used
|
||||
[DIAGNOSE] Handling method callerTwo
|
||||
[DIAGNOSE] Line 47, mock method callFromDifferentMethod used
|
||||
[DIAGNOSE] Handling method innerFunc
|
||||
[DIAGNOSE] Handling method callFromDifferentMethod
|
||||
[DIAGNOSE] Line 34, mock method "trim" used
|
||||
[DIAGNOSE] Line 34, mock method "sub" used
|
||||
[DIAGNOSE] Line 34, mock method "startsWith" used
|
||||
... ...
|
||||
```
|
||||
|
||||
The log shows all the mocked invocation and corresponding code line numbers in the class under test.
|
||||
|
||||
- Self troubleshooting:
|
||||
|
||||
- If there is no output, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies
|
||||
|
||||
- If only the first line of `Handling 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 `LogLevel.VERBOSE` for further investigation information.
|
||||
|
||||
```java
|
||||
class DemoTest {
|
||||
@MockDiagnose(LogLevel.VERBOSE)
|
||||
public static class Mock {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Executing the unit test again will print out the signatures of all mock methods, and the signatures of all invocations scanned in the class under test:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[VERBOSE] Test case "should_mock_new_object"
|
||||
... ...
|
||||
[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.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;"
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[VERBOSE] Line 13, constructing "java.lang.Object()"
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[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.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"
|
||||
[VERBOSE] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
... ...
|
||||
```
|
||||
|
||||
The logs are formatted in follow pattern:
|
||||
|
||||
- `Mock constructor "<MockMethodName>" as "<Signature>" for "<TypeName>"` Mock constructor found in test class
|
||||
- `Mock method "<MockMethodName>" as "<Signature>"` Mock method found in test class (the first parameter that identify the mock target class is currently kept)
|
||||
- `Line XX, constructing "<TypeName>" as "<Signature>"` Constructor invocation found in test under class
|
||||
- `Line XX, invoking "<MethodName>" as "<Signature>"` Member method invocation found in test under class
|
||||
|
||||
67
docs/en-us/doc/upgrade-guide.md
Normal file
67
docs/en-us/doc/upgrade-guide.md
Normal file
@@ -0,0 +1,67 @@
|
||||
Upgrade Guide
|
||||
---
|
||||
|
||||
### 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, 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:
|
||||
|
||||
```java
|
||||
public class DemoMockTest {
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "hello_" + text;
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After upgrading to the `0.5` version, move all mock methods (in this example, only the `innerFunc` method) to a static inner class named `Mock`, which is equivalent to adding two lines of code:
|
||||
|
||||
```java
|
||||
public class DemoMockTest {
|
||||
|
||||
public static class Mock { // Add this line
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "hello_" + text;
|
||||
}
|
||||
} // Add this line
|
||||
|
||||
@Test
|
||||
void should_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally, upgrade the `TestableMock` dependency in `pom.xml` or `build.gradle` file to the new version.
|
||||
@@ -5,10 +5,6 @@ Use TestableMock In IDE
|
||||
|
||||
IntelliJ IDE supports the `JSR-269` annotation processor and the `maven-surefire-plugin` arguments very well (both are techniques back the `TestableMock`). Usually you don't need any special configuration to make everything work, it's all out of the box.
|
||||
|
||||
> In IntelliJ 2020.3 and later versions, its built-in annotation processor had some parameter types changed, which no longer consistent with the standard `Maven` compilation process. In the version of `TestableMock` lower than `0.4.5`, you will encounter the problem that the `@EnablePrivateAccess` annotation does not take effect. You can turn on "Delegate IDE build/run actions to maven" option in "Build Tools > Maven > Runner" of the system configuration to solve the problem.
|
||||
>
|
||||
> 
|
||||
|
||||
## Use Eclipse IDE
|
||||
|
||||
Since the built-in compilation feature of `Eclipse` is based on a self-made compiler, it is not compatible with the standard `javac` compilation process, which will cause the `@EnablePrivateAccess` annotation to be invalid when running test cases in the IDE. However, the function of accessing the private members of the class under test through the `PrivateAccessor` tool class will not be affected by differences in the compiler.
|
||||
|
||||
4
docs/en-us/doc/use-mock-with.md
Normal file
4
docs/en-us/doc/use-mock-with.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Use MockWith Annotation
|
||||
---
|
||||
|
||||
TO BE TRANSLATED
|
||||
@@ -4,18 +4,32 @@ Fast Mocking
|
||||
Compared with the class-granularity mocking practices of existing mock tools, `TestableMock` allows developers to directly define a single method and use it for mocking. With the principle of convention over configuration, mock method replacement will automatically happen when the specified method in the test class match an invocation in the class under test.
|
||||
|
||||
> In summary, there are two simple rules:
|
||||
> - Mock non-constructive method, copy the original method definition to the test class, add a `@MockMethod` annotation
|
||||
> - Mock construction method, copy the original method definition to the test class, replace the return value with the constructed type, the method name is arbitrary, and add a `@MockContructor` annotation
|
||||
> - Mock non-constructive method, copy the original method definition to the mock class, add a `@MockMethod` annotation
|
||||
> - Mock construction method, copy the original method definition to the mock class, replace the return value with the constructed type, the method name is arbitrary, and add a `@MockContructor` annotation
|
||||
|
||||
The detail mock method definition convention is as follows:
|
||||
The detail mock method definition convention is as follows.
|
||||
|
||||
#### 1. Mock method calls of any class
|
||||
### 0. Pre-step, prepare the mock class
|
||||
|
||||
Define an ordinary method annotated with `@MockMethod` in the test class with exactly the same signature (name, parameter, and return value type) as the method to be mocked, and then add the type of target object (which the method originally belongs to) as `targetMethod` parameter of `@MockMethod` annotation.
|
||||
First, create a mock class as the container for mock methods associated with the test class. The simplest way is to add a static inner class named `Mock` to the test class. E.g:
|
||||
|
||||
```java
|
||||
public class DemoTest {
|
||||
|
||||
public static class Mock {
|
||||
// mock methods goes here
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### 1.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.
|
||||
|
||||
At this time, all invocations to that original method in the class under test will be automatically replaced with invocations to the above-mentioned mock method when the unit test is running.
|
||||
|
||||
For example, there is a call to `"anything".substring(1, 2)` in the class under test, and we want to change it to a fixed string when running the test, we only need to define the following method in the test class:
|
||||
For example, there is a call to `"anything".substring(1, 2)` in the class under test, and we want to change it to a fixed string when running the test, we only need to define the following method in the mock class:
|
||||
|
||||
```java
|
||||
// The original method signature is `String substring(int, int)`
|
||||
@@ -53,15 +67,15 @@ 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.
|
||||
|
||||
The solution is the same as the previous case. Just set `targetClass` parameter value to the type of class under test.
|
||||
|
||||
For example, there is a private method with the signature `String innerFunc(String)` in the class under test. If we want to replace it during testing, we only need to define the following method in the test class:
|
||||
For example, there is a private method with the signature `String innerFunc(String)` in the class under test. If we want to replace it during testing, we only need to define the following method in the mock class:
|
||||
|
||||
```java
|
||||
// The type to test is `DemoMock`
|
||||
@@ -73,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.
|
||||
|
||||
@@ -88,11 +102,11 @@ 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 test class, make the return value type of the method the type of the object to be created, and the method parameters are exactly the same as the constructor parameters to be mocked, the method name is arbitrary.
|
||||
Define an ordinary method annotated with `@MockContructor` in the mock class, make the return value type of the method the type of the object to be created, and the method parameters are exactly the same as the constructor parameters to be mocked, the method name is arbitrary.
|
||||
|
||||
At this time, all operations in the class under test that use `new` to create the specified class (and use the constructor that is consistent with the mock method parameters) will be replaced with calls to the custom method.
|
||||
|
||||
@@ -109,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 the current test case and invoke source
|
||||
### 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.
|
||||
|
||||
@@ -124,7 +138,6 @@ public void testDemo() {
|
||||
assertEquals(true, demo());
|
||||
MOCK_CONTEXT.set("case", "has-error");
|
||||
assertEquals(false, demo());
|
||||
MOCK_CONTEXT.clear();
|
||||
}
|
||||
```
|
||||
|
||||
@@ -144,24 +157,11 @@ private Data mockDemo() {
|
||||
}
|
||||
```
|
||||
|
||||
Note that because `TestableMock` does not (and won't to) rely on any specific test framework, it cannot automatically identify the end position of a single test case, which makes the parameters set to the `TestableTool.MOCK_CONTEXT` variable may exist cross test cases in the same test class. It is recommended to always use `MOCK_CONTEXT.clear()` to clear the context immediately after use. You can also add this statement to the unified position where the test case ends of the specific unit test framework, such as the `@AfterEach` method of JUnit 5.
|
||||
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.
|
||||
|
||||
In the current version, the effect of this variable at runtime is similar to a normal `Map` type member object in the test class, but please try to use this variable instead of a custom object to pass additional mock parameters in order to get better compatibility in the upcoming`v0.5` version.
|
||||
|
||||
> The `TestableTool.MOCK_CONTEXT` variable is currently shared within the test class. When the unit test runs in parallel, it is recommended to select the `parallel` type as `classes`
|
||||
|
||||
For complete code examples, see the `should_able_to_get_source_method_name()` and `should_able_to_get_test_case_name()` test cases in the `java-demo` and `kotlin-demo` sample projects.
|
||||
|
||||
#### 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.
|
||||
|
||||
For details, please refer to the [Check Mock Call](en-us/doc/matcher.md) document.
|
||||
|
||||
#### Additional note
|
||||
|
||||
> **Mock convention in version 0.4.x**:
|
||||
> - The name of the test class must be `<NameOfClassUnderTest> + Test` (and in the same package path), which is usually the by-default naming convention of Java project managed by `Maven` or `Gradle`.
|
||||
> - Do NOT access any non-`static` members in mock methods. Currently, methods that is decorated by `@MockMethod` or `@MockContructor` annotations will be automatically modified to `static` methods during runtime.
|
||||
>
|
||||
> These constraints will be removed in `0.5` versions of `TestableMock`.
|
||||
|
||||
@@ -2,17 +2,24 @@
|
||||
- [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)
|
||||
- [Effective Scope Of Mocking](zh-cn/doc/scope-of-mock.md)
|
||||
- [Reusing Mock Methods](zh-cn/doc/mock-method-reusing.md)
|
||||
- [Use MockWith Annotation](zh-cn/doc/use-mock-with.md)
|
||||
- [Frequently Asked Questions](en-us/doc/frequently-asked-questions.md)
|
||||
- [Use TestableMock In IDE](en-us/doc/use-in-ide.md)
|
||||
- [Self-Help Troubleshooting](en-us/doc/troubleshooting.md)
|
||||
- [Testable Maven Plugin](en-us/doc/use-maven-plugin.md)
|
||||
|
||||
- Technical Reference
|
||||
- [Upgrade 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)
|
||||
- [Mock Tools Comparison](en-us/doc/comparation.md)
|
||||
- [Release Note](en-us/doc/release-note.md)
|
||||
- [About Us](en-us/doc/about-us.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工具**。
|
||||
|
||||
|
||||
60
docs/zh-cn/doc/annotations.md
Normal file
60
docs/zh-cn/doc/annotations.md
Normal file
@@ -0,0 +1,60 @@
|
||||
注解参数清单
|
||||
---
|
||||
|
||||
基于轻量的原则,`TestableMock`为开发者提供了尽可能精炼、易用的注解组合,以下参数信息可供开发参考。
|
||||
|
||||
#### @EnablePrivateAccess
|
||||
|
||||
启用对被测类的<u>私有成员访问编译期增强</u>和<u>私有目标存在性的编译期校验</u>功能。
|
||||
|
||||
- 作用于:测试类
|
||||
|
||||
| 参数 | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| srcClass | Class | 否 | N/A | 当测试类命名不符合约定时,指定实际被测类 |
|
||||
| verifyTargetOnCompile | boolean | 否 | true | 是否启用私有目标的编译期存在性校验 |
|
||||
|
||||
#### @MockMethod
|
||||
|
||||
将当前方法标识为待匹配的Mock成员方法。
|
||||
|
||||
- 作用于:Mock容器类中的方法
|
||||
|
||||
| 参数 | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| targetClass | Class | 否 | N/A | 指定Mock目标的调用者类型 |
|
||||
| targetMethod | String | 否 | N/A | 指定Mock目标的方法名 |
|
||||
| scope | MockScope | 否 | MockScope.GLOBAL | 指定Mock的生效范围 |
|
||||
|
||||
#### @MockConstructor
|
||||
|
||||
将当前方法标识为待匹配的Mock构造方法。
|
||||
|
||||
- 作用于:Mock容器类中的方法
|
||||
|
||||
| 参数 | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| scope | MockScope | 否 | MockScope.GLOBAL | 指定Mock的生效范围 |
|
||||
|
||||
|
||||
#### @MockWith
|
||||
|
||||
显式指定当前类型关联的Mock容器类。
|
||||
|
||||
- 作用于:测试类、被测类
|
||||
|
||||
| 参数(`N/A`为默认参数) | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| N/A | Class | 否 | NullType.class | 指定使用的Mock容器类 |
|
||||
| treatAs | ClassType | 否 | ClassType.GuessByName | 指定当前类是测试类或被测类 |
|
||||
|
||||
#### @MockDiagnose
|
||||
|
||||
启用或禁止Mock相关的诊断信息输出。
|
||||
|
||||
- 作用于:Mock容器类
|
||||
|
||||
| 参数(`N/A`为默认参数) | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| N/A | LogLevel | 是 | N/A | 指定当前Mock容器关联测试用例的诊断日志级别 |
|
||||
|
||||
@@ -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编写习惯会带来一定改变。
|
||||
|
||||
29
docs/zh-cn/doc/design-and-mechanism.md
Normal file
29
docs/zh-cn/doc/design-and-mechanism.md
Normal file
@@ -0,0 +1,29 @@
|
||||
TestableMock的设计和原理
|
||||
---
|
||||
|
||||
这篇文档主要介绍`TestableMock`中Mock功能的设计思想和实现原理。
|
||||
|
||||
与常见的Mock工具在每个测试用例里写Mock定义不同,`TestableMock`让每个业务类直接提供自己的Mock方法集合,描述自身在测试时需要被Mock的调用以及相应替代逻辑(即每个业务类有自己的独立Test类和独立Mock类)。采用约定优于配置,降低Mock学习理解成本、减少冗余信息。
|
||||
|
||||
这种设计基于两项基本假设:
|
||||
|
||||
1. 同一个测试类里,一个测试用例里需要Mock掉的方法,在其他测试用例里通常也都需要Mock。因为这些被Mock的方法往往访问了不便于测试的外部依赖。
|
||||
2. 需要Mock的调用都来自被测类的代码。(此假设是符合单元测试初衷的,即单元测试只应该关注当前单元的内部行为,单元外的逻辑应该被替换为Mock)
|
||||
|
||||
据此通过约定来简化符合该假设的单元测试场景,通过配置来支持其余复杂的使用场景。
|
||||
|
||||
`TestableMock`的原理可以用一句话概括:<u>利用JavaAgent动态修改字节码,把被测的业务类中与所有与Mock方法定义匹配的调用在单元测试运行时替换成对Mock方法的调用</u>。
|
||||
|
||||
最终达到的效果则是,不论代码用什么服务框架、什么对象容器,不论要Mock的目标对象是注入的、new出来的、全局的还是局部的,不论要Mock的目标方法是私有的、外部的、静态的、继承来的或者重载过的,全部无差别通吃,让单元测试回归简单。
|
||||
|
||||
> 划重点:Mock的目标是**被测类**中的**方法调用**。测试用例里的代码不会被Mock,方法的定义本身没有变化,只是发起调用的代码被替换了。
|
||||
|
||||
具体来说,在单元测试启动时,`TestableMock`会对加载到内存中的类进行预处理,同时分别建立“被测类”、“测试类”、“Mock容器类”之间的关联关系(可以是一对一,也可以是多对一)。这个关联一方面是为了在测试用例执行时能够正确匹配Mock调用并进行替换,另一方面则是为了能控制Mock方法的生效范围。
|
||||
|
||||
对于被测类,将匹配到的调用换成对Mock容器方法的调用。
|
||||
|
||||
对于测试类,在每个测试用例开头插入Mock上下文初始化代码。
|
||||
|
||||
对于Mock容器类,增加`testableIns()`方法变成单例类,在每个Mock方法开头插入记录调用的代码。
|
||||
|
||||
以上是整个Mock的核心逻辑,更多实现细节,请参考源码。若有任何问题、建议、改进提议,都欢迎通过Github Issue和Pull Request参与讨论、贡献😃
|
||||
@@ -1,9 +1,13 @@
|
||||
常见使用问题
|
||||
---
|
||||
|
||||
#### 1. 如何Mock被测类中通过`@Autowired`初始化的字段?
|
||||
#### 1. 如何初始化被测类中通过`@Autowired`或`@Resource`注入的私有字段?
|
||||
|
||||
直接创建被测类对象,然后利用`TestableMock`访问私有成员的能力直接给这些字段赋值即可。
|
||||
若该对象的方法在测试时需要被Mock,则无需初始化。
|
||||
|
||||
若测试运行时需用到该对象的真实调用,则可以在测试类的构造方法内直接创建对象,然后利用`TestableMock`访问私有成员的能力给这些字段赋值。
|
||||
|
||||
对于JUnit框架,还可以使用`@RunWith(SpringRunner.class)`注解将Spring上下文启动起来,然后在测试类里用`@Resource`把需要依赖对象注入进来,再利用`PrivateAccessor`赋值给被测类的私有字段。
|
||||
|
||||
#### 2. `TestableMock`是否能够与其他Mock工具一起使用?
|
||||
|
||||
@@ -29,45 +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. 当被Mock的方法被其它测试类**间接调用**时依然有效吗?
|
||||
|
||||
同样有效,Mock的作用范围是整个测试运行过程。
|
||||
|
||||
例如测试类`AaaTest`中Mock了`Aaa`类的某些私有方法(或者某些外部方法调用);在另一个测试类`BbbTest`中测试`Bbb`类时,某些方法间接用到了`Aaa`类被Mock过的方法或调用,此时实际调用的同样会是`AaaTest`类中定义的Mock方法。
|
||||
|
||||
#### 7. `TestableMock`能否用于Android项目的测试?
|
||||
|
||||
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。
|
||||
|
||||
Android系统的`Dalvik`和`ART`虚拟机采用了与标准JVM不同的字节码体系,会影响`TestableMock`的正常工作。`Roboelectric`框架能在普通JVM虚拟机上运行Android单元测试,其速度比通过Android虚拟机运行单元测试快非常多,绝大多数Android App的单元测试都在使用`Roboelectric`框架。
|
||||
|
||||
#### 8. 使用Mock时候遇到"Attempt to access none-static member in mock method"错误?
|
||||
|
||||
当前`TestableMock`的设计不允许在Mock方法中访问测试类的非`static`成员(因为Mock方法自身会在运行期被动态修改为`static`类型)。然而有些Java语句,包括构造块(譬如`new ArrayList<String>() {{ append("data"); }}`)、匿名函数(譬如`list.stream().map(i -> i.get)`)等等,会在编译过程中生成额外的成员方法调用,导致Mock方法执行报错。
|
||||
|
||||
最简单的解决办法是将Mock方法本身也声明为`static`类型(这样动态生成的调用也会是`static`的,避免了以上错误),例如原方法定义为:
|
||||
|
||||
```java
|
||||
@MockMethod
|
||||
private int getXxx(Demo self) {}
|
||||
```
|
||||
|
||||
将其修改为:
|
||||
|
||||
```java
|
||||
@MockMethod
|
||||
private static int getXxx(Demo self) {}
|
||||
```
|
||||
|
||||
在下一个大迭代版本(**即`0.5`版本**)中,将会在保持当前Mock体验的前提下,对Mock的实现机制进行修改,不再需要修改Mock方法为静态方法,从而彻底解决此类报错问题。
|
||||
|
||||
#### 9. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
|
||||
#### 8. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
|
||||
|
||||
这个问题是由于系统ClassPath包含太多路径所致,与是否使用`TestableMock`无关。但需要注意的是,IntelliJ提供了两种辅助解决机制:`JAR manifest`和`classpath file`,若测试中使用了`TestableMock`,请选择`JAR manifest`。
|
||||
|
||||
|
||||
45
docs/zh-cn/doc/javaagent-args.md
Normal file
45
docs/zh-cn/doc/javaagent-args.md
Normal file
@@ -0,0 +1,45 @@
|
||||
全局运行参数
|
||||
---
|
||||
|
||||
`TestableMock`的许多功能采用了基于JavaAgent的运行时字节码修改实现。在JavaAgent启动时,可以通过额外的全局参数来调整`TestableMock`的执行过程。
|
||||
|
||||
### 可用参数清单
|
||||
|
||||
| 参数 | 描述 | 可用值和示例(`N/A`表示无需赋值) |
|
||||
| ---- | ---- | ---- |
|
||||
| logLevel | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
|
||||
| dumpPath | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 例如 `/tmp/bytecode`(需要此目录事先存在) |
|
||||
| pkgPrefix | 限定`TestableMock`仅对部分包生效 | 使用`,`分隔的包路径前缀列表,例如 `com.demo.svc,com.demo.dao` |
|
||||
| mockScope | 修改默认的Mock生效范围(详见[Mock生效范围](zh-cn/doc/scope-of-mock)) | 可用值为:`global`(全局生效) / `associated`(只对关联的测试用例生效) |
|
||||
| useThreadPool | 启用基于`TransmittableThreadLocal`的Mock上下文存储(用于包含线程池的测试用例) | `N/A` |
|
||||
|
||||
### 参数的连接
|
||||
|
||||
若参数有值,参数名和值之间用`=`符合连接。例如:
|
||||
|
||||
`useThreadPool`、`logLevel=debug`、`dumpPath=/tmp/debug`
|
||||
|
||||
多个参数之间使用`&`符号连接,例如:
|
||||
|
||||
`useThreadPool&logLevel=debug`、`logLevel=debug&dumpPath=/tmp/debug`
|
||||
|
||||
### 添加运行参数
|
||||
|
||||
与其他基于JavaAgent的工具相似,`TestableMock`通过在单元测试引入`testable-agent`包的末尾加上一个`=`符号,然后连接额外参数来传递用户的自定义参数。
|
||||
|
||||
对于Maven项目,可将参数追加到`maven-surefire-plugin`参数`testable-agent`包尾部,紧接着`.jar`的位置。例如:
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar=mockScope=associated&pkgPrefix=com.demo.</argLine>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
> 注意:在`xml`文件中,连接参数的`&`符号需要写为`&`
|
||||
|
||||
对于Gradle项目,同样是直接将参数追加到引入`testable-agent`的配置末尾。例如:
|
||||
|
||||
|
||||
```groovy
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}=mockScope=associated&pkgPrefix=com.demo."
|
||||
```
|
||||
63
docs/zh-cn/doc/mock-method-reusing.md
Normal file
63
docs/zh-cn/doc/mock-method-reusing.md
Normal file
@@ -0,0 +1,63 @@
|
||||
复用Mock类与方法
|
||||
---
|
||||
|
||||
Don't Repeat Yourself是软件开发过程当中的一项重要原则(即“DRY原则”),在编写测试代码时,有些通用的基础功能调用语句常常出现在许多相似的业务类里,若测试中需要Mock这些调用,就要在各个测试类中重复提供同样的Mock方法。而通过Mock方法的复用机制,能够很好的避免编写臃肿重复Mock代码的麻烦。
|
||||
|
||||
TestableMock支持两种粒度的Mock复用方式:<u>复用Mock类</u>和<u>复用Mock方法</u>。
|
||||
|
||||
## 复用Mock类
|
||||
|
||||
如果有两个或以上测试类需要Mock的方法近乎相同,那么采用类级别的Mock复用就是最省心的一种方式。
|
||||
|
||||
进行类级别的Mock复用,只需将Mock容器定义为独立的类,然后在要使用它的测试类上通过`@MockWith`进行引用。例如:
|
||||
|
||||
|
||||
```java
|
||||
@MockWith(ServiceMock.class)
|
||||
public class AaaServiceTest {
|
||||
...
|
||||
}
|
||||
|
||||
@MockWith(ServiceMock.class)
|
||||
public class BbbServiceTest {
|
||||
...
|
||||
}
|
||||
|
||||
public class ServiceMock {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
这样在`AaaServiceTest`和`BbbServiceTest`类中的测试用例在执行时,都会用`ServiceMock`容器类中定义的Mock方法进行调用匹配和Mock替换。
|
||||
|
||||
## 复用Mock方法
|
||||
|
||||
实际场景中,相比一次性复用整个Mock类的情况,更常见的是对部分高频Mock方法进行复用。
|
||||
|
||||
Mock方法的复用可以通过Mock容器类的继承来实现,父类中定义的所有Mock方法都会在子类中自然存在,例如:
|
||||
|
||||
```java
|
||||
public class AaaServiceTest {
|
||||
public static class Mock extends BasicMock {
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
public class BbbServiceTest {
|
||||
public static class Mock extends BasicMock {
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
public class BasicMock {
|
||||
@MockMethod(targetClass = UserDao.class)
|
||||
protected String getById(int id) {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
则名为`getById`的Mock方法在`AaaServiceTest`和`BbbServiceTest`的测试用例执行时都会生效。
|
||||
|
||||
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.5.x`版本中推出。
|
||||
@@ -5,30 +5,50 @@
|
||||
|
||||
此外,在单元测试中时常会需要对被测对象进行特定的成员字段初始化,但有时由于被测类的构造方法限制,使得无法便捷的对这些字段进行赋值。那么,能否在不破坏被测类型封装的情况下,允许单元测试用例内的代码直接访问被测类的私有方法和成员字段呢?`TestableMock`提供了两种简单的解决方案。
|
||||
|
||||
### 方法一:使用`@EnablePrivateAccess`注解
|
||||
### 1.1 使用`PrivateAccessor`工具类
|
||||
|
||||
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
|
||||
第一种方法是借助`PrivateAccessor`工具类来直接访问私有成员。这个类提供7个静态方法:
|
||||
|
||||
- 调用被测类的私有方法(包括静态方法)
|
||||
- 读取被测类的私有字段(包括静态字段)
|
||||
- 修改被测类的私有字段(包括静态字段)
|
||||
- 修改被测类的常量字段(使用final修饰的字段,包括静态字段)
|
||||
- `PrivateAccessor.get(任意对象, "私有字段名")` ➜ 读取任意类的私有字段
|
||||
- `PrivateAccessor.set(任意对象, "私有字段名", 新的值)` ➜ 修改任意类的私有字段(或常量字段)
|
||||
- `PrivateAccessor.invoke(任意对象, "私有方法名", 调用参数...)` ➜ 调用任意类的私有方法
|
||||
- `PrivateAccessor.getStatic(任意类型, "私有静态字段名")` ➜ 读取任意类的**静态**私有字段
|
||||
- `PrivateAccessor.setStatic(任意类型, "私有静态字段名", 新的值)` ➜ 修改任意类的**静态**私有字段(或**静态**常量字段)
|
||||
- `PrivateAccessor.invokeStatic(任意类型, "私有静态方法名", 调用参数...)` ➜ 调用任意类的**静态**私有方法
|
||||
- `PrivateAccessor.construct(任意类型, 构造方法参数...)` ➜ 调用任意类的私有构造方法
|
||||
|
||||
访问和修改私有、常量成员时,IDE可能会提示语法有误,但编译器将能够正常运行测试。(使用编译期代码增强,目前仅实现了Java语言的适配)
|
||||
详见`java-demo`和`kotlin-demo`示例项目`DemoPrivateAccessorTest`测试类中的用例。
|
||||
|
||||
效果见`java-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
|
||||
### 1.2 防代码重构机制
|
||||
|
||||
### 方法二:使用`PrivateAccessor`工具类
|
||||
本质上来说,`PrivateAccessor`只是JVM反射机制的“易用型”封装,因此会存在与JVM反射相同的“对代码重构不友好”问题。当被测类中的私有方法名称、参数进行了修改,由于IDE无法自动订正反射访问的代码,往往相关错误要在单元测试运行时才能被发现。
|
||||
|
||||
若不希望看到IDE的语法错误提醒,或是在非Java语言的JVM工程(譬如Kotlin语言)里,也可以借助`PrivateAccessor`工具类来直接访问私有成员。
|
||||
为此,`TestableMock`对`PrivateAccessor`进行了增强,赋予其编译期私有成员校验能力。这项功能默认关闭,需要通过`@EnablePrivateAccess`注解开启。(实际上是通过该注解的`verifyTargetOnCompile`参数控制,由于此参数默认值为`true`,因此只需在被测类上添加该注解即可启用私有成员校验)
|
||||
|
||||
这个类提供了6个静态方法:
|
||||
**注意 1.**:当私有成员校验功能开启时,`PrivateAccessor`工具类将只能用于访问**被测类**的私有成员,从某种角度而言,这也有助于限制将`PrivateAccessor`工具类用于与当前测试无关的“越权”操作。
|
||||
|
||||
- `PrivateAccessor.get(被测对象, "私有字段名")` ➜ 读取被测类的私有字段
|
||||
- `PrivateAccessor.set(被测对象, "私有字段名", 新的值)` ➜ 修改被测类的私有字段(或常量字段)
|
||||
- `PrivateAccessor.invoke(被测对象, "私有方法名", 调用参数..)` ➜ 调用被测类的私有方法
|
||||
- `PrivateAccessor.getStatic(被测类型, "私有静态字段名")` ➜ 读取被测类的**静态**私有字段
|
||||
- `PrivateAccessor.setStatic(被测类型, "私有静态字段名", 新的值)` ➜ 修改被测类的**静态**私有字段(或**静态**常量字段)
|
||||
- `PrivateAccessor.invokeStatic(被测类型, "私有静态方法名", 调用参数..)` ➜ 调用被测类的**静态**私有方法
|
||||
**注意 2.**:`TestableMock`默认约定测试类与**被测类**的包路径相同,且名称为`被测类+Test`。若测试类名称不符合此约定时,在使用`@EnablePrivateAccess`注解时,需用`srcClass`参数显式指明实际的**被测类**位置。
|
||||
|
||||
详见`java-demo`和`kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
|
||||
**注意 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,40 @@
|
||||
# Release Note
|
||||
|
||||
## 0.6.0
|
||||
- 增加`OmniConstructor`和`OmniAccessor`,支持快速构建方法入参
|
||||
- 修复一处`FRAME FULL`字节码处理的异常 (issue-117)
|
||||
- 移除`@MockWith`的`diagnose`参数支持
|
||||
|
||||
## 0.5.2
|
||||
- 支持使用`PrivateAccessor`访问父类中的私有成员 (issue-91)
|
||||
- 修复在较高版本JVM下的非法类型错误 (issue-112)
|
||||
- 修复Mock目标为接口类型时的字节码异常 (issue-82)
|
||||
|
||||
## 0.5.1
|
||||
- 在`VERBOSE`级别诊断日志中使用更易于阅读的方法签名格式
|
||||
- 增加对JUnit5参数化测试的支持 (issue-98)
|
||||
- 修复在调用参数中包含三元表达式时的Mock异常(issue-92)
|
||||
- 修复使用`@MockWith`时,日志输出丢失的BUG (issue-99)
|
||||
|
||||
## 0.5.0
|
||||
- 分离测试类与Mock类,实现Mock类和Mock方法的复用机制
|
||||
- 支持测试类与被测类在不同包路径的情况下实施Mock
|
||||
- 支持将Mock方法生效范围缩小为所属测试类的测试用例
|
||||
- 使用`TransmittableThreadLocal`替换基于线程堆栈的Mock上下文识别机制
|
||||
- 增加专用于输出诊断信息的`@MockDiagnose`注解
|
||||
|
||||
## 0.4.12
|
||||
- 支持`VERBOSE`级别的Mocking过程日志,增强错误自助排查能力
|
||||
- 支持使用`verifyTargetOnCompile`参数禁用编译期私有目标校验功能
|
||||
- 支持通过agent参数指定Mock目标的扫描包范围
|
||||
- 修复一处`ArrayIndexOutOfBoundsException`异常 (issue-52)
|
||||
|
||||
## 0.4.11
|
||||
- 支持测试类访问与自身包路径不同的被测类的私有成员
|
||||
- 增加`PrivateAccessor`访问的私有方法参数数目检查,提高抗代码重构能力
|
||||
- 修复被Mock方法包含数组参数可能导致出错的BUG (issue-48)
|
||||
- 修复一处会导致在IntelliJ中构建找不到私有成员的问题
|
||||
|
||||
## 0.4.10
|
||||
- 修复在Lambda函数中使用Mock出错的BUG(issue-44)
|
||||
- 修复调用私有方法时参数值不能为null的问题(issue-27)
|
||||
|
||||
27
docs/zh-cn/doc/scope-of-mock.md
Normal file
27
docs/zh-cn/doc/scope-of-mock.md
Normal file
@@ -0,0 +1,27 @@
|
||||
Mock的生效范围
|
||||
---
|
||||
|
||||
在`@MockMethod`和`@MockConstructor`注解上都有一个`scope`参数,其可选值有两种
|
||||
|
||||
- `MockScope.GLOBAL`:该Mock方法将全局生效
|
||||
- `MockScope.ASSOCIATED`:该Mock方法仅对Mock容器关联测试类中的测试用例生效
|
||||
|
||||
举例来说,`AaaService`和`BbbService`是两个需要被测试的类,在`BbbService`的代码里有个`recordTicket()`调用依赖外部系统。因此在进行单元测试时,开发者在`BbbService`关联的Mock容器里使用`@MockMethod`注解定义了这个调用的替代方法。此时,若该Mock方法的`scope`值为`MockScope.GLOBAL`,则不论是在`AaaServiceTest`测试类还是在`BbbServiceTest`测试类的测试用例,只要直接或间接的执行到这行调用,都会被置换为调用Mock方法。若该Mock方法的`scope`值为`MockScope.ASSOCIATED`,则Mock只对`BbbServiceTest`类中的测试用例生效,而`AaaServiceTest`类中的测试用例在运行过程中执行到了`BbbService`类的相关代码,将会执行`recordTicket()`的原本调用。
|
||||
|
||||
对于常规项目而言,单元测试里需要被Mock的调用都是由于其中包含了不需要或不便于测试的逻辑,譬如“依赖外部系统”、“包含随机结果”、“执行非常耗时”等等,这类调用在整个单元测试的生命周期里都应该被Mock方法置换,不论调用的发起者是谁。因此`TestableMock`默认所有Mock方法都是全局生效的,即`scope`默认值为`MockScope.GLOBAL`。
|
||||
|
||||
在一些大型项目中,会有“下层模块编写单元测试,上层模块编写端到端集成测试,两者混合在一起运行”的情况,这时候大部分Mock方法都应该使用`MockScope.ASSOCIATED`作为生效范围。针对这种情况,`TestableMock`支持通过`mockScope`运行参数来修改默认的Mock方法生效范围,详见[全局运行参数](zh-cn/doc/javaagent-args.md)文档。
|
||||
|
||||
> 特别说明。若要Mock静态块里的调用,Mock方法的`scope`必须为`MockScope.GLOBAL`,因为静态块中的代码在程序初始化时就会执行,不属于任何测试用例。典型场景是在使用JNI开发的项目中Mock系统库的加载方法。
|
||||
> ```java
|
||||
> static {
|
||||
> System.loadLibrary("native-lib");
|
||||
> }
|
||||
> ```
|
||||
> 若默认的`scope`参数不是`MockScope.GLOBAL`,则相应Mock方法应当显式的声明`scope`值,例如:
|
||||
> ```java
|
||||
> @MockMethod(targetClass = System.class, scope = MockScope.GLOBAL)
|
||||
> private void loadLibrary(String libname) {
|
||||
> System.err.println("loadLibrary " + libname);
|
||||
> }
|
||||
> ```
|
||||
@@ -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.4.10</testable.version>
|
||||
<testable.version>0.6.0</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.10')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.10')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -77,18 +77,20 @@ class Demo {
|
||||
class DemoTest {
|
||||
private Demo demo = new Demo();
|
||||
|
||||
// 拦截`System.out.println`调用
|
||||
@MockMethod
|
||||
public void println(PrintStream ps, String msg) {
|
||||
// 执行原调用
|
||||
ps.println(msg);
|
||||
public static class Mock {
|
||||
// 拦截System.out.println调用
|
||||
@MockMethod
|
||||
public void println(PrintStream ps, String msg) {
|
||||
// 执行原调用
|
||||
ps.println(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecordAction() {
|
||||
Action action = new Action("click", ":download");
|
||||
demo.recordAction();
|
||||
// 验证Mock方法`println`被调用,且传入参数符合预期
|
||||
// 验证Mock方法println被调用,且传入参数格式符合预期
|
||||
verify("println").with(matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,93 @@
|
||||
自助问题排查
|
||||
---
|
||||
|
||||
相比`Mockito`等由开发者手工放置Mock类的做法,`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也有可能发生预料之外的Mock替换。
|
||||
相比`Mockito`等由开发者手工放置Mock类的做法,`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也容易导致对“Mock究竟有没生效”的疑问。
|
||||
|
||||
若要排查Mock相关的问题,只需在测试类上添加`@MockWith`注解,并配置参数`diagnose`值为`MockDiagnose.ENABLE`,在运行测试时就会打印出详细的Mock方法替换过程。
|
||||
若要排查Mock相关的问题,只需在相应的**Mock容器类**上添加`@MockDiagnose`注解,并配置参数值为`LogLevel.ENABLE`,在运行测试时就会打印出详细的Mock方法替换过程。例如:
|
||||
|
||||
```java
|
||||
@MockWith(diagnose = MockDiagnose.ENABLE)
|
||||
class DemoTest {
|
||||
...
|
||||
@MockDiagnose(LogLevel.ENABLE)
|
||||
public static class Mock {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出日志示例如下:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
|
||||
[DIAGNOSE] Found 7 mock methods
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[DIAGNOSE] Found 6 test cases
|
||||
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[DIAGNOSE] Line 14, mock method createBlackBox used
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[DIAGNOSE] Line 22, mock method innerFunc used
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
[DIAGNOSE] Line 27, mock method "staticFunc" used
|
||||
[DIAGNOSE] Handling method commonFunc
|
||||
[DIAGNOSE] Line 29, mock method trim used
|
||||
[DIAGNOSE] Line 29, mock method sub used
|
||||
[DIAGNOSE] Line 29, mock method startsWith used
|
||||
[DIAGNOSE] Handling method getBox
|
||||
[DIAGNOSE] Line 36, mock method secretBox used
|
||||
[DIAGNOSE] Handling method callerOne
|
||||
[DIAGNOSE] Line 43, mock method callFromDifferentMethod used
|
||||
[DIAGNOSE] Handling method callerTwo
|
||||
[DIAGNOSE] Line 47, mock method callFromDifferentMethod used
|
||||
[DIAGNOSE] Handling method innerFunc
|
||||
[DIAGNOSE] Handling method callFromDifferentMethod
|
||||
[DIAGNOSE] Line 34, mock method "trim" used
|
||||
[DIAGNOSE] Line 34, mock method "sub" used
|
||||
[DIAGNOSE] Line 34, mock method "startsWith" used
|
||||
... ...
|
||||
```
|
||||
|
||||
该日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
|
||||
其中`Line XX, mock method "XXX" used`日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
|
||||
|
||||
简单排查方法:
|
||||
|
||||
- 若没有任何输出,请检查`pom.xml`或`build.gradle`配置是否正确引入了`TestableMock`依赖
|
||||
- 若只输出了`Handling mock class`,请检查Mock容器类的名称和位置是否符合规范
|
||||
- 若只输出了`Handling mock class`和`Handling test class`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test",或者是否正确的使用了`@MockWith`注解
|
||||
- 若输出了`Handling source class`以及`Handling method xxx`,但预期的代码行位置没有发生Mock替换,请继续检查Mock方法定义是否未与目标方法匹配
|
||||
|
||||
对于上述的最后一种情况(预期Mock未生效),可将日志级别提升到`LogLevel.VERBOSE`做进一步排查。例如:
|
||||
|
||||
```java
|
||||
class DemoTest {
|
||||
@MockDiagnose(LogLevel.VERBOSE)
|
||||
public static class Mock {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
再次执行单元测试,此时将会打印出所有Mock方法的签名定义,以及被测类中扫描到所有调用的实际方法签名:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[VERBOSE] Test case "should_mock_new_object"
|
||||
... ...
|
||||
[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.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;"
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[VERBOSE] Line 13, constructing "java.lang.Object()"
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[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.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"
|
||||
[VERBOSE] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
... ...
|
||||
```
|
||||
|
||||
输出日志结构参考如下:
|
||||
|
||||
- `Mock constructor "<Mock方法名>" as "<方法签名>" for "<类型>"` 在测试类中扫描到的**Mock构造方法**及其签名
|
||||
- `Mock method "<Mock方法名>" as "<方法签名>"` 在测试类中扫描到的**普通Mock方法**及其签名
|
||||
- `Line XX, constructing "<类型>" as "<方法签名>"` 在被测类中扫描掉的**构造方法调用**及其签名
|
||||
- `Line XX, invoking "<方法名>" as "<方法签名>"` 在被测类中扫描到的**成员方法调用**及其签名
|
||||
|
||||
67
docs/zh-cn/doc/upgrade-guide.md
Normal file
67
docs/zh-cn/doc/upgrade-guide.md
Normal file
@@ -0,0 +1,67 @@
|
||||
版本升级说明
|
||||
---
|
||||
|
||||
### 升级到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`版本延续了简洁轻量原则,同时为了更好的实现Mock方法复用,新版本的Mock类与测试类之间有了明确的边界。从`0.4`版本升级到`0.5`时,唯一需要的改变是将测试类中的所有Mock方法使用一个`public static class Mock { }`包裹起来。
|
||||
|
||||
例如,原先有如下测试类定义:
|
||||
|
||||
```java
|
||||
public class DemoMockTest {
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "hello_" + text;
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
升级为`0.5`版本后,将所有Mock方法(此例中只有`innerFunc`这一个方法)移到一个名称为`Mock`的静态内部类中,相当于增加两行代码:
|
||||
|
||||
```java
|
||||
public class DemoMockTest {
|
||||
|
||||
public static class Mock { // 增加此行
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "hello_" + text;
|
||||
}
|
||||
} // 增加此行
|
||||
|
||||
@Test
|
||||
void should_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后将`pom.xml`或`build.gradle`文件中的`TestableMock`依赖升级到对应的新版本即可。
|
||||
@@ -5,10 +5,6 @@
|
||||
|
||||
IntelliJ IDE对`TestableMock`所用到的`JSR-269`注释处理器以及`maven-surefire-plugin`插件的附加参数均支持良好。通常无需特殊配置,可开箱即用。
|
||||
|
||||
> 在IntelliJ 2020.3及后续版本里,其内置的编译期注释处理器运行时参数类型与标准`Maven`编译过程不一致。在`TestableMock`低于`0.4.5`的版本中会遇到`@EnablePrivateAccess`注解未生效的问题,可通过在系统配置的"Build Tools > Maven > Runner"中开启"Delegate IDE build/run actions to maven"选项解决。
|
||||
>
|
||||
> 
|
||||
|
||||
## 使用Eclipse IDE
|
||||
|
||||
由于`Eclipse`内置的自动编译功能基于三方编译器实现,与标准`javac`编译过程不兼容,会导致在IDE中运行测试用例时`@EnablePrivateAccess`注解无效。不过,通过`PrivateAccessor`工具类访问被测类私有成员的功能不会受编译器差异影响。
|
||||
|
||||
@@ -41,4 +41,4 @@
|
||||
|
||||
最后点击运行单元测试,如图中位置③
|
||||
|
||||
总体来说,使用`testable-maven-plugin`带来的额外复杂性依然高于其对配置的简化作用。目前而言,直接在`pom.xml`文件中修改`maven-surefire-plugin`插件配置还是相对推荐的实用方案。
|
||||
总体来说,由于当下的IDE支持问题,使用`testable-maven-plugin`带来的额外复杂性依然高于其对配置的简化作用。目前直接在`pom.xml`文件中修改`maven-surefire-plugin`插件配置还是相对推荐的实用方案。
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user