Compare commits

...

107 Commits

Author SHA1 Message Date
金戟
9924d7d2fc release v0.6.4 2021-04-24 17:43:43 +08:00
金戟
9f5867ed76 support nested annotation of junit 5 2021-04-24 16:59:29 +08:00
金戟
be0a580a04 AbstractCollection is unranchable in java agent 2021-04-24 16:23:01 +08:00
金戟
78d7ee21a6 use void type instead of null type 2021-04-24 11:47:41 +08:00
金戟
af409a65a8 fix circle check rounded by getType not equals with getClass 2021-04-23 21:41:30 +08:00
金戟
a67619d574 add verbose log for omni constructor 2021-04-23 20:34:15 +08:00
金戟
b89daa22c3 always skip null element when traveling class 2021-04-23 19:30:54 +08:00
金戟
e948e625ea better to skip classes in javax/crypto package 2021-04-21 16:32:00 +08:00
金戟
d463f863db local variable table of non-static method can be empty 2021-04-20 21:17:09 +08:00
金戟
31140bef87 add description of global properties configurations 2021-04-18 15:06:36 +08:00
金戟
d2944a88fb reorder parameters 2021-04-18 13:37:36 +08:00
金戟
9d6e4dd1fa bump to v0.6.3 2021-04-18 10:26:06 +08:00
金戟
9c09bf9653 avoid to select constructor with parameter of its own type 2021-04-18 09:45:57 +08:00
金戟
6d87653fa2 handle non-static method without this reference 2021-04-18 09:15:03 +08:00
金戟
eec88f4bd9 should convert package prefix to slash separated format 2021-04-17 19:52:32 +08:00
金戟
5921ccab07 allow skip specified class from omni enhancer 2021-04-17 19:24:02 +08:00
金戟
8dde402a8c get resource could be null 2021-04-17 18:45:29 +08:00
金戟
d10478f95f package prefix should auto append dot 2021-04-17 17:46:35 +08:00
金戟
0c063e9b1f support custom inner mock class name 2021-04-17 17:16:02 +08:00
金戟
b8771f0dac simplify premain logic 2021-04-16 12:52:24 +08:00
金戟
42dd58584b mark date as basic type 2021-04-14 09:28:07 +08:00
金戟
ce55bb4c9a skip all configuration class for omni handler 2021-04-12 16:40:53 +08:00
金戟
cff2d1ae5a fix release script 2021-04-09 14:25:02 +08:00
金戟
5904ad99cb release v0.6.2 2021-04-07 21:38:24 +08:00
金戟
74fbc5b53f support configure file 2021-04-07 09:40:20 +08:00
金戟
f6b5bab250 manual generated serialVersionUID not works 2021-04-07 00:05:29 +08:00
金戟
f2feb3cc68 more precise omni handler filter 2021-04-06 23:08:30 +08:00
金戟
b41923bfa0 release v0.6.1 2021-04-06 08:09:23 +08:00
金戟
7c1f18de46 optimize docs 2021-04-06 00:03:38 +08:00
金戟
c393d40d0d better log description, handling -> found 2021-04-05 15:52:12 +08:00
金戟
d4a69528a7 use relative path for bytecode dumping 2021-04-05 14:58:29 +08:00
金戟
a46e73a421 support specify log file path 2021-04-05 14:09:44 +08:00
金戟
01e9890234 always generate log file 2021-04-05 11:56:06 +08:00
金戟
93cdef1d6c remove gradle settings in kotlin demo 2021-04-04 15:38:43 +08:00
金戟
e2e986e50d add a tiny demo for spock 2021-04-04 15:31:18 +08:00
金戟
f5475aa18e simplify log level config 2021-04-04 00:10:56 +08:00
金戟
844323af3a fix compatibility with spring boot 2021-04-03 23:54:01 +08:00
金戟
cc2deeff1c ClassCircularityError is not an exception 2021-04-03 23:35:22 +08:00
金戟
14e4d2db5b ignore cglib classes 2021-04-03 22:51:28 +08:00
金戟
c9c8ba6c24 junit test class can also has parent 2021-04-03 22:10:18 +08:00
金戟
b5c0afb3da ignore any exception when try load class 2021-04-03 18:17:11 +08:00
金戟
617d372545 avoid to transfer gradle TestWorker class 2021-04-03 18:16:36 +08:00
金戟
58da25de53 add mock verifier support spock 2021-04-03 13:41:51 +08:00
金戟
46706ba56c add supported test framwork 2021-04-03 10:02:03 +08:00
金戟
1ced04bf29 fix loging 2021-04-02 23:50:39 +08:00
金戟
7b2f967626 add tool scripts 2021-04-01 09:22:28 +08:00
金戟
e0a4a39d55 move dump method to util package 2021-03-31 10:34:45 +08:00
金戟
b4da23a90e adopt for groovy bytecode 2021-03-31 10:34:19 +08:00
金戟
c1659242b6 fix markdown format 2021-03-28 15:10:52 +08:00
金戟
929abffb66 add english doc for omni methods 2021-03-28 14:47:00 +08:00
金戟
e370114b01 release v0.6 2021-03-27 20:05:21 +08:00
金戟
26d3178840 support set dump path for single class 2021-03-27 00:31:02 +08:00
金戟
55ccc385d5 remove unnecessary mock class reference in test class handler 2021-03-26 22:26:12 +08:00
金戟
20b346ed34 remove diagnose parameter in MockWith annotation 2021-03-26 20:20:45 +08:00
金戟
00055a4626 always print more error information 2021-03-26 01:12:52 +08:00
金戟
27174afa93 implement fall-fast anti-refactor machenism 2021-03-26 01:12:30 +08:00
金戟
3d6d3c9dd1 skip $jacocoData field 2021-03-25 13:10:49 +08:00
金戟
df7e641a0c update doc to fit version 0.6 2021-03-24 23:51:19 +08:00
金戟
3acf341d1f add kotlin test case for omni 2021-03-24 13:23:02 +08:00
金戟
52878364ee don't travel into basic class 2021-03-24 13:17:15 +08:00
金戟
9871a96db0 create omni constructor correctly 2021-03-24 00:34:17 +08:00
金戟
9b56e3d64a fit common collection interfaces 2021-03-23 21:29:54 +08:00
金戟
ea8f305152 handle circle reference 2021-03-23 00:00:48 +08:00
金戟
3feba7fdfa allow non-public mock class 2021-03-22 14:57:59 +08:00
金戟
9064f7a582 better demo for omni methods 2021-03-22 00:41:19 +08:00
金戟
b062dbc592 move private accessor to tool package 2021-03-21 07:43:22 +08:00
金戟
a623bd0a33 Merge branch 'master' into 0.6
* master:
  update faq about how to mock inner class
2021-03-20 20:25:10 +08:00
金戟
a54bb33c09 support quick create array instance 2021-03-20 20:23:23 +08:00
金戟
bbc41f2987 fix omni get path with index 2021-03-20 18:02:01 +08:00
金戟
7b9a0964bc use k8s-like model for omni demos 2021-03-20 17:30:10 +08:00
金戟
a0dae0dfe1 support wildcard match 2021-03-20 14:34:19 +08:00
金戟
f54a6ce022 handle array in query path correctly 2021-03-20 00:50:30 +08:00
金戟
606760d632 refactor test code, and omni constructor test 2021-03-18 23:51:06 +08:00
金戟
206fc5486e support access array element by index 2021-03-18 00:15:01 +08:00
金戟
af3af6b272 handle array and primary types 2021-03-17 19:26:50 +08:00
金戟
fe7420c99a implement basic omni instance construction 2021-03-17 16:24:20 +08:00
金戟
7857782125 do omni transform 2021-03-17 14:24:16 +08:00
金戟
f776deff0e update faq about how to mock inner class 2021-03-16 22:19:16 +08:00
金戟
328296ae3a complete get and set by path of common class 2021-03-16 14:31:37 +08:00
金戟
a5038320b4 convert query path to match pattern 2021-03-16 11:50:22 +08:00
金戟
bdd16c0948 generate member index of specified class 2021-03-15 23:51:21 +08:00
金戟
dce1accafb fix class reference of frame full operation 2021-03-15 19:14:26 +08:00
Fan Lin
2865093fef Merge pull request #116 from mikero8/fix-readme
Update README_EN.md
2021-03-14 17:02:18 +08:00
Mikero
5f69b21532 Update README_EN.md
Fix an issue in English README
2021-03-13 09:14:41 -08:00
金戟
250a1beaf9 bump to version 0.5.2 2021-03-13 09:35:33 +08:00
金戟
323ab398b2 fix check-cast byte code format 2021-03-13 00:38:00 +08:00
金戟
edd4c24d88 duplication check is unnecessary 2021-03-09 15:54:05 +08:00
金戟
4995bbd9a9 support access private member of super class 2021-03-09 14:21:05 +08:00
金戟
121536a337 support mocking invocation inside an inner class 2021-03-05 00:04:44 +08:00
金戟
b163152c2f better diagnose print 2021-03-04 22:13:10 +08:00
金戟
5897e087c8 print exception location 2021-03-04 11:16:49 +08:00
金戟
6f079db3f3 move demo code out of testable package 2021-03-04 11:09:12 +08:00
金戟
40073ad784 should handle void return type 2021-03-04 10:00:07 +08:00
金戟
e46ec3d98a release v0.5.1 2021-03-03 09:46:21 +08:00
金戟
1849cf85c5 use kotlin style method descriptor 2021-03-02 23:42:04 +08:00
金戟
f8450d7047 use pre-check to reduce unnecessary verbose parameter calculation 2021-03-02 23:33:17 +08:00
金戟
196dffc73d print diagnose verbose using java style descriptor 2021-03-02 23:18:27 +08:00
金戟
1ff6ec5961 support junit 5 parameterized test 2021-03-01 23:47:50 +08:00
金戟
e50633b884 fit for ternary operation in object constructor parameter 2021-02-27 00:59:22 +08:00
金戟
2976ca0211 print diagnose descriptor in java-like format 2021-02-26 14:02:58 +08:00
金戟
24c6a9cc5c validate mock method before use 2021-02-25 13:48:33 +08:00
金戟
9e7ceb2dc1 fix diagnose setup when using MockWith annotation 2021-02-24 21:29:48 +08:00
金戟
8dfd0b4516 handle bytecode jump opcode 2021-02-24 21:07:56 +08:00
金戟
190cf4cc3c should handle frame byte code when mock substitutions 2021-02-24 14:31:50 +08:00
金戟
d49f526376 add placeholder for english docs 2021-02-23 13:12:23 +08:00
金戟
f01ff10fba narrow down Exception scope when possible 2021-02-22 15:56:50 +08:00
金戟
f64b07e9da add doc for mock design 2021-02-21 14:32:32 +08:00
181 changed files with 4282 additions and 1040 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
# maven ignore
target/
.mvn/
# gradle ignore
build/

View File

@@ -9,9 +9,9 @@
阅读[这里](https://mp.weixin.qq.com/s/KyU6Eu7mDkZU8FspfSqfMw)了解更多故事。
<font size="5">**0.5版本已发布**</font>,从`0.4.x`升级到`0.5.x`版本请参考[0.5版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-to-v05)
<font size="5">**0.6版本已发布**</font>,从`0.5.x`升级到`0.6.x`版本请参考[版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-guide)
如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码我们将在24小时内回复并处理
如果有遇到其他任何使用问题和建议,请直接在[Issues](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码我们将在24小时内回复并处理
-----
@@ -19,8 +19,8 @@
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
- `0.5` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
- `0.6` 实现第四项单元测试增强能力"[快速入参构造器](https://alibaba.github.io/testable-mock/#/zh-cn/doc/parameter-constructor)"
- `0.6` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
- `0.7` 完善Mock功能的防代码重构机制支持快速Mock指定类型的所有方法
- `1.0` 功能稳定,一个崭新的开始
## 目录结构
@@ -32,6 +32,7 @@
|-- testable-agent ➜ JavaAgent模块提供Mock测试相关功能
|-- testable-core ➜ 基础功能模块提供Mock相关注解和工具类
|-- testable-maven-plugin ➜ Maven插件模块用于简化JavaAgent注入
|-- tool ➜ 项目开发过程中的工具脚本
|-- demo
| |-- java-demo ➜ Java语言的示例代码
| `-- kotlin-demo ➜ Kotlin语言的示例代码

View File

@@ -7,13 +7,12 @@ Write a mock method, add an `@MockMethod` annotation, everything is done.
Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
## Loadmap
## Roadmap
`TestableMock` is still under heavy development, the following version plans may be adjusted during the iteration
- `v0.4` it's the current version, refer to the [issue](https://github.com/alibaba/testable-mock/issues) list for the work in progress
- `v0.5` implementation mock class inherit mechanism, so that the same mock method can be reused between test classes conveniently
- `v0.6` narrows the default effective scope of mock methods to the class under test, to avoid interaction of mocking cross classes
- `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
@@ -25,6 +24,7 @@ Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
|-- testable-agent ➜ JavaAgent module, provides test mocking related functions
|-- testable-core ➜ Basic function module, provides mock related class and annotation
|-- testable-maven-plugin ➜ Maven plugin module, for simplify JavaAgent injection
|-- tool ➜ Scripts for project maintain
|-- demo
| |-- java-demo ➜ Java code example
| `-- kotlin-demo ➜ Kotlin code example

View File

@@ -13,8 +13,8 @@ repositories {
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
testImplementation('com.alibaba.testable:testable-all:0.5.0')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.0')
testImplementation('com.alibaba.testable:testable-all:0.6.4')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.4')
}
tasks.withType(JavaCompile) {

View File

@@ -12,7 +12,7 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>5.6.2</junit.version>
<testable.version>0.5.0</testable.version>
<testable.version>0.6.4</testable.version>
</properties>
<dependencies>

View File

@@ -1,8 +1,8 @@
package com.alibaba.testable.demo.basic;
package com.alibaba.demo.basic;
import com.alibaba.testable.demo.basic.model.BlackBox;
import com.alibaba.testable.demo.basic.model.Box;
import com.alibaba.testable.demo.basic.model.Color;
import com.alibaba.demo.basic.model.mock.BlackBox;
import com.alibaba.demo.basic.model.mock.Box;
import com.alibaba.demo.basic.model.mock.Color;
/**
* 演示父类变量引用子类对象时的Mock场景

View File

@@ -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";
}
}

View File

@@ -1,7 +1,7 @@
package com.alibaba.testable.demo.basic;
package com.alibaba.demo.basic;
import com.alibaba.testable.demo.basic.model.BlackBox;
import com.alibaba.demo.basic.model.mock.BlackBox;
import java.util.ArrayList;
import java.util.HashMap;

View File

@@ -1,6 +1,6 @@
package com.alibaba.testable.demo.basic;
package com.alibaba.demo.basic;
import com.alibaba.testable.demo.basic.model.BlackBox;
import com.alibaba.demo.basic.model.mock.BlackBox;
import java.nio.file.Files;
import java.nio.file.Paths;

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.basic;
package com.alibaba.demo.basic;
import java.util.List;

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.basic;
package com.alibaba.demo.basic;
import java.util.*;

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.basic.model;
package com.alibaba.demo.basic.model.mock;
public class BlackBox extends Box implements Color {

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.basic.model;
package com.alibaba.demo.basic.model.mock;
abstract public class Box {

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.basic.model;
package com.alibaba.demo.basic.model.mock;
public interface Color {

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi;
package com.alibaba.demo.one2multi;
public class ASvc {

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi;
package com.alibaba.demo.one2multi;
public class BSvc {

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi;
package com.alibaba.demo.one2multi;
public class CSvc {

View File

@@ -1,10 +1,9 @@
package com.alibaba.testable.demo.basic;
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.basic.DemoInherit;
import com.alibaba.testable.demo.basic.model.BlackBox;
import com.alibaba.testable.demo.basic.model.Box;
import com.alibaba.testable.demo.basic.model.Color;
import org.junit.jupiter.api.Test;
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
@@ -51,42 +50,42 @@ class DemoInheritTest {
}
@Test
void should_able_to_mock_call_sub_object_method_by_parent_object() {
void should_mock_call_sub_object_method_by_parent_object() {
BlackBox box = (BlackBox)demoInherit.putIntoBox();
verify("put_into_box").withTimes(1);
assertEquals("put_data_into_box", box.get());
}
@Test
void should_able_to_mock_call_sub_object_method_by_sub_object() {
void should_mock_call_sub_object_method_by_sub_object() {
BlackBox box = demoInherit.putIntoBlackBox();
verify("put_into_blackbox").withTimes(1);
assertEquals("put_data_into_blackbox", box.get());
}
@Test
void should_able_to_mock_call_parent_object_method_by_parent_object() {
void should_mock_call_parent_object_method_by_parent_object() {
String content = demoInherit.getFromBox();
verify("get_from_box").withTimes(1);
assertEquals("get_from_box", content);
}
@Test
void should_able_to_mock_call_parent_object_method_by_sub_object() {
void should_mock_call_parent_object_method_by_sub_object() {
String content = demoInherit.getFromBlackBox();
verify("get_from_blackbox").withTimes(1);
assertEquals("get_from_blackbox", content);
}
@Test
void should_able_to_mock_call_interface_method_by_interface_object() {
void should_mock_call_interface_method_by_interface_object() {
String color = demoInherit.getColorViaColor();
verify("get_color_from_color").withTimes(1);
assertEquals("color_from_color", color);
}
@Test
void should_able_to_mock_call_interface_method_by_sub_class_object() {
void should_mock_call_interface_method_by_sub_class_object() {
String color = demoInherit.getColorViaBox();
verify("get_color_from_blackbox").withTimes(1);
assertEquals("color_from_blackbox", color);

View File

@@ -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());
}
}

View File

@@ -1,9 +1,8 @@
package com.alibaba.testable.demo.basic;
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.basic.DemoMatcher;
import com.alibaba.testable.demo.basic.model.BlackBox;
import org.junit.jupiter.api.Test;
import static com.alibaba.testable.core.matcher.InvokeMatcher.*;

View File

@@ -1,9 +1,9 @@
package com.alibaba.testable.demo.basic;
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.basic.DemoMock;
import com.alibaba.testable.demo.basic.model.BlackBox;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.concurrent.Executors;
@@ -72,20 +72,20 @@ class DemoMockTest {
}
@Test
void should_able_to_mock_new_object() {
void should_mock_new_object() {
assertEquals("mock_something", demoMock.newFunc());
verify("createBlackBox").with("something");
}
@Test
void should_able_to_mock_member_method() throws Exception {
void should_mock_member_method() throws Exception {
assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoMock.outerFunc("hello"));
verify("innerFunc").with("hello");
verify("staticFunc").with();
}
@Test
void should_able_to_mock_common_method() {
void should_mock_common_method() {
assertEquals("trim_string__sub_string__false", demoMock.commonFunc());
verify("trim").withTimes(1);
verify("sub").withTimes(1);
@@ -93,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
@@ -109,7 +109,7 @@ class DemoMockTest {
}
@Test
void should_able_to_set_mock_context() throws Exception {
void should_set_mock_context() throws Exception {
MOCK_CONTEXT.put("case", "special_case");
// synchronous
assertEquals("mock_special", demoMock.callerOne());

View File

@@ -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"));
}
}

View File

@@ -1,8 +1,6 @@
package com.alibaba.testable.demo.basic;
package com.alibaba.demo.basic;
import com.alibaba.testable.core.accessor.PrivateAccessor;
import com.alibaba.testable.demo.basic.DemoPrivateAccess;
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
import com.alibaba.testable.core.tool.PrivateAccessor;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
@@ -12,64 +10,46 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* 演示私有成员访问功能
* Demonstrate private member access functionality
* 演示使用`PrivateAccessor`工具类访问私有成员
* Demonstrate access private member via `PrivateAccessor` class
*/
@EnablePrivateAccess
class DemoPrivateAccessTest {
class DemoPrivateAccessorTest {
private DemoPrivateAccess demoPrivateAccess = new DemoPrivateAccess();
@Test
void should_able_to_access_private_method() {
void should_access_private_method() {
List<String> list = new ArrayList<String>() {{ add("a"); add("b"); add("c"); }};
assertEquals("member", demoPrivateAccess.privateFunc());
assertEquals("member", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc"));
assertEquals("abc + hello + 1", demoPrivateAccess.privateFuncWithArgs(list, "hello", 1));
assertEquals("abc + hello + 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFuncWithArgs", list, "hello", 1));
}
@Test
void should_able_to_access_private_field() {
demoPrivateAccess.count = 2;
assertEquals(Integer.valueOf(2), demoPrivateAccess.count);
void should_access_private_field() {
PrivateAccessor.set(demoPrivateAccess, "count", 3);
assertEquals(Integer.valueOf(3), PrivateAccessor.get(demoPrivateAccess, "count"));
}
@Test
void should_able_to_access_private_static_method() {
assertEquals("static", DemoPrivateAccess.privateStaticFunc());
void should_access_private_static_method() {
assertEquals("static", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFunc"));
assertEquals("hello + 1", DemoPrivateAccess.privateStaticFuncWithArgs("hello", 1));
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", "hello", 1));
}
@Test
void should_able_to_access_private_static_field() {
DemoPrivateAccess.staticCount = 2;
assertEquals(Integer.valueOf(2), DemoPrivateAccess.staticCount);
void should_access_private_static_field() {
PrivateAccessor.setStatic(DemoPrivateAccess.class, "staticCount", 3);
assertEquals(Integer.valueOf(3), PrivateAccessor.getStatic(DemoPrivateAccess.class, "staticCount"));
}
@Test
void should_able_to_update_final_field() {
demoPrivateAccess.pi = 4.13;
assertEquals(Double.valueOf(4.13), demoPrivateAccess.pi);
void should_update_final_field() {
PrivateAccessor.set(demoPrivateAccess, "pi", 3.14);
assertEquals(Double.valueOf(3.14), PrivateAccessor.get(demoPrivateAccess, "pi"));
}
@Test
void should_able_to_use_null_parameter() {
demoPrivateAccess.pi = null;
assertNull(demoPrivateAccess.pi);
assertEquals("null + 1", DemoPrivateAccess.privateStaticFuncWithArgs(null, 1));
void should_use_null_parameter() {
PrivateAccessor.set(demoPrivateAccess, "pi", null);
assertNull(PrivateAccessor.get(demoPrivateAccess, "pi"));
assertEquals("null + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", null, 1));

View File

@@ -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));
}
}

View File

@@ -1,8 +1,8 @@
package com.alibaba.testable.demo.basic;
package com.alibaba.demo.basic;
import com.alibaba.testable.core.annotation.MockConstructor;
import com.alibaba.testable.core.annotation.MockMethod;
import com.alibaba.testable.demo.basic.DemoTemplate;
import com.alibaba.demo.basic.DemoTemplate;
import org.junit.jupiter.api.Test;
import java.util.*;
@@ -72,19 +72,19 @@ class DemoTemplateTest {
}
@Test
void should_able_to_mock_single_template_method() {
void should_mock_single_template_method() {
String res = demoTemplate.singleTemplateMethod();
assertEquals("demo_mock_list", res);
}
@Test
void should_able_to_mock_double_template_method() {
void should_mock_double_template_method() {
String res = demoTemplate.doubleTemplateMethod();
assertEquals("testable_mock_map", res);
}
@Test
void should_able_to_mock_new_template_method() {
void should_mock_new_template_method() {
Set<?> res = demoTemplate.newTemplateMethod();
assertEquals(2, res.size());
Iterator<?> iterator = res.stream().iterator();

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi;
package com.alibaba.demo.one2multi;
import com.alibaba.testable.core.annotation.MockMethod;

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi;
package com.alibaba.demo.one2multi;
import com.alibaba.testable.core.annotation.MockMethod;

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi;
package com.alibaba.demo.one2multi;
import com.alibaba.testable.core.annotation.MockMethod;

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi;
package com.alibaba.demo.one2multi;
import com.alibaba.testable.core.annotation.MockWith;
import org.junit.jupiter.api.Test;
@@ -15,7 +15,7 @@ public class OneToMultiSvcTest {
private CSvc cSvc = new CSvc();
@Test
public void should_able_to_test_multi_class_together() {
public void should_test_multi_class_together() {
assertEquals("a_mock", aSvc.demo("test"));
assertEquals("b_mock", bSvc.demo("test"));
assertEquals("c_mock", cSvc.demo("test"));

View File

@@ -0,0 +1 @@
omni.constructor.enhance.enable = true

View File

@@ -16,8 +16,8 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
testImplementation("com.alibaba.testable:testable-all:0.5.0")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.5.0")
testImplementation("com.alibaba.testable:testable-all:0.6.4")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.6.4")
}
tasks.withType<KotlinCompile> {

View File

@@ -14,7 +14,7 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>5.6.2</junit.version>
<testable.version>0.5.0</testable.version>
<testable.version>0.6.4</testable.version>
</properties>
<dependencies>

View File

@@ -1 +0,0 @@
rootProject.name = "demo"

View File

@@ -1,8 +1,8 @@
package com.alibaba.testable.demo.basic
package com.alibaba.demo.basic
import com.alibaba.testable.demo.basic.model.BlackBox
import com.alibaba.testable.demo.basic.model.Box
import com.alibaba.testable.demo.basic.model.Color
import com.alibaba.demo.basic.model.mock.BlackBox
import com.alibaba.demo.basic.model.mock.Box
import com.alibaba.demo.basic.model.mock.Color
/**
* 演示父类变量引用子类对象时的Mock场景

View File

@@ -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"
}
}
}

View File

@@ -1,6 +1,6 @@
package com.alibaba.testable.demo.basic
package com.alibaba.demo.basic
import com.alibaba.testable.demo.basic.model.BlackBox
import com.alibaba.demo.basic.model.mock.BlackBox
import java.util.*
/**

View File

@@ -1,7 +1,7 @@
package com.alibaba.testable.demo.basic
package com.alibaba.demo.basic
import com.alibaba.testable.demo.basic.model.BlackBox
import com.alibaba.testable.demo.basic.model.ColorBox
import com.alibaba.demo.basic.model.mock.BlackBox
import com.alibaba.demo.basic.model.mock.ColorBox
import java.nio.file.Files
import java.nio.file.Paths

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.basic
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"
}

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.basic
package com.alibaba.demo.basic
import java.util.ArrayList
import java.util.HashMap

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.basic.model
package com.alibaba.demo.basic.model.mock
class BlackBox(var input: String) : Box(), Color {

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.basic.model
package com.alibaba.demo.basic.model.mock
abstract class Box {

View File

@@ -0,0 +1,7 @@
package com.alibaba.demo.basic.model.mock
interface Color {
val color: String
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,9 @@
package com.alibaba.demo.basic.model.omni
class GrandChild {
// ---------- 内部成员字段 ----------
var value = 0
var content: String? = null
}

View File

@@ -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
}

View File

@@ -1,9 +1,9 @@
package com.alibaba.testable.demo.java2kotlin
package com.alibaba.demo.java2kotlin
import java.io.File
import java.io.IOException
object PathUtil {
object PathDemo {
fun deleteRecursively(file: File) {
if (!file.exists()) {

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi
package com.alibaba.demo.one2multi
class ASvc {

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi
package com.alibaba.demo.one2multi
class BSvc {

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi
package com.alibaba.demo.one2multi
class CSvc {

View File

@@ -1,7 +0,0 @@
package com.alibaba.testable.demo.basic.model
interface Color {
val color: String
}

View File

@@ -1,10 +1,10 @@
package com.alibaba.testable.demo.basic
package com.alibaba.demo.basic
import com.alibaba.testable.core.annotation.MockMethod
import com.alibaba.testable.core.matcher.InvokeVerifier
import com.alibaba.testable.demo.basic.model.BlackBox
import com.alibaba.testable.demo.basic.model.Box
import com.alibaba.testable.demo.basic.model.Color
import 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
@@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test
*/
internal class DemoInheritTest {
private val demoInherit = DemoInherit()
private val demoInherit = com.alibaba.demo.basic.DemoInherit()
class Mock {
@MockMethod(targetMethod = "put")
@@ -49,42 +49,42 @@ internal class DemoInheritTest {
}
@Test
fun should_able_to_mock_call_sub_object_method_by_parent_object() {
fun should_mock_call_sub_object_method_by_parent_object() {
val box = demoInherit.putIntoBox() as BlackBox
InvokeVerifier.verify("put_into_box").withTimes(1)
Assertions.assertEquals("put_data_into_box", box.get())
}
@Test
fun should_able_to_mock_call_sub_object_method_by_sub_object() {
fun should_mock_call_sub_object_method_by_sub_object() {
val box = demoInherit.putIntoBlackBox()
InvokeVerifier.verify("put_into_blackbox").withTimes(1)
Assertions.assertEquals("put_data_into_blackbox", box.get())
}
@Test
fun should_able_to_mock_call_parent_object_method_by_parent_object() {
fun should_mock_call_parent_object_method_by_parent_object() {
val content = demoInherit.fromBox
InvokeVerifier.verify("get_from_box").withTimes(1)
Assertions.assertEquals("get_from_box", content)
}
@Test
fun should_able_to_mock_call_parent_object_method_by_sub_object() {
fun should_mock_call_parent_object_method_by_sub_object() {
val content = demoInherit.fromBlackBox
InvokeVerifier.verify("get_from_blackbox").withTimes(1)
Assertions.assertEquals("get_from_blackbox", content)
}
@Test
fun should_able_to_mock_call_interface_method_by_interface_object() {
fun should_mock_call_interface_method_by_interface_object() {
val color = demoInherit.colorViaColor
InvokeVerifier.verify("get_color_from_color").withTimes(1)
Assertions.assertEquals("color_from_color", color)
}
@Test
fun should_able_to_mock_call_interface_method_by_sub_class_object() {
fun should_mock_call_interface_method_by_sub_class_object() {
val color = demoInherit.colorViaBox
InvokeVerifier.verify("get_color_from_blackbox").withTimes(1)
Assertions.assertEquals("color_from_blackbox", color)

View File

@@ -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())
}
}

View File

@@ -1,10 +1,10 @@
package com.alibaba.testable.demo.basic
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.basic.model.BlackBox
import com.alibaba.demo.basic.model.mock.BlackBox
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
@@ -14,19 +14,19 @@ import org.junit.jupiter.api.Test
*/
internal class DemoMatcherTest {
private val demoMatcher = DemoMatcher()
private val demoMatcher = com.alibaba.demo.basic.DemoMatcher()
class Mock {
@MockMethod(targetMethod = "methodToBeMocked")
private fun methodWithoutArgument(self: DemoMatcher) {
private fun methodWithoutArgument(self: com.alibaba.demo.basic.DemoMatcher) {
}
@MockMethod(targetMethod = "methodToBeMocked")
private fun methodWithArguments(self: DemoMatcher, a1: Any, a2: Any) {
private fun methodWithArguments(self: com.alibaba.demo.basic.DemoMatcher, a1: Any, a2: Any) {
}
@MockMethod(targetMethod = "methodToBeMocked")
private fun methodWithArrayArgument(self: DemoMatcher, a: Array<Any>) {
private fun methodWithArrayArgument(self: com.alibaba.demo.basic.DemoMatcher, a: Array<Any>) {
}
}

View File

@@ -1,12 +1,12 @@
package com.alibaba.testable.demo.basic
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.basic.model.BlackBox
import com.alibaba.testable.demo.basic.model.ColorBox
import com.alibaba.demo.basic.model.mock.BlackBox
import com.alibaba.demo.basic.model.mock.ColorBox
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.util.concurrent.Executors
@@ -64,26 +64,26 @@ internal class DemoMockTest {
}
@Test
fun should_able_to_mock_new_object() {
fun should_mock_new_object() {
assertEquals("mock_something", demoMock.newFunc())
verify("createBlackBox").with("something")
}
@Test
fun should_able_to_mock_member_method() {
fun should_mock_member_method() {
assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoMock.outerFunc("hello"))
verify("innerFunc").with("hello")
verify("staticFunc").with()
}
// @Test
// fun should_able_to_mock_method_in_companion_object() {
// fun should_mock_method_in_companion_object() {
// assertEquals("CALL_MOCK_TAIL", DemoMock.callStaticFunc())
// verify("staticFunc").with()
// }
@Test
fun should_able_to_mock_common_method() {
fun should_mock_common_method() {
assertEquals("trim_string__sub_string__false", demoMock.commonFunc())
verify("trim").withTimes(1)
verify("sub").withTimes(1)
@@ -91,14 +91,14 @@ internal class DemoMockTest {
}
@Test
fun should_able_to_mock_static_method() {
fun should_mock_static_method() {
assertEquals("White_not_secret_box", demoMock.getBox().get())
verify("secretBox").withTimes(1)
verify("createBox").withTimes(1)
}
@Test
fun should_able_to_get_source_method_name() {
fun should_get_source_method_name() {
// synchronous
assertEquals("mock_one_mock_others", demoMock.callerOne() + "_" + demoMock.callerTwo())
// asynchronous
@@ -109,7 +109,7 @@ internal class DemoMockTest {
}
@Test
fun should_able_to_get_test_case_name() {
fun should_get_test_case_name() {
MOCK_CONTEXT["case"] = "special_case"
// synchronous
assertEquals("mock_special", demoMock.callerOne())

View File

@@ -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"))
}
}

View File

@@ -1,6 +1,6 @@
package com.alibaba.testable.demo.basic
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)
}

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.basic
package com.alibaba.demo.basic
import com.alibaba.testable.core.annotation.MockConstructor
import com.alibaba.testable.core.annotation.MockMethod
@@ -40,19 +40,19 @@ internal class DemoTemplateTest {
}
@Test
fun should_able_to_mock_single_template_method() {
fun should_mock_single_template_method() {
val res = demoTemplate.singleTemplateMethod()
Assertions.assertEquals("demo_mock_list", res)
}
@Test
fun should_able_to_mock_double_template_method() {
fun should_mock_double_template_method() {
val res = demoTemplate.doubleTemplateMethod()
Assertions.assertEquals("testable_mock_map", res)
}
@Test
fun should_able_to_mock_new_template_method() {
fun should_mock_new_template_method() {
val res = demoTemplate.newTemplateMethod()
Assertions.assertEquals(2, res.size)
val iterator = res.stream().iterator()

View File

@@ -1,11 +1,11 @@
package com.alibaba.testable.demo.java2kotlin
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 PathDemoTest {
class Mock {
@MockMethod
@@ -41,8 +41,8 @@ class PathUtilTest {
}
@Test
fun should_able_to_mock_java_method_invoke_in_kotlin() {
PathUtil.deleteRecursively(File("/a/b/"))
fun should_mock_java_method_invoke_in_kotlin() {
PathDemo.deleteRecursively(File("/a/b/"))
verify("listFiles").withTimes(2)
verify("delete").withTimes(4)
}

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi
package com.alibaba.demo.one2multi
import com.alibaba.testable.core.annotation.MockMethod

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi
package com.alibaba.demo.one2multi
import com.alibaba.testable.core.annotation.MockMethod

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi
package com.alibaba.demo.one2multi
import com.alibaba.testable.core.annotation.MockMethod

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.demo.one2multi
package com.alibaba.demo.one2multi
import com.alibaba.testable.core.annotation.MockWith
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
@@ -13,7 +13,7 @@ class OneToMultiSvcTest {
private val cSvc = CSvc()
@Test
fun should_able_to_test_multi_class_together() {
fun should_test_multi_class_together() {
Assertions.assertEquals("a_mock", aSvc.demo("test"))
Assertions.assertEquals("b_mock", bSvc.demo("test"))
Assertions.assertEquals("c_mock", cSvc.demo("test"))

View File

@@ -0,0 +1 @@
omni.constructor.enhance.enable = true

View File

@@ -12,6 +12,7 @@
<modules>
<module>java-demo</module>
<module>kotlin-demo</module>
<module>spock-demo</module>
</modules>
</project>

5
demo/settings.gradle Normal file
View File

@@ -0,0 +1,5 @@
rootProject.name = 'demos'
include 'java-demo'
include 'kotlin-demo'
include 'spock-demo'

View File

@@ -0,0 +1,28 @@
plugins {
id 'groovy'
}
group = 'com.alibaba.testable'
version = '1.0.0-SNAPSHOT'
sourceCompatibility = '8'
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
testImplementation 'org.codehaus.groovy:groovy-all:3.0.7'
testImplementation 'org.spockframework:spock-core:2.0-M5-groovy-3.0'
testImplementation('com.alibaba.testable:testable-all:0.6.4')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.4')
}
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
test {
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
useJUnitPlatform()
}

86
demo/spock-demo/pom.xml Normal file
View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Refer to: https://github.com/spockframework/spock-example/blob/master/pom.xml -->
<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>spock-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>spock-demo</name>
<description>Spock demo for Testable</description>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<testable.version>0.6.4</testable.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-bom</artifactId>
<version>2.0-M5-groovy-3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>3.0.7</version>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-junit4</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<!-- The gmavenplus plugin is used to compile Groovy code -->
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.12.0</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compileTests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
<useFile>false</useFile>
<includes>
<include>**/*Test.java</include>
<include>**/*Spec.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,19 @@
package com.github.pbetkier.spockdemo;
import com.github.pbetkier.spockdemo.model.SpockBox;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Collectors;
public class DemoSpock {
public SpockBox createBoxOfNum() {
SpockBox box = new SpockBox();
for (int i = 1; i <= 3; i++) {
box.put(String.valueOf(i));
}
return box;
}
}

View File

@@ -0,0 +1,27 @@
package com.github.pbetkier.spockdemo.model;
import java.util.ArrayList;
import java.util.List;
public class SpockBox {
private List<String> contents = new ArrayList<>();
public int size() {
return contents.size();
}
public void put(String data) {
contents.add(data);
}
public String pop() {
if (contents.isEmpty()) {
return null;
}
String data = contents.get(size() - 1);
contents.remove(size() - 1);
return data;
}
}

View File

@@ -0,0 +1,44 @@
package com.github.pbetkier.spockdemo
import com.alibaba.testable.core.annotation.MockConstructor
import com.alibaba.testable.core.annotation.MockMethod
import com.github.pbetkier.spockdemo.model.SpockBox
import spock.lang.Shared
import spock.lang.Specification
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
class DemoSpockTest extends Specification {
@Shared
def demoSpock = new DemoSpock()
static class Mock {
@MockConstructor
SpockBox createBox() {
SpockBox box = new SpockBox()
box.put("mock zero")
return box
}
@MockMethod(targetMethod = "put")
void putBox(SpockBox self, String data) {
self.put("mock " + data)
}
}
def "should get a box of numbers"() {
given:
def box = demoSpock.createBoxOfNum()
expect:
box.size() == 4
box.pop() == "mock 3"
box.pop() == "mock 2"
box.pop() == "mock 1"
box.pop() == "mock zero"
verify("createBox").withTimes(1)
verify("putBox").withInOrder("1").withInOrder("2").withInOrder("3")
}
}

View File

@@ -0,0 +1,4 @@
Available Annotations
---
TO BE TRANSLATED

View File

@@ -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.

View File

@@ -0,0 +1,29 @@
How Mock Works
---
This document mainly introduces the design and implementation principles of the mock function in `TestableMock`.
Unlike common mock-tools which write mock definitions in each test case, `TestableMock` allows each business class to provide its own set of mock methods, describing what should be mocked during testing, and define the corresponding replacement logic (that is, each business class has its own independent "Test class" and independent "Mock class"). By adopting "convention is better than configuration" principle, `TestableMock` not only reduce redundant codes but also reduce the cost of mock learning.
This design is based on two basic assumptions:
1. In any business class, methods that need to be mocked in one test case are usually required to be mocked in other test cases. For these mocked methods usually requiring external dependencies that are not easy to test.
2. Each unit test only focuses on the logic inside the unit under test, and irrelevant calls outside the unit should be replaced with mocks. That is, the invocations that need to be mocked should all be in the code of the class under test.
Accordingly, the unit test scenarios that meet the above assumptions are simplified through conventions, and the remaining more complex use scenarios are supported through configuration.
The mechanism of `TestableMock` can be summarized in one sentence: <u>Using java agent to dynamically modify the bytecode, before the unit test is about to run, replace all invocations in the business class under test which match the mock method definition with invocations to the mock method itself.</u>.
The final effect is that no matter what service framework or object-container the code uses, no matter whether the object of mock target is injected by a framework, created by new operation, and whether the target method of mock is private or external, defined as global, local, static, inherited or overloaded, all can be mocked in a same and simple way, which make unit testing much easier.
> Notice: Mock's goal is the method invocation in the class under test. The code inside the test case will not be mocked, and the method definition itself has not changed, but the invocation code to those methods will be replaced.
Specifically, when the unit test is started, `TestableMock` will preprocess the classes loaded into memory and establish the association relationship between the "class under test", the "test class", and the "mock container class" respectively (can be one-to-one or many-to-one). On the one hand, this association is to correctly match the mock call and replace it before the test case is executed, and on the other hand, it is used to control the effective scope of the mock method.
For the class under test, replace the matched call with a call to the mock container method.
For the test class, insert the mock context initialization code at the beginning of each test case.
For the mock container class, add the `testableIns()` method to make the class become a singleton class, and insert codes to record the call at the beginning of each Mock method.
The above is the core logic of the entire mocking logic. For more implementation details, please refer to the source code. If you have any questions, suggestions, or improvement proposals, you are welcome to participate in the discussion and contribute through Github Issue and Pull Request 😃

View File

@@ -11,7 +11,15 @@ Create the class under test object directly, and then use the ability of `Testab
For mock tools that modify the class loader or the bytecode of the class under test, such as `PowerMock` and `JMockit`, there is no yet case to prove that they will conflict with `TestableMock`, but in principle, there may be a risk of incompatibility between the two, Please use with caution.
#### 3. How to implement the mock method when the parent class variable points to the child class object?
#### 3. Which test frameworks does `TestableMock` support?
The `PrivateAccessor` / `OmniConstructor` / `OmniAccessor` and basic functionality of `TestableMock` are theoretically test-frameworks-independent, and should work with any test frameworks out-of-box.
The only test-framework-related part is the mock invocation verifier, which currently supports `JUnit 4` / `JUnit 5` / `TestNG` and `Spock` appropriately.
If you encounter compatibility problem under a specific test framework, or want to add mock invocation verifier support for any other frameworks, please raise an [issue](https://github.com/alibaba/testable-mock/issues) to tell us.
#### 4. How to implement the mock method when the parent class variable points to the child class object?
In the code, there are often cases of using <u>interface variables or parent class variables</u> to point to an instance of subclasses and calling methods provided by the parent class or subclass.
@@ -21,7 +29,7 @@ Therefore, regardless of whether the actually called method comes from the paren
See the use case of the `DemoInheritTest` test class in the Java and Kotlin examples.
#### 4. How to mock generic methods (template methods)?
#### 5. How to mock generic methods (template methods)?
Just use the same generic parameters directly on the mock method.
@@ -29,19 +37,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?
#### 6. 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.
#### 7. Why mocking methods in the `String` class in the Kotlin project does not work?
The `String` type in Kotlin language is actually `kotlin.String` instead of `java.lang.String`. However, when this type is built from bytecode, it will be replaced with Java's `java.lang.String` class, so no matter if the mock target is written as `kotlin.String` or `java.lang.String`, it cannot match the original called method.
In actual scenarios, there are very few scenarios where methods in the `String` class need to be mocked, so `TestableMock` has not dealt with this situation specifically.
#### 6. Can `TestableMock` be used for testing Android projects?
#### 8. Can `TestableMock` be used for testing Android projects?
It can be used in combination with [Roboelectric](https://github.com/robolectric/robolectric) testing framework.
The `Dalvik` and `ART` virtual machines of the Android system use a bytecode system different from the standard JVM, which will affect the normal functionality of `TestableMock`. The `Roboelectric` framework can run Android unit tests on a standard JVM virtual machine, which is much faster than running unit tests through the Android virtual machine. Recently, most Android App unit tests are written with the `Roboelectric` framework.
#### 7. Meet "Command Line is too Long. Shorten command line for ..." error when triggering test in IntelliJ IDE?
#### 9. Meet "Command Line is too Long. Shorten command line for ..." error when triggering test in IntelliJ IDE?
This problem is caused by the system `Class Path` content is too long, and has nothing to do with `TestableMock`. However, it should be noted that IntelliJ provides two auxiliary solutions: `JAR manifest` and `classpath file`. If `TestableMock` is used in the test, please select `JAR manifest`.

View File

@@ -0,0 +1,4 @@
Available Global Arguments
---
TO BE TRANSLATED

View File

@@ -0,0 +1,4 @@
Reusing Mock Methods
---
TO BE TRANSLATED

View File

@@ -0,0 +1,117 @@
Parameter Constructor
---
In unit testing, the preparation and construction of test data is a necessary and tedious task. Object-oriented layer-by-layer encapsulation becomes an obstacle to initializing the state of the object during testing. Especially when the type structure is complicated, there is no suitable construction method, or some fields need to use private inner classes, etc. Using conventional methods to construct those class often appear to be inadequate.
For this reason, `TestableMock` provides two minimalist tool classes, `OmniConstructor` and `OmniAccessor`, which makes the construction of any object no longer difficult.
### 1. Construct any object with one line of code
No matter how special the target type is, `OmniConstructor` will hand it to you immediately~~ The universal object constructor `OmniConstructor` has two static methods:
- `newInstance(<AnyClass>)` ➜ Specify any type, and return an object of that type
- `newArray(<AnyClass>, <ArraySize>)` ➜ Specify any type, and return an array of that type
Usage example:
```java
// Construct a object of "WhatEverClass" type
WhatEverClass obj = OmniConstructor.newInstance(WhatEverClass.class);
// Construct a array of "WhatEverClass[]" type with capability of 5
WhatEverClass[] arr = OmniConstructor.newArray(WhatEverClass.class, 5);
```
Beside that, object constructed by `OmniConstructor` is not just a simple empty object with all member values of `null`, but a "fullness" object in which all members and all sub-members of all members have been recursively initialized. Compared with using `new` operation, `OmniConstructor` can ensure the integrity of the object structure and avoid the `NullPointerException` problem caused by partial initialization of test data.
```java
// Construct object using new operation
Parent parent = new Parent();
// Inner member is not initialized, will cause NullPointerException (❌)
parent.getChild().getGrandChild();
// Construct object using OmniConstructor
Parent parent = OmniConstructor.newInstance(Parent.class);
// No need to worry, visit any child member safely (✅)
parent.getChild().getGrandChild().getContent();
```
> **Note 1**: In the current version, the member fields of type interface or abstract class will still be initialized to `null`, this problem will be fixed in the future.
>
> **Note 2**: Based on the light-weight principle, in the default mode, `OmniConstructor` will only uses the original constructor of the class to create objects. For POJO and most model layer objects, this mode has been able to meet the needs.
> But for more complex situations, such as when certain class have constructors throwing exceptions or contain other statements that hinder the normal execution of the construction, the object construction may fail.
> In those situations, you can use the [Testable global configuration](en-us/doc/javaagent-args.md) `omni.constructor.enhance.enable = true` to enable bytecode enhancement mode of `OmniConstructor`, in this mode, any Java class can be constructed.
In addition to use as input parameters of the method under test, `OmniConstructor` can also be used to quickly construct the return value of the mock method. Compared to using `null` as the return value of the mock method, using a fully initialized object can better guarantee the reliability of the test .
In the `DemoOmniMethodsTest` test class of the `java-demo` and `kotlin-demo` sample projects, it is shown in detail how `OmniConstructor` could be used when the target type has a multi-layered nested structure, the construction method is throwing exception, and even without public construction method available.
### 2. Access any inner member with one line of code
For test data, even with complex structure, it is usually only part of its attributes and states that are related to a specific test case. However, it is sometimes not easy to assign values to these fields deep wrapped in the object structure.
As an enhanced version of the `PrivateAccessor` tool, `OmniAccessor` is inspired by the [XPath node selector](https://www.w3schools.com/xml/xpath_syntax.asp) in the `XML` language, It provides two main static methods of `get` and `set`:
- `get(arbitrary object, "access path")` ➜ returns all member objects searched for based on path-matching
- `set(arbitrary object, "access path", new value)` ➜ Assign a value to any objects based on path-matching
There is also a `getFirst()` method used to directly obtain the unique target object during exact path matching. Its function is equivalent to `OmniAccessor.get(...).get(0)`:
- `getFirst(arbitrary object, "access path")` ➜ returns the first member object searched based on path-matching
You only need to write the access path that meets the rules, no matter what type and depth of members, you can directly reach them with one line of code:
```java
// Get all field of the parent object, which named as content and inside type GrandChild
OmniAccessor.get(parent, "{GrandChild}/content");
// Assign 100 to any fields which named as value and inside any child member that matches the 3rd item of the array named children
OmniAccessor.set(parent, "children[2]/*/value", 100);
```
The path rules are as follows:
**1. Matching member name**
The path name without additional decoration will match any member object with the same name
- `child`: match any descendant member whose name is `child`
- `child/grandChild`: matches the child member named `grandChild` among the descendants of the name `child`
**2. Matching member type**
Use curly braces to match the type name, usually used to obtain or assign multiple member objects of the same type in batches
- `{Child}`: match all descendants of `Child`
- `{Children[]}`: match all descendants of the `Children` array
- `{Child}/{GrandChild}`: match all descendant members of `Child`, all types are children of `GrandChild`
The member name and type can be mixed on the path (currently it is not supported to specify the member name and type at the same time in the same level path)
- `child/{GrandChild}`: match all descendant members whose name is `child`, all types are child members of `GrandChild`
- `{Child}/grandChild/content`: match all descendant members whose type is `Child`, the child members named `grandChild`, and the child members named `content`
**3. Use subscripts to access array members**
Use square brackets with numerical values to indicate that the matching position is an array type, and the object with the specified subscript is taken (without subscript, when the matching object is an array type, all objects in the array are matched)
- `children[1]/content`: match the descendant members of the array type named `children`, and take the child member named `content` in the `2`th object
- `parent/children[1]`: match the child member of the array type named `children` among the descendant members named `parent`, and take the `2`th object among them
**4. Use wildcards**
Wildcards can be used to match member names or type names
- `child*`: match all descendant members whose name starts with `child`
- `{*Child}`: match all descendant members whose type ends with `Child`
- `c*ld/{Grand*ld}`: match the descendant members whose name starts with `c` and ends with `ld`, and the members whose type starts with `Grand` and ends with `ld`
- `child/*/content`: At this time, `*` will match any member, that is, the child member of `content` contained in any child member of the `child` object
For details, see the use cases in the test classes of the `java-demo` and `kotlin-demo` sample projects `DemoOmniMethodsTest`.
### 3. Special instructions
> **Do you really need to use `OmniAccessor`? **
>
> `OmniAccessor` implement the basic anti-refactoring mechanism based on the Fail-Fast principle. When the access path provided by the user cannot match any member, the `OmniAccessor` will immediately throw a `NoSuchMemberError` error, so that the unit test is terminated early. However, compared to the conventional member access method, the support of `OmniAccessor` in IDE refactoring is still weak.
>
> For content assignment of complex objects, in most cases, we recommend using [Builder Pattern](https://www.geeksforgeeks.org/builder-pattern-in-java/), or exposing Getter/Setter method implementations. Although these conventional methods are slightly clumsy (especially when you need to assign values to many similar members in batches), they are more friendly to the encapsulation and reconstruction of business logic.
> Only when the original type is not suitable for transformation, and there is no other way to access the target member, `OmniAccessor` is the last resort.

View File

@@ -1,6 +0,0 @@
Parameter constructor
---
No matter how intricate the parameter structure required by the method under test is, even there is no suitable construction method, or even there are private internal class objects... Call `TestableMock`, the parameter object will be handed to you immediately~
This feature is planned to be released in the `0.6` version.

View File

@@ -1,5 +1,43 @@
# Release Note
## 0.6.4
- `TestableNull` class removed, let `OmniConstructor` even lighter
- support `@Nested` annotation of JUnit 5
- fix several `OmniConstructor` compatibility issuesthanks to [@ddatsh](https://github.com/ddatsh)
## 0.6.3
- support custom inner `Mock` class name
- support skip specified packages from `OmniConstructor` enhancement
- fix 2 circle construction cases in `OmniConstructor`
- fix an index out-of-range issue caused by non-static method without `this` reference
- fix a null pointer exception caused by resource folder unavailable at runtime
## 0.6.2
- support setup global testable agent configure via properties file
- disable OmniConstructor bytecode enhancement by default
- fix a `Spring` framework compatibility issue with OmniConstructor
## 0.6.1
- generate mock scanning log file automatically, for self-troubleshooting
- fix a `Spock` test framework compatibility issue
- fix a `Gradle` unit test runtime compatibility issue
## 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

View File

@@ -0,0 +1,4 @@
Effective Scope Of Mocking
---
TO BE TRANSLATED

View File

@@ -1,12 +1,12 @@
Use TestableMock
---
`TestableMock` is an assist tool for Java unit testing based on source code and bytecode enhancement, including the following functions:
`TestableMock` is now not only a lightweight and easy-to-use unit testing mock tool, but also a comprehensive set of auxiliary tools aimed at **simplifying Java unit testing**, including the following functions:
- [Quickly mock arbitrary call](en-us/doc/use-mock.md): quickly replace any method invocation in the class under test with a mock method, solve the cumbersome use of traditional mock tools problem
- [Access private members of the class under test](en-us/doc/private-accessor.md): enable unit tests directly invoke or access private members of the class under test, solve the problems of private member initialization and private method testing
- [Quickly construct complicated parameter object](en-us/doc/omni-constructor.md)generate arbitrarily nested object instances, simplify their internal member assignment methods, solve the problem of long initialization codes for method parameters
- [Auxiliary test void method](en-us/doc/test-void-method.md): use the mock validator to check the internal logic of method, solve the problem that unit testing is difficult to implement to the method with no return value
- [Quickly construct complicated parameter object](en-us/doc/parameter-constructor.md)generate arbitrarily nested object instances, simplify their internal member assignment methods, solve the problem of long initialization codes for method parameters
## Use in Maven project
@@ -16,7 +16,7 @@ It is recommended to add a `property` field that identifies the TestableMock ver
```xml
<properties>
<testable.version>0.5.0</testable.version>
<testable.version>0.6.4</testable.version>
</properties>
```
@@ -63,8 +63,8 @@ Add dependence of `TestableMock` in `build.gradle` file:
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.5.0')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.0')
testImplementation('com.alibaba.testable:testable-all:0.6.4')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.4')
}
```
@@ -93,3 +93,20 @@ See the [build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo
> ```
>
> See [issue-43](https://github.com/alibaba/testable-mock/issues/43) for a complete example.
> If the project is using `Spock` test framework, you need to specify the bytecode version generated by `Groovy` to be 1.6 or above, the method is as follows (please modify the properties value according to the actual JVM version used).
>
> For Maven project, add `<maven.compiler.source>` and `<maven.compiler.target>` properties inside the `pom.xml` file, e.g.
> ```xml
> <properties>
> <maven.compiler.source>1.6</maven.compiler.source>
> <maven.compiler.target>1.6</maven.compiler.target>
> </properties>
> ```
>
> For Gradle project, add a `sourceCompatibility` property inside the `build.gradle` file, e.g.
> ```groovy
> sourceCompatibility = '6'
> ```
>
> See project `demo/spock-demo` for a complete example.

View File

@@ -3,84 +3,79 @@ 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.
For this reason, `TestableMock` will automatically save the mock scanning log of the last test run in the project build directory. The default location is `target/testable-agent.log` (Maven project) or `build/testable-agent.log` (Gradle project).
```java
@MockWith(diagnose = MockDiagnose.ENABLE)
class DemoTest {
...
}
```
The output log example is as follows:
Examples of log content are as follows:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
[DIAGNOSE] Handling method <init>
[DIAGNOSE] Handling method newFunc
[DIAGNOSE] Line 19, mock method "createBlackBox" used
[DIAGNOSE] Handling method outerFunc
[DIAGNOSE] Line 27, mock method "innerFunc" used
[DIAGNOSE] Line 27, mock method "staticFunc" used
[DIAGNOSE] Handling method commonFunc
[DIAGNOSE] Line 34, mock method "trim" used
[DIAGNOSE] Line 34, mock method "sub" used
[DIAGNOSE] Line 34, mock method "startsWith" used
[INFO] Start at Mon Jan 00 00:00:00 CST 0000
... ...
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
[INFO] Found 6 test cases
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[INFO] Found 8 mock methods
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
[INFO] Found method <init>
[INFO] Found method newFunc
[INFO] Line 19, mock method "createBlackBox" used
[INFO] Found method outerFunc
[INFO] Line 27, mock method "innerFunc" used
[INFO] Line 27, mock method "staticFunc" used
[INFO] Found method commonFunc
[INFO] Line 34, mock method "trim" used
[INFO] Line 34, mock method "sub" used
[INFO] Line 34, mock method "startsWith" used
... ...
[INFO] Completed at Mon Jan 00 00:00:00 CST 0000
```
The log shows all the mocked invocation and corresponding code line numbers in the class under test.
- Self troubleshooting:
According to the targeted test classes, below are some simple clues for self-troubleshooting. Suppose the class under test is "com.demo.BizService", the test class is "com.demo.BizServiceTest", and the mock container class is "com.demo.BizServiceTest.Mock":
- If there is no output, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies
- If only the first line of `Handling test class` is output, please check whether the test class is in the same package of the class under test, and the name is "<ClassUnderTest>+Test" (required for `0.4.x` version)
- If `Handling source class` and `Handling method xxx` are output, but there is no mock replacement happen at the expected code line, please check whether the mock method definition matches the target method
- If the log file is no generated, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies
- If only `com/demo/BizServiceTest$Mock` is found in the output, please check whether the mock class is created at correct place
- If both the test class and the mock class are found, but the class under test `com/demo/BizService` not appeared, 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 all the three classes are found, and `Found method xxx` are output, but there is no mock replacement happen at the expected code line, please check whether the mock method definition matches the target method
For situations where expected mocking is not take effect, you could set the diagnosis level to `MockDiagnose.VERBOSE` for further investigation information.
For situations where expected mocking is not take effect, you could add a `@MockDiagnose` annotation to the mock class, and set the diagnosis level to `LogLevel.VERBOSE` for further investigation information.
```java
@MockWith(diagnose = MockDiagnose.VERBOSE)
class DemoTest {
...
class BizServiceTest {
@MockDiagnose(LogLevel.VERBOSE)
public static class Mock {
...
}
}
```
Executing the unit test again will print out the runtime-signatures of all mock methods, and the runtime-signatures of all invocations scanned in the class under test:
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/DemoMockTest
[VERBOSE] Mock constructor "createBlackBox" as "(Ljava/lang/String;)V" for "com/alibaba/testable/demo/model/BlackBox"
[VERBOSE] Mock method "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
[VERBOSE] Mock method "staticFunc" as "()Ljava/lang/String;"
[VERBOSE] Mock method "trim" as "()Ljava/lang/String;"
[VERBOSE] Mock method "sub" as "(II)Ljava/lang/String;"
[VERBOSE] Mock method "startsWith" as "(Ljava/lang/String;)Z"
[VERBOSE] Mock method "secretBox" as "()Lcom/alibaba/testable/demo/model/BlackBox;"
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
[DIAGNOSE] Handling method <init>
[VERBOSE] Line 13, constructing "java/lang/Object" as "()V"
[DIAGNOSE] Handling method newFunc
[VERBOSE] Line 19, constructing "com/alibaba/testable/demo/model/BlackBox" as "(Ljava/lang/String;)V"
[DIAGNOSE] Line 19, mock method "createBlackBox" used
[VERBOSE] Line 19, invoking "createBlackBox" as "(Ljava/lang/String;)Lcom/alibaba/testable/demo/model/BlackBox;"
[VERBOSE] Line 20, invoking "get" as "()Ljava/lang/String;"
[DIAGNOSE] Handling method outerFunc
[VERBOSE] Line 27, constructing "java/lang/StringBuilder" as "()V"
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
[DIAGNOSE] Line 27, mock method "innerFunc" used
[VERBOSE] Line 27, invoking "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "staticFunc" as "()Ljava/lang/String;"
[DIAGNOSE] Line 27, mock method "staticFunc" used
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "toString" as "()Ljava/lang/String;"
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
[TIP] Test case "should_mock_new_object"
... ...
[TIP] Test case "should_set_mock_context"
[INFO] Found 6 test cases
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[TIP] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[TIP] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
... ...
[TIP] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[INFO] Found 8 mock methods
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
[INFO] Found method <init>
[TIP] Line 13, constructing "java.lang.Object()"
[INFO] Found method newFunc
[TIP] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[INFO] Line 19, mock method "createBlackBox" used
[TIP] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
[TIP] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
[INFO] Found method outerFunc
[TIP] Line 27, constructing "java.lang.StringBuilder()"
[TIP] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
[TIP] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[INFO] Line 27, mock method "innerFunc" used
... ...
```
@@ -90,3 +85,7 @@ The logs are formatted in follow pattern:
- `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
> In order to clearly distinguish between the type of method return value and the type of invoker, the method signature recorded in the log uses a method definition structure similar to `Kotlin`.
Comparing the actual signature of the original call with the signature defined by the mock method, the problem is usually found quickly.

View File

@@ -1,13 +1,31 @@
Upgrade to version 0.5
Upgrade Guide
---
After nearly a month of design and development, the `0.5` version of TestableMock has finally come out. Compared with the `0.4` version, the new version solves the three historical problems left over before:
### Upgrade to v0.6
In version `0.6``TestableMock` provided a new [quick complicated class construction](en-us/doc/omni-constructor.md) capability. Meanwhile, it includes a modification that is incompatible with the `0.5` version: class `PrivateAccessor` have been moved from `com.alibaba.testable.core.accessor` package to `com.alibaba.testable.core.tool` package.
If you are using `PrivateAccessor` and having import statement like this:
```java
import com.alibaba.testable.core.accessor.PrivateAccessor;
```
When upgrade `TestableMock` to `0.6` version, please replace it as
```java
import com.alibaba.testable.core.tool.PrivateAccessor;
```
### Upgrade to v0.5
The `0.5` version solves the three historical problems left over before:
1. <s>**Mock method cannot call other non-static methods**</s>. The mock method in the new version no longer has any difference from the ordinary method, and can access any external method and member variable.
2. <s>**Mock method always acts on the entire test life cycle**</s>. From now on, the mock method supports restricting the effective scope to the test cases in the test class of which it belongs, so there is no need to worry about accidentally mocking cross-class test invocations.
3. <s>**The MOCK_CONTEXT needs manually cleaned up and only supports class-level parallel testing**</s>. Now each test case has an independent `MOCK_CONTEXT` variable, no need to clean up after used, and you can use unit test with any parallel level.
In order to better realize the reuse of Mock methods, we have made a clear boundary between the mock class and the test class in the new version. When upgrading from `0.4` to `0.5`, the only change required is to wrap all mock methods in the test class with a `public static class Mock {}`.
In order to better realize the reuse of Mock methods, the version `0.5` have made a clear boundary between the mock class and the test class in the new version. When upgrading from `0.4` to `0.5`, the only change required is to wrap all mock methods in the test class with a `public static class Mock {}`.
For example, the original test class definition was as follows:
@@ -19,7 +37,7 @@ public class DemoMockTest {
}
@Test
void should_able_to_mock_member_method() throws Exception {
void should_mock_member_method() throws Exception {
assertEquals("hello_world", demoMock.outerFunc());
verify("innerFunc").with("world");
}
@@ -39,11 +57,11 @@ public class DemoMockTest {
} // Add this line
@Test
void should_able_to_mock_member_method() throws Exception {
void should_mock_member_method() throws Exception {
assertEquals("hello_world", demoMock.outerFunc());
verify("innerFunc").with("world");
}
}
```
Then upgrade the `TestableMock` dependency in the `pom.xml` or `build.gradle` file to `0.5.0` or above.
Finally, upgrade the `TestableMock` dependency in `pom.xml` or `build.gradle` file to the new version.

View File

@@ -0,0 +1,4 @@
Use MockWith Annotation
---
TO BE TRANSLATED

View File

@@ -1,7 +1,9 @@
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 unit testing, the main role of the mock method is to replace those methods with **need external dependencies**, **with time-consuming**, **has random results**, or other effects that affect the development of the test, but do not affect the key the logic to be tested. Generally speaking, a certain call needs to be mocked, which is usually only related to its own characteristics, and has nothing to do with the source of the invocation.
Based on the above information, `TestableMock` has designed a minimalist mock mechanism. Unlike the common mock tools that uses **class** as the definition granularity of mocking, and repeats the description of mock behavior in each test case, `TestableMock` allows each business class (class under test) to be associated with a set of reusable collection of mock methods (carried by the mock container class), following the principle of "convention over configuration", and 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 mock class, add a `@MockMethod` annotation
@@ -9,7 +11,7 @@ Compared with the class-granularity mocking practices of existing mock tools, `T
The detail mock method definition convention is as follows.
#### 0. Pre-step, prepare the mock class
### 0. Pre-step, prepare the mock class
First, create a mock class as the container for mock methods associated with the test class. The simplest way is to add a static inner class named `Mock` to the test class. E.g:
@@ -23,7 +25,7 @@ public class DemoTest {
}
```
#### 1. Mock method calls of any class
### 1.1 Mock method calls of any class
Define an ordinary method annotated with `@MockMethod` in the mock class with exactly the same signature (name, parameter, and return value type) as the method to be mocked, and then add the type of target object (which the method originally belongs to) as `targetMethod` parameter of `@MockMethod` annotation.
@@ -55,7 +57,7 @@ private String use_any_mock_method_name(int i, int j) {
Sometimes, the mock method need to access the member variables in the original object that initiated the invocation, or invoke other methods of the original object. At this point, you can remove the `targetClass` parameter in the `@MockMethod` annotation, and then add a extra parameter whose type is the original object type of the method to the first index of the method parameter list.
The `TestableMock` convention is that when the `targetClass` parameter value of the `@MockMethod` annotation is empty, the first parameter of the mock method is the type of the target method, and the parameter name is arbitrary. In order to facilitate code reading, it is recommended to name this parameter as `self` or `src`. Example as follows:
The `TestableMock` convention is that when the `targetClass` parameter of the `@MockMethod` annotation is not defined, the first parameter of the mock method is the type of the target method, and the parameter name is arbitrary. In order to facilitate code reading, it is recommended to name this parameter as `self` or `src`. Example as follows:
```java
// Adds a `String` type parameter to the first position the mock method parameter list (parameter name is arbitrary)
@@ -67,9 +69,9 @@ private String substring(String self, int i, int j) {
}
```
For complete code examples, see the `should_able_to_mock_common_method()` test cases in the `java-demo` and `kotlin-demo` sample projects. (Because Kotlin has made magical changes to the String type, the method under test in the Kotlin example adds a layer of encapsulation to the `BlackBox` class)
For complete code examples, see the `should_mock_common_method()` test cases in the `java-demo` and `kotlin-demo` sample projects. (Because `Kotlin` has made magical changes to the String type, the method under test in the `Kotlin` example adds a layer of encapsulation to the `BlackBox` class)
#### 2. Mock the member method of the class under test itself
### 1.2 Mock the member method of the class under test itself
Sometimes, when testing certain methods, it is desirable to mock out some other member methods of the class under test itself.
@@ -87,9 +89,9 @@ private String innerFunc(String text) {
Similarly, if the method in the above example needs to access the original tested object that initiated the call, it may not use the `targetClass` parameter, but when defining the mock method, add a parameter of type `DemoMock` to the first index of the method parameter list.
For complete code examples, see the `should_able_to_mock_member_method()` test case in the `java-demo` and `kotlin-demo` sample projects.
For complete code examples, see the `should_mock_member_method()` test case in the `java-demo` and `kotlin-demo` sample projects.
#### 3. Mock static methods of any class
### 1.3 Mock static methods of any class
Mock for static methods is the same as for any ordinary methods.
@@ -102,9 +104,9 @@ private BlackBox secretBox() {
}
```
For complete code examples, see the `should_able_to_mock_static_method()` test case in the `java-demo` and `kotlin-demo` sample projects.
For complete code examples, see the `should_mock_static_method()` test case in the `java-demo` and `kotlin-demo` sample projects.
#### 4. Mock `new` operation of any type
### 1.4 Mock `new` operation of any type
Define an ordinary method annotated with `@MockContructor` in the mock class, make the return value type of the method the type of the object to be created, and the method parameters are exactly the same as the constructor parameters to be mocked, the method name is arbitrary.
@@ -123,9 +125,9 @@ private BlackBox createBlackBox(String text) {
> You can still use the `@MockMethod` annotation, and configure the `targetMethod` parameter value to `"<init>"`, and the rest is the same as above. The effect is the same as using the `@MockContructor` annotation
For complete code examples, see the `should_able_to_mock_new_object()` test case in the `java-demo` and `kotlin-demo` sample projects.
For complete code examples, see the `should_mock_new_object()` test case in the `java-demo` and `kotlin-demo` sample projects.
#### 5. Identify different invocation source in mock method
### 2. Identify different invocation source in mock method
In the mock method, you can use the `TestableTool.SOURCE_METHOD` variable to identify **the method name of the class under test before entering the mock method**; in addition, the `TestableTool.MOCK_CONTEXT` variable can **inject additional context parameters into the mock method**, to distinguish and process different calling scenarios.
@@ -157,11 +159,21 @@ private Data mockDemo() {
}
```
For complete code examples, see the `should_able_to_get_source_method_name()` and `should_able_to_get_test_case_name()` test cases in the `java-demo` and `kotlin-demo` sample projects.
For complete code examples, see the `should_get_source_method_name()` and `should_get_test_case_name()` test cases in the `java-demo` and `kotlin-demo` sample projects.
#### 6. Verify the sequence and parameters of the mock method being invoked
### 3. Verify the sequence and parameters of the mock method being invoked
In test cases, you can use the `TestableTool.verify()` method, and cooperate with `with()`, `withInOrder()`, `without()`, `withTimes()` and other methods to verify the mock call situation.
In test cases, you can use the `InvokeVerifier.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.
### 4. Special instructions
> **Naming Conventions for Test Classes and Mock Containers**
>
> By default, `TestableMock` assumes that the <u>package path of the test class and the class under test are the same, and the name is `<ClassUnderTest>+Test`</u> (usually Java project with `Maven` or `Gradle` conform to this convention).
> At the same time, it is agreed that the mock container associated with the test class is <u>in its internal static class named `Mock`</u>, or <u>an independent class named `<ClassUnderTest>+Mock` under the same package path</u>.
>
> When the test class or mock container path does not follow to this convention, you can use the `@MockWith` annotation to specify it explicitly. For details, see [Use MockWith Annotation](en-us/doc/use-mock-with.md).
For more implementation details of `TestableMock`, please refer to the [Design and Principle](en-us/doc/design-and-mechanism.md) document.

View File

@@ -2,18 +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 To 0.5 Version](en-us/doc/upgrade-to-v05.md)
- [Upgrade Guide](en-us/doc/upgrade-guide.md)
- [Available Global Arguments](zh-cn/doc/javaagent-args.md)
- [Available Annotations](zh-cn/doc/annotations.md)
- [How Mock Works](zh-cn/doc/design-and-mechanism.md)
- [Mock Tools Comparison](en-us/doc/comparation.md)
- [Release Note](en-us/doc/release-note.md)
- [About Us](en-us/doc/about-us.md)

View File

@@ -5,7 +5,7 @@ TestableMock简介
在定义Mock方法时开发者真正关心的只有一件事"<u>这个调用在测试的时候要换成那个假的Mock方法</u>"。
然而当下主流的Mock框架在实现Mock功能时需要开发者操心的事情实在太多Mock框架如何初始化、与所用的服务框架是否兼容、要被Mock的方法是不是私有的、是不是静态的、被Mock对象是new出来的还是注入的、怎样把被测对象送回被测类里...这些非关键的额外工作极大分散了使用Mock工具应有的乐趣。
当下主流的Mock框架在实现Mock功能时需要开发者操心的事情实在太多Mock框架如何初始化、与所用的服务框架是否兼容、要被Mock的方法是不是私有的、是不是静态的、被Mock对象是new出来的还是注入的、怎样把被测对象送回被测类里...这些非关键的额外工作极大分散了使用Mock工具应有的乐趣。
于是,我们开发了`TestableMock`**一款特立独行的轻量Mock工具**。

View File

@@ -47,7 +47,6 @@
| --- | --- | --- | ---- | --- |
| N/A | Class | 否 | NullType.class | 指定使用的Mock容器类 |
| treatAs | ClassType | 否 | ClassType.GuessByName | 指定当前类是测试类或被测类 |
| diagnose | LogLevel | 否 | N/A | (**deprecated**)指定Mock诊断日志级别 |
#### @MockDiagnose

View File

@@ -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编写习惯会带来一定改变。

View 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参与讨论、贡献😃

View File

@@ -1,9 +1,13 @@
常见使用问题
---
#### 1. 如何Mock被测类中通过`@Autowired`初始化的字段?
#### 1. 如何初始化被测类中通过`@Autowired`或`@Resource`注入的私有字段?
直接创建被测类对象,然后利用`TestableMock`访问私有成员的能力直接给这些字段赋值即可
若该对象的方法在测试时需要被Mock则无需初始化
若测试运行时需用到该对象的真实调用,则可以在测试类的构造方法内直接创建对象,然后利用`TestableMock`访问私有成员的能力给这些字段赋值。
对于JUnit框架还可以使用`@RunWith(SpringRunner.class)`注解将Spring上下文启动起来然后在测试类里用`@Resource`把需要依赖对象注入进来,再利用`PrivateAccessor`赋值给被测类的私有字段。
#### 2. `TestableMock`是否能够与其他Mock工具一起使用
@@ -11,7 +15,15 @@
对于会修改类加载器或被测类字节码的Mock工具譬如`PowerMock``JMockit`,尚无案例证明会与`TestableMock`发生冲突,但从原理来说二者可能存在不兼容风险,请谨慎使用。
#### 3. 父类变量指向子类对象时如何实现Mock方法
#### 3. `TestableMock`支持哪些测试框架
`TestableMock``PrivateAccessor`/`OmniConstructor`/`OmniAccessor`以及基础Mock功能均与测试框架无关理论上适用于任何测试框架。
唯独Mock方法的调用校验器是与测试框架相关的目前已支持`JUnit 4`/`JUnit 5`/`TestNG`/`Spock`四款主流框架。
若亲遇到在特定测试框架下的兼容问题,或希望增加其他框架的支持,请通过[Issues](https://github.com/alibaba/testable-mock/issues)告诉我们。
#### 4. 父类变量指向子类对象时如何实现Mock方法
在代码中,经常会有使用<u>接口变量或父类变量</u>指向子类实例,调用父类或子类方法的情况。
@@ -21,7 +33,7 @@
参见Java和Kotlin示例中`DemoInheritTest`测试类的用例。
#### 4. 如何Mock对于泛型方法(模板方法)
#### 5. 如何Mock对于泛型方法(模板方法)
与普通方法的Mock方法相同直接在Mock方法上使用相同的泛型参数即可。
@@ -29,19 +41,25 @@
> 由于JVM存在泛型擦除机制对于Java项目也可以直接使用`Object`类型替代泛型参数见Java版`DemoTemplateTest`测试类中被注释掉的"第二种写法"示例。
#### 5. 在Kotlin项目对`String`类中的方法进行Mock不生效
#### 6. 如何Mock在内部类代码里的调用
在其所在外部类对应的Mock容器中定义所需的Mock方法即可。
参见Java和Kotlin示例中`DemoInnerClass`测试类的用例。
#### 7. 在Kotlin项目对`String`类中的方法进行Mock不生效
Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.String`。但在构建生成自字节码的时候又会被替换为Java的`java.lang.String`因此无论将Mock目标写为`kotlin.String``java.lang.String`均无法正常匹配到原始的被调用方法。
实际场景中需要对`String`类中的方法进行Mock的场景很少`TestableMock`暂未对这种情况做特别处理。
#### 6. `TestableMock`能否用于Android项目的测试
#### 8. `TestableMock`能否用于Android项目的测试
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。
Android系统的`Dalvik``ART`虚拟机采用了与标准JVM不同的字节码体系会影响`TestableMock`的正常工作。`Roboelectric`框架能在普通JVM虚拟机上运行Android单元测试其速度比通过Android虚拟机运行单元测试快非常多绝大多数Android App的单元测试都在使用`Roboelectric`框架。
#### 7. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
#### 9. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
这个问题是由于系统ClassPath包含太多路径所致与是否使用`TestableMock`无关。但需要注意的是IntelliJ提供了两种辅助解决机制`JAR manifest``classpath file`,若测试中使用了`TestableMock`,请选择`JAR manifest`

View File

@@ -1,15 +1,43 @@
全局运行参数
---
`TestableMock`的许多功能采用了基于JavaAgent的运行时字节码修改实现。在JavaAgent启动时可以通过额外的全局参数来调整`TestableMock`的执行过程。
`TestableMock`的许多功能采用了基于JavaAgent的运行时字节码修改实现。在JavaAgent启动时可以通过两种方式调整`TestableMock`的执行过程。
## 1.使用全局配置文件
在项目的`src/test/resources`目录下创建一个名为`testable.properties`的文本文件,其中每行为一条配置项,配置项的名称与值之间用`=`连接。
对于大多数情况,我们更推荐使用配置文件来调节`TestableMock`的行为这种方式相比Agent参数更加易读。
### 可用配置项
| 配置项 | 描述 | 可用值和示例(`N/A`表示无需赋值) |
| ---- | ---- | ---- |
| dump.path | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 相对项目根目录的位置,例如:`target/bytecode` |
| enhance.pkgPrefix.includes | 限定`TestableMock`仅对部分包生效 | 使用`,`分隔的包路径前缀列表,例如:`com.demo.svc,com.demo.dao` |
| log.file | 指定TestableAgent日志文件位置 | 相对项目根目录的位置,例如:`target/testable/agent.log`,特殊值`null`表示禁用日志文件 |
| log.level | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
| mock.innerClass.name | 修改测试类中的内置Mock容器类名 | 任意符合Java类名的值默认值为`Mock` |
| mock.scope.default | 修改默认的Mock生效范围详见[Mock生效范围](zh-cn/doc/scope-of-mock) | 可用值为:`global`(全局生效) / `associated`(只对关联的测试用例生效) |
| omni.constructor.enhance.enable | 是否启用`OmniConstructor`的字节码增强模式 | 可用值为:`true` / `false` |
| omni.constructor.enhance.pkgPrefix.excludes | 对特定包禁用`OmniConstructor`的字节码增强模式 | 使用`,`分隔的包路径前缀列表,例如:`com.demo.model` |
| thread.pool.enhance.enable | 是否启用基于`TransmittableThreadLocal`的Mock上下文存储 | 可用值为:`true` / `false` |
示例见`java-demo``kotlin-demo`项目中的`testable.properties`文件。
## 2.使用全局Agent参数
与其它基于JavaAgent的工具相似`TestableMock`也支持通过在引入`testable-agent`包时,在末尾加上一个`=`符号,然后连接额外参数来传递用户的自定义参数。
### 可用参数清单
| 参数 | 描述 | 可用值和示例(`N/A`表示无需赋值) |
| ---- | ---- | ---- |
| configFile | 修改全局配置文件位置 | 相对项目根目录的位置,默认值为`src/test/resources/testable.properties` |
| logLevel | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
| dumpPath | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 例如 `/tmp/bytecode`(需要此目录事先存在) |
| pkgPrefix | 限定`TestableMock`仅对部分包生效 | 使用`,`分隔的包路径前缀列表,例如 `com.demo.svc,com.demo.dao` |
| logFile | 指定TestableAgent日志文件位置 | 相对项目根目录的位置,例如`target/testable/agent.log`,特殊值`null`表示禁用日志文件 |
| dumpPath | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 相对项目根目录的位置,例如`target/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` |
@@ -25,13 +53,11 @@
### 添加运行参数
与其他基于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&amp;pkgPrefix=com.demo.</argLine>
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar=mockScope=associated&amp;pkgPrefix=com.demo</argLine>
</configuration>
```
@@ -39,7 +65,6 @@
对于Gradle项目同样是直接将参数追加到引入`testable-agent`的配置末尾。例如:
```groovy
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}=mockScope=associated&pkgPrefix=com.demo."
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}=mockScope=associated&pkgPrefix=com.demo"
```

View File

@@ -0,0 +1,117 @@
快速构造复杂的参数对象
---
在单元测试中,测试数据的准备和构造是一件既必须又繁琐的任务,面向对象的层层封装,在测试时就成为了初始化对象状态的重重阻碍。尤其遇到类型结构嵌套复杂、没有合适的构造方法、需要使用私有内部类等等状况时,常规手段往往显得力不从心。
为此`TestableMock`提供了`OmniConstructor``OmniAccessor`两个极简的工具类,从此让一切对象构造不再困难。
### 1. 一行代码构造任何对象
不论目标类型多么奇葩,呼唤`OmniConstructor`,马上递给您~ 万能的对象构造器`OmniConstructor`有两个静态方法:
- `newInstance(任意类型)` ➜ 指定任意类型,返回一个该类型的对象
- `newArray(任意类型, 数组大小)` ➜ 指定任意类型,返回一个该类型的数组
用法举例:
```java
// 构造一个WhatEverClass类型的对象
WhatEverClass obj = OmniConstructor.newInstance(WhatEverClass.class);
// 构造一个WhatEverClass[]类型容量为5的数组
WhatEverClass[] arr = OmniConstructor.newArray(WhatEverClass.class, 5);
```
不仅如此,`OmniConstructor`构造出的绝非是所有成员值为`null`的简单空对象,而是所有成员、以及所有成员的所有子成员,都已经依次递归初始化的"丰满"对象。相比使用`new`进行构造,`OmniConstructor`能够确保对象结构完整,避免测试数据部分初始化导致的`NullPointerException`问题。
```java
// 使用构造函数创建对象
Parent parent = new Parent();
// 内部成员未初始化直接访问报NullPointerException异常
parent.getChild().getGrandChild();
// 使用OmniConstructor创建对象
Parent parent = OmniConstructor.newInstance(Parent.class);
// 无需顾虑,安心访问任意子成员(✅)
parent.getChild().getGrandChild().getContent();
```
> **注意 1** :在当前版本中,类型为接口或抽象类的成员字段依然会被初始化为`null`,此问题将在后续版本修复
>
> **注意 2** :基于轻量优先原则,默认模式下,`OmniConstructor`仅利用类型原有的构造方法来创建对象对于绝大多数POJO和`Model`层对象而言这种模式已经能够满足需要。
> 但对于更复杂的情形,譬如遇到某些类型的构造方法会抛出异常或包含其他妨碍构造正常执行的语句时,对象构造可能会失败。
> 此时可通过[Testable全局配置](zh-cn/doc/javaagent-args.md)`omni.constructor.enhance.enable = true`启用`OmniConstructor`的字节码增强模式在该模式下任何Java类型皆可构造。
除了用于构造方法的入参,`OmniConstructor`也可以用于快速构造Mock方法的返回值相比将`null`作为Mock方法的返回值使用完全初始化的对象能够更好保障测试的可靠性。
`java-demo``kotlin-demo`示例项目的`DemoOmniMethodsTest`测试类中,详细展示了当目标类型有多层嵌套结构、构造方法无法正常使用,甚至没有公开的构造方法时,如何用`OmniConstructor`轻松创建所需对象。
### 2. 一行代码访问任意深度成员
对于测试数据而言,即使是结构复杂的参数对象,与特定测试用例有关的通常也只是其中的部分属性和状态,然而要为这些深藏在对象结构内部的字段赋值有时却并非易事。
做为`PrivateAccessor`功能的加加加强版,`OmniAccessor`的灵感来自于`XML`语言中的[XPath节点选择器](https://www.w3school.com.cn/xpath/xpath_syntax.asp),它提供了`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`才是最后的终极手段。

View File

@@ -1,6 +0,0 @@
快速构造复杂入参
---
不论被测方法所需的参数结构多么错综复杂、甚至没有合适的构造方法、甚至需要私有内部类对象... 呼唤TestableMock马上递给您~
计划在`0.6`版本中推出。

View File

@@ -5,31 +5,9 @@
此外,在单元测试中时常会需要对被测对象进行特定的成员字段初始化,但有时由于被测类的构造方法限制,使得无法便捷的对这些字段进行赋值。那么,能否在不破坏被测类型封装的情况下,允许单元测试用例内的代码直接访问被测类的私有方法和成员字段呢?`TestableMock`提供了两种简单的解决方案。
### 方法一:使用`@EnablePrivateAccess`注解
### 1.1 使用`PrivateAccessor`工具类
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力
- 调用**被测类**的私有方法(包括静态方法)
- 读取**被测类**的私有字段(包括静态字段)
- 修改**被测类**的私有字段(包括静态字段)
- 修改**被测类**的常量字段使用final修饰的字段包括静态字段
访问和修改私有、常量成员时IDE可能会提示语法有误但编译器将能够正常运行测试。使用编译期代码增强目前仅实现了Java语言的适配
效果见`java-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
> 此功能默认假设测试类与被测类同包,且名称为`被测类+Test`。当不符合此约定时,可在测试类的`@EnablePrivateAccess`注解上使用`srcClass`参数指定实际的被测类。例如:
>
> ```java
> @EnablePrivateAccess(srcClass = DemoServiceImpl.class)
> class DemoServiceTest() { ... }
> ```
### 方法二:使用`PrivateAccessor`工具类
若不希望看到IDE的语法错误提醒或是在非Java语言的JVM工程譬如Kotlin语言可以借助`PrivateAccessor`工具类来直接访问私有成员。
这个类提供了7个静态方法
第一种方法是借助`PrivateAccessor`工具类来直接访问私有成员。这个类提供7个静态方法
- `PrivateAccessor.get(任意对象, "私有字段名")` ➜ 读取任意类的私有字段
- `PrivateAccessor.set(任意对象, "私有字段名", 新的值)` ➜ 修改任意类的私有字段(或常量字段)
@@ -39,12 +17,38 @@
- `PrivateAccessor.invokeStatic(任意类型, "私有静态方法名", 调用参数...)` ➜ 调用任意类的**静态**私有方法
- `PrivateAccessor.construct(任意类型, 构造方法参数...)` ➜ 调用任意类的私有构造方法
> 使用`PrivateAccessor`工具类并不需要测试类具有`@EnablePrivateAccess`注解,但加上此注解将开启被测类私有成员的编译期校验功能
详见`java-demo``kotlin-demo`示例项目`DemoPrivateAccessorTest`测试类中的用例
详见`java-demo``kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
### 1.2 防代码重构机制
### 私有成员编译期校验
本质上来说,`PrivateAccessor`只是JVM反射机制的“易用型”封装因此会存在与JVM反射相同的“对代码重构不友好”问题。当被测类中的私有方法名称、参数进行了修改由于IDE无法自动订正反射访问的代码往往相关错误要在单元测试运行时才能被发现。
上述两种私有成员访问功能的本质都是利用了JVM的反射机制但JVM编译器不会检查反射目标的存在性当代码重构时如果对源类型中的私有方法名称、参数进行了修改就会导致错误要在单元测试运行时才能被发现。为此`@EnablePrivateAccess`注解的另一项功能是为**被测类**的私有成员访问进行额外的编译期校验
为此,`TestableMock``PrivateAccessor`进行了增强,赋予其编译期私有成员校验能力。这项功能默认关闭,需要通过`@EnablePrivateAccess`注解开启。(实际上是通过该注解的`verifyTargetOnCompile`参数控制,由于此参数默认值为`true`,因此只需在被测类上添加该注解即可启用私有成员校验
**注意**:当私有成员校验功能开启时,`PrivateAccessor`工具类将只能用于访问**被测类**的私有成员,这会有助于限制将`PrivateAccessor`工具类用于与当前测试无关的“越权”操作。如果确实需要访问其他类的私有成员,可将`@EnablePrivateAccess`注解移除,或将注解的`verifyTargetOnCompile`参数设为`false`,手工关闭校验功能。
**注意 1**:当私有成员校验功能开启时,`PrivateAccessor`工具类将只能用于访问**被测类**的私有成员,从某种角度而言,这也有助于限制将`PrivateAccessor`工具类用于与当前测试无关的“越权”操作。
**注意 2**`TestableMock`默认约定测试类与**被测类**的包路径相同,且名称为`被测类+Test`。若测试类名称不符合此约定时,在使用`@EnablePrivateAccess`注解时,需用`srcClass`参数显式指明实际的**被测类**位置。
**注意 3** 此机制目前只针对`Java`语言实现,对于`Kotlin`以及其他JVM方言均无效。
> 将`DemoPrivateAccessorTest`示例代码稍加修改,添加`@EnablePrivateAccess`注解(注意此时测试类名不符合约定,需加`srcClass`参数):
>
> ```java
> @EnablePrivateAccess(srcClass = DemoPrivateAccess.class) // <- 添加此行
> class DemoPrivateAccessorTest() { ... }
> ```
>
> 然后将任意一处通过`PrivateAccessor`访问的目标名称改为实际不存在的成员名,再次编译时即可发现该行有编译错误,提示信息为访问目标不存在。
### 2.1 直接访问私有成员
第二种方法,除了借助`PrivateAccessor`工具类以外,凡是使用了`@EnablePrivateAccess`注解的测试类还会被自动赋予以下“特殊能力”:
- 调用**被测类**的私有方法(包括静态方法)
- 读取**被测类**的私有字段(包括静态字段)
- 修改**被测类**的私有字段(包括静态字段)
- 修改**被测类**的常量字段(使用`final`修饰的字段,包括静态常量字段)
访问和修改私有、常量成员时IDE可能会提示语法有误但编译器将能够正常运行测试。使用编译期代码增强目前仅实现了`Java`语言的适配)
效果见`java-demo`示例项目`DemoPrivateProcessorTest`测试类中的用例。

View File

@@ -1,5 +1,43 @@
# Release Note
## 0.6.4
- 移除`TestableNull`类型,让`OmniConstructor`更轻量
- 支持JUnit 5的`@Nested`注解issue-140
- 修复多处`OmniConstructor`的兼容性BUG感谢[@ddatsh](https://github.com/ddatsh)
## 0.6.3
- 支持自定义内部Mock容器类名issue-137
- 支持自定义`OmniConstructor`字节码增强范围
- 修复两处可导致`OmniConstructor`无限递归构建的问题包括issue-138
- 修复一处非静态方法获取`this`引用失败导致的下标越界异常issue-136
- 修复获取构建目录失败导致的空指针异常issue-135
## 0.6.2
- 支持通过Properties文件配置TestableAgent
- 默认禁用OmniConstructor相关的字节码增强
- 修复OmniConstructor与`Spring`框架的兼容问题issue-129
## 0.6.1
- 自动生成Mock扫描过程日志文件便于自助排查问题
- 修复一处`Spock`测试框架的兼容问题 (issue-121)
- 修复一处`Gradle`运行单元测试的兼容问题issue-123
## 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
@@ -94,26 +132,26 @@
- 重名了现有的各种注解,以便与更好的与实际功能对应
## v0.1.0
- move generated agent jar to class folder
- support mock method of any object
- 将生成的Agent Jar包自动移到打包工程的Class目录
- 支持Mock任意类型的方法调用不再局限于被测类中的方法
## v0.0.5
- use dynamically runtime modification to replace static `e.java` file
- get rid of unit test framework dependence
- add testable ref field in test class at runtime instead of compile time
- 使用运行期字节码修改替代预先生成的`e.java`文件
- 消除所有对特定单元测试框架的定制逻辑
- 将测试类中的Testable引用字段从编译期生成改为运行期生成
## v0.0.4
- use runtime byte code rewrite to invoke testable setup method
- add `TestableUtil` class to fetch current test case and invocation source
- 改用运行期字节码修改来调用Testable`setup`方法
- 添加`TestableUtil`工具类用于获取当前测试用例和调用来源
## v0.0.3
- use global method invoke to access private members instead of modification in place
- use `e.java` replace `testable` class make code more readable
- introduce `agent` module, use runtime byte code modification to support new operation and member method mocking
- 使用静态方法封装私有成员的访问过程,不在原调用处直接添加反射代码
- 使用`e.java`文件替代`testable`类型,提高代码可读性
- 引入`agent`模块,使用运行期字节码修改实现`new`操作和私有成员调用的Mock
## v0.0.2
- add support of member method mocking by compile time code modification
- 基于编译期代码修改支持Mock私有成员方法的调用
## v0.0.1
- PoC version
- use compile time code modification to support new operation mocking and private field & method access
- 首个概念验证版本
- 基于编译期代码修改支持`new`操作的Mock和访问被测类私有成员

View File

@@ -1,12 +1,12 @@
使用TestableMock
---
`TestableMock`是基于源码和字节码增强的Java单元测试辅助工具,包含以下功能:
`TestableMock`现在已不仅是一款轻量易上手的单元测试Mock工具更是以**简化Java单元测试**为目标的综合辅助工具,包含以下功能:
- [快速Mock任意调用](zh-cn/doc/use-mock.md)使被测类的任意方法调用快速替换为Mock方法实现"指哪换哪"解决传统Mock工具使用繁琐的问题
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
- [快速构造参数对象](zh-cn/doc/omni-constructor.md):生成任意复杂嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题
- [辅助测试void方法](zh-cn/doc/test-void-method.md)利用Mock校验器对方法的内部逻辑进行检查解决无返回值方法难以实施单元测试的问题
- [快速构造参数对象](zh-cn/doc/parameter-constructor.md):生成任意多层嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题
## 在Maven项目中使用
@@ -16,7 +16,7 @@
```xml
<properties>
<testable.version>0.5.0</testable.version>
<testable.version>0.6.4</testable.version>
</properties>
```
@@ -63,8 +63,8 @@
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.5.0')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.0')
testImplementation('com.alibaba.testable:testable-all:0.6.4')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.4')
}
```
@@ -93,3 +93,20 @@ test {
> ```
>
> 完整示例参考[issue-43](https://github.com/alibaba/testable-mock/issues/43)
> 若项目使用`Spock`测试框架,需指定`Groovy`编译生成的JVM 1.6或以上版本字节码方法如下请根据实际使用的JVM版本修改属性值
>
> Maven项目在`pom.xml`中添加`<maven.compiler.source>`和`<maven.compiler.target>`属性,例如:
> ```xml
> <properties>
> <maven.compiler.source>1.6</maven.compiler.source>
> <maven.compiler.target>1.6</maven.compiler.target>
> </properties>
> ```
>
> Gradle项目在`build.gradle`中添加`sourceCompatibility`属性,例如:
> ```groovy
> sourceCompatibility = '6'
> ```
>
> 完整代码可参考`demo/spock-demo`示例项目。

Some files were not shown because too many files have changed in this diff Show More