Compare commits

..

110 Commits

Author SHA1 Message Date
金戟
02ba7ad83c follow up to 0.4.11 2021-02-06 21:11:48 +08:00
金戟
7bbdf82a41 support specify package prefix of class to be mocked 2021-02-06 11:31:51 +08:00
金戟
126c7257d8 fix out of index exception when mocking native method 2021-02-06 10:44:51 +08:00
金戟
adfba4bac0 support verbose log for self diagnosing 2021-02-05 22:30:04 +08:00
金戟
2c18ea12ed use space instead of tab 2021-02-04 17:59:26 +08:00
金戟
d4bce13918 remove note about 0.4.5 version 2021-02-03 18:03:07 +08:00
金戟
b23b8b98b3 add self troubleshooting information 2021-02-02 14:24:12 +08:00
金戟
f228ed5002 release v0.4.11 2021-02-02 07:49:20 +08:00
金戟
2303ac188e add roadmap 2021-02-01 23:49:16 +08:00
金戟
290d0db400 support specify source class for private accessor 2021-02-01 08:08:33 +08:00
金戟
77b955a5ce use system null type 2021-01-31 21:46:46 +08:00
金戟
ab8a0b32e5 fix compile time check for private member access 2021-01-31 20:18:53 +08:00
金戟
39c788cd02 check method parameter count 2021-01-31 15:10:34 +08:00
金戟
7029579534 fix intellij gradle build issue with incorrect file protocol syntax 2021-01-31 12:39:58 +08:00
金戟
ba951ce744 should always load array variable via aload 2021-01-30 23:53:22 +08:00
Fan Lin
2bbe2eba93 be more precise 2021-01-29 11:23:58 +08:00
Fan Lin
4c96f4cc0e Update use-mock.md
use static declaration for complicate mock method
2021-01-29 11:17:00 +08:00
Fan Lin
3a2e3032df Merge pull request #53 from npvip/patch-1
Update use-mock.md
2021-01-28 19:24:23 +08:00
npvip
530f3295bd Update use-mock.md
MOCK_CONTEXT.put不是set
2021-01-28 17:53:50 +08:00
金戟
99e7c3bf18 bump to version 0.4.10 2021-01-22 08:26:01 +08:00
金戟
ef5d21baf6 fit INVOKEDYNAMIC 2021-01-22 00:09:32 +08:00
金戟
6951b98c7e support private method invoke with null parameter 2021-01-21 22:03:53 +08:00
金戟
369c2bd192 put parameter constructor doc to sidebar 2021-01-19 15:34:02 +08:00
金戟
13de5e42ad update en-us docs to fit targetClass parameter usage 2021-01-13 17:45:34 +08:00
金戟
064926e952 use junit 4 in java demo 2021-01-12 19:46:23 +08:00
金戟
2f110ae4d6 bump version to 0.4.9 2021-01-09 08:03:11 +08:00
金戟
7ba44ae618 validate member accessed via PrivateAccessor exists 2021-01-08 22:57:25 +08:00
金戟
66cb8f7732 correctly remove the invoker instance byte code 2021-01-08 17:30:30 +08:00
金戟
1f29aff00f manual recursively check argument is unnecessary 2021-01-08 11:29:27 +08:00
金戟
8d12bb42ba simplify description 2021-01-08 08:23:10 +08:00
金戟
c1744d2254 bump to v0.4.8 2021-01-08 08:14:12 +08:00
金戟
444967f676 recurisively looking into method parameters for more private access expression 2021-01-07 19:48:18 +08:00
金戟
860fdcf072 support private method invocation in assign statement 2021-01-07 18:55:33 +08:00
Fan Lin
851ae91406 Merge pull request #37 from Yiniu/master
解决:Windows下文件分隔符替换多重转义,URLCloassLoader路径不合规的问题,增加本地maven仓库
2021-01-07 14:02:33 +08:00
xuyuqing
580fed7dac 解决:Windows下文件分隔符替换多重转义的问题,增加本地maven仓库 2021-01-07 10:31:35 +08:00
金戟
f07ac8f21f always build with UTF-8 encoding 2021-01-06 16:31:05 +08:00
金戟
31a0b49b7a use targetClass in demo 2021-01-06 00:23:46 +08:00
金戟
0a1b46b352 fit for kotlin auto generated accessor method 2021-01-05 23:28:35 +08:00
金戟
55ec1ca5b5 follow up kotlin demo to same as java ones 2021-01-05 20:44:05 +08:00
金戟
2806d38c94 add shorten command line issue to faq 2021-01-05 15:51:22 +08:00
金戟
070aeb6559 fit mock invocation recorder for targetClass parameter 2021-01-05 14:57:20 +08:00
金戟
0255ffa911 record mock method desc 2021-01-05 14:15:18 +08:00
金戟
f64e0cd959 handle targetClass parameter 2021-01-05 10:34:05 +08:00
金戟
74b68d495a add targetClass parameter 2021-01-04 23:31:58 +08:00
金戟
6c0bd7de46 add code example for MOCK_CONTEXT in doc 2021-01-03 22:54:38 +08:00
金戟
593d134672 update docs 2021-01-03 21:44:16 +08:00
金戟
c496837861 add a place holder page for parameter constructor 2021-01-02 21:04:33 +08:00
金戟
b42ee1c9b9 also fit for EnhancerBySpringCGLIB 2021-01-02 19:47:12 +08:00
金戟
079410ee0f add about us docs 2021-01-02 16:23:57 +08:00
Fan Lin
acc3f0bdf2 Merge pull request #29 from itwild/master
fit for class which enhanced by CGLIB
2021-01-02 11:16:01 +08:00
金戟
6f9d535cee add future feature plan 2021-01-02 11:10:44 +08:00
itwild
631f45f4c5 fit for class which enhanced by CGLIB,for example,some class annotated with '@Service' in Spring Framework 2021-01-01 22:23:50 +08:00
金戟
94127064e8 add demo for private method without argument 2020-12-31 20:40:42 +08:00
金戟
36f170bdd3 release version 0.4.7 2020-12-31 07:30:21 +08:00
金戟
5ab1cf7a40 fit for gradle build in intellij 2020-12-30 23:51:38 +08:00
金戟
68075a2ead fix method node stack change 2020-12-30 22:38:17 +08:00
金戟
783b127bbb fit windows path split 2020-12-30 16:03:36 +08:00
金戟
cd7097ded9 release version 0.4.6 2020-12-30 07:24:49 +08:00
金戟
0f2c889c46 rename mock context class 2020-12-29 17:41:27 +08:00
金戟
df4ac8e950 for static invoke, no more stack required 2020-12-29 16:38:13 +08:00
金戟
9e393b5ef5 use mock context to inject extra mock parameters 2020-12-29 16:35:49 +08:00
金戟
bb47f5d199 raise a issue if any problem found 2020-12-29 13:23:18 +08:00
金戟
2efee1feb2 added notification of recent frequent questions 2020-12-29 13:18:06 +08:00
金戟
3df95db607 static constraint will be remove in future 2020-12-29 11:45:07 +08:00
金戟
a6b289e136 fix issue cause by iinc bytecode 2020-12-28 14:43:08 +08:00
金戟
3c375713aa update yunxiao link and doc description 2020-12-28 10:22:14 +08:00
金戟
9a7372d9ab recommand to use static declared mock method 2020-12-27 22:07:08 +08:00
金戟
e11f2e1645 release version 0.4.5 2020-12-27 20:13:07 +08:00
金戟
d14c570c96 search this reference properly 2020-12-27 13:11:26 +08:00
金戟
8a8065e179 show warning when access none static method in mock method 2020-12-27 11:35:14 +08:00
金戟
28bb6454c8 avoid source class be re-transformed 2020-12-27 11:34:25 +08:00
金戟
13fa8a76ce support dump transformed bytecode to local file 2020-12-26 21:48:57 +08:00
金戟
cbdab5a246 adapt source class loader to intellij context 2020-12-26 20:51:13 +08:00
金戟
d7945f52f3 extra global config 2020-12-26 19:31:10 +08:00
金戟
96ea5680df get Javac processing environment for intellij 2020-12-26 19:30:35 +08:00
金戟
ee21c8dcd8 transform mock method body to static context 2020-12-26 16:20:34 +08:00
金戟
281be6bd51 let mock method be static 2020-12-26 14:27:08 +08:00
金戟
a8b58ffe17 refer to lombok javac processing environment fetch 2020-12-26 11:48:36 +08:00
金戟
8d33cb718f test class package should match the class under test 2020-12-25 18:13:26 +08:00
金戟
2d8433056c jsr269 compile issue only effect in intellij 2020.2.2 version and above 2020-12-25 17:48:30 +08:00
金戟
32eefc25fe better translation 2020-12-24 22:38:17 +08:00
金戟
7f73fc0cc1 complete the english doc 2020-12-24 16:14:18 +08:00
金戟
44fed6ce8b add demo case for access private method with interface parameter 2020-12-24 11:58:17 +08:00
金戟
d6ac715f57 release v0.4.4 2020-12-24 07:33:45 +08:00
金戟
6b126223e8 fix mocking bug of static method without parameter 2020-12-23 21:12:40 +08:00
金戟
508610b0c8 test scope is actually enough 2020-12-23 20:23:09 +08:00
金戟
459fea37c3 more english docs 2020-12-23 20:19:15 +08:00
金戟
703aa319ba type assignable instead of equal 2020-12-23 18:53:26 +08:00
金戟
df7deedcce update translated english doc 2020-12-23 14:21:02 +08:00
金戟
4701b3b42b fix doc according to recent feedbacks 2020-12-23 10:19:42 +08:00
金戟
085c6784d4 add ali-tech link and english readme 2020-12-23 08:45:53 +08:00
Fan Lin
a1437e7291 Update README.md
add gitee doc link
2020-12-22 14:54:08 +08:00
金戟
d17bdb258b release v0.4.3 2020-12-21 10:27:46 +08:00
金戟
03253e529d handle static member access 2020-12-20 21:30:34 +08:00
金戟
6b4467a03b update introduction 2020-12-20 19:02:28 +08:00
金戟
8cfaa6d36e add one more faq and update doc images 2020-12-19 22:43:51 +08:00
金戟
78a19b6876 add feedback doc 2020-12-19 17:48:35 +08:00
金戟
5484b216f1 merge maven plugin configure 2020-12-17 16:07:13 +08:00
金戟
d1007f6166 support mute agent log 2020-12-17 13:17:56 +08:00
金戟
9b82fcdc24 fix incorrect matches invoke in doc 2020-12-17 11:55:16 +08:00
金戟
bf4c263d93 bump to v0.4.2 2020-12-15 19:19:58 +08:00
金戟
28bde0b85a method invocation could contain LabelNode and LineNumberNode 2020-12-15 14:19:05 +08:00
金戟
179e71c7c2 loaded class cache not actually working 2020-12-14 22:57:13 +08:00
金戟
f624c9131e fix null pointer exception 2020-12-14 22:45:41 +08:00
金戟
83974cfb2a avoid duplicate test class injection 2020-12-14 22:36:35 +08:00
金戟
abb6d04c16 support set log level via maven plugin 2020-12-14 21:25:51 +08:00
金戟
16ec3b9444 also add drawbacks 2020-12-14 19:13:12 +08:00
金戟
180dfe9da8 set to next snapshot 2020-12-14 19:06:38 +08:00
金戟
92a2c69e05 replace trump with simpson 2020-12-12 10:36:42 +08:00
金戟
f40834eb94 add comparation doc 2020-12-12 08:42:08 +08:00
104 changed files with 2863 additions and 777 deletions

View File

@@ -2,9 +2,27 @@
换种思路写Mock让单元测试更简单。
无需初始化,不挑测试框架,甭管要换的方法是被测类的私有方法、静态方法还是其他任何类的成员方法也甭管要换的对象是怎么创建的。写好Mock方法,加个`@MockMethod`注解,一切统统搞定。
无需初始化,不挑测试框架,甭管要换的私有方法、静态方法、构造方法还是其他任何类的任何方法也甭管要换的对象是怎么创建的。写好Mock定义,加个`@MockMethod`注解,一切统统搞定。
文档https://alibaba.github.io/testable-mock/
- 文档https://alibaba.github.io/testable-mock/
- 国内文档镜像http://freyrlin.gitee.io/testable-mock/
阅读[这里](https://mp.weixin.qq.com/s/KyU6Eu7mDkZU8FspfSqfMw)了解更多故事。
> 特别说明
> 1. 如遇到"Attempt to access non-static member in mock method"错误,参见[常见问题](https://alibaba.github.io/testable-mock/#/zh-cn/doc/frequently-asked-questions)第8条
> 2. 如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码我们将在24小时内回复并处理
-----
## 版本计划
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
- `0.4` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
- `0.5` 实现以"独立Mock类"为单元的Mock方法复用机制让测试类之间可以方便复用相同的Mock方法
- `0.6` 实现将Mock方法的默认生效范围缩小至当前被测类避免Mock方法在测试类之间相互影响
- `1.0` 功能稳定,一个崭新的开始
## 目录结构
@@ -35,5 +53,5 @@ mvn clean install
docsify serve docs
```
> Testable文档使用`docsify`工具生成,构建前请安装[nodejs](https://nodejs.org/en/download/)运行时,并使用`npm install -g docsify`命令安装文档生成工具。
> TestableMock文档使用`docsify`工具生成,构建前请安装[nodejs](https://nodejs.org/en/download/)运行时,并使用`npm install -g docsify`命令安装文档生成工具。

48
README_EN.md Normal file
View File

@@ -0,0 +1,48 @@
# TestableMock
Write mock faster, make unit testing easier.
Any test framework, no initialization, no matter private method, static method, construction method, or any other method of any class, and no matter how the object created.
Write a mock method, add an `@MockMethod` annotation, everything is done.
Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
## Loadmap
`TestableMock` is still under heavy development, the following version plans may be adjusted during the iteration
- `v0.4` it's the current version, refer to the [issue](https://github.com/alibaba/testable-mock/issues) list for the work in progress
- `v0.5` implementation mock class inherit mechanism, so that the same mock method can be reused between test classes conveniently
- `v0.6` narrows the default effective scope of mock methods to the class under test, to avoid interaction of mocking cross classes
- `v1.0` all functions are stable, a brand-new start
## Directory Structure
```bash
|-- testable-parent ➜ Provide parent pom file shared by other modules
|-- testable-all ➜ Dependence aggration, for easily import all modules at once
|-- testable-processor ➜ Compile-time code preprocessing module, provides test assist functions
|-- 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
|-- demo
| |-- java-demo ➜ Java code example
| `-- kotlin-demo ➜ Kotlin code example
`-- docs ➜ Source code of usage document
```
## Build project
The project is built using JDK 1.6+ and Maven 3+, except for the `demo` sub-project requires JDK 1.8+.
```bash
mvn clean install
```
## Generate document
```bash
docsify serve docs
```
> `TestableMock` document is generated by `docsify` tool, please install [nodejs](https://nodejs.org/en/download/) before execution, and run `npm install -g docsify` command to install the tool required.

View File

@@ -7,16 +7,21 @@ version = '1.0.0-SNAPSHOT'
sourceCompatibility = '8'
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
testImplementation('com.alibaba.testable:testable-all:0.4.1')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.1')
testImplementation('junit:junit:4.13.1')
testImplementation('com.alibaba.testable:testable-all:0.4.11')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.11')
}
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
test {
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
useJUnitPlatform()
useJUnit()
}

View File

@@ -11,8 +11,8 @@
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>5.6.2</junit.version>
<testable.version>0.4.1</testable.version>
<junit.version>4.13.1</junit.version>
<testable.version>0.4.11</testable.version>
</properties>
<dependencies>
@@ -20,11 +20,11 @@
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
@@ -32,18 +32,15 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
</configuration>
</plugin>
<!-- <plugin>-->
<!-- <groupId>com.alibaba.testable</groupId>-->
<!-- <artifactId>testable-maven-plugin</artifactId>-->

View File

@@ -5,7 +5,8 @@ import com.alibaba.testable.demo.model.Box;
import com.alibaba.testable.demo.model.Color;
/**
* @author flin
* 演示父类变量引用子类对象时的Mock场景
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
*/
public class DemoInherit {

View File

@@ -9,7 +9,8 @@ import java.util.HashSet;
import java.util.List;
/**
* @author flin
* 演示Mock方法调用校验器
* Demonstrate mock method invocation verifier
*/
public class DemoMatcher {

View File

@@ -6,6 +6,10 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Collectors;
/**
* 演示基本的Mock功能
* Demonstrate basic mock functionality
*/
public class DemoMock {
/**
@@ -20,7 +24,7 @@ public class DemoMock {
* method with member method invoke
*/
public String outerFunc(String s) throws Exception {
return "{ \"res\": \"" + innerFunc(s) + "\"}";
return "{ \"res\": \"" + innerFunc(s) + staticFunc() + "\"}";
}
/**
@@ -48,6 +52,10 @@ public class DemoMock {
return callFromDifferentMethod();
}
private static String staticFunc() {
return "_STATIC_TAIL";
}
private String innerFunc(String s) throws Exception {
return Files.readAllLines(Paths.get("/a-not-exist-file")).stream().collect(Collectors.joining());
}

View File

@@ -1,7 +1,10 @@
package com.alibaba.testable.demo;
import java.util.List;
/**
* @author flin
* 演示私有成员访问功能
* Demonstrate private member access functionality
*/
public class DemoPrivateAccess {
@@ -19,17 +22,31 @@ public class DemoPrivateAccess {
public final Double pi = 3.14;
/**
* private static method
* private static method without arguments
*/
private static String privateStaticFunc(String s, int i) {
return s + " + " + i;
private static String privateStaticFunc() {
return "static";
}
/**
* private member method
* private static method with arguments
*/
private String privateFunc(String s, int i) {
return s + " - " + i;
private static String privateStaticFuncWithArgs(String str, int i) {
return (str == null ? "null" : str) + " + " + i;
}
/**
* private member method without arguments
*/
private String privateFunc() {
return "member";
}
/**
* private member method with arguments
*/
private String privateFuncWithArgs(List<String> list, String str, int i) {
return list.stream().reduce((a, s) -> a + s).orElse("") + " + " + str + " + " + i;
}
}

View File

@@ -2,6 +2,10 @@ package com.alibaba.testable.demo;
import java.util.*;
/**
* 演示模板方法的Mock场景
* Demonstrate scenario of mocking template method
*/
public class DemoTemplate {
private <T> List<T> getList(T value) {

View File

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

View File

@@ -3,17 +3,17 @@ package com.alibaba.testable.demo;
import com.alibaba.testable.core.annotation.MockMethod;
import com.alibaba.testable.core.error.VerifyFailedError;
import com.alibaba.testable.demo.model.BlackBox;
import org.junit.jupiter.api.Test;
import org.junit.Test;
import static com.alibaba.testable.core.matcher.InvokeMatcher.*;
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.Assert.fail;
/**
* 演示Mock方法调用校验器
* Demonstrate mock method invocation verifier
*/
class DemoMatcherTest {
public class DemoMatcherTest {
private DemoMatcher demoMatcher = new DemoMatcher();
@@ -28,7 +28,7 @@ class DemoMatcherTest {
@Test
void should_match_no_argument() {
public void should_match_no_argument() {
demoMatcher.callMethodWithoutArgument();
verify("methodWithoutArgument").withTimes(1);
demoMatcher.callMethodWithoutArgument();
@@ -36,7 +36,7 @@ class DemoMatcherTest {
}
@Test
void should_match_number_arguments() {
public void should_match_number_arguments() {
demoMatcher.callMethodWithNumberArguments();
verify("methodWithArguments").without(anyString(), 2);
verify("methodWithArguments").withInOrder(anyInt(), 2);
@@ -49,7 +49,7 @@ class DemoMatcherTest {
}
@Test
void should_match_string_arguments() {
public void should_match_string_arguments() {
demoMatcher.callMethodWithStringArgument();
verify("methodWithArguments").with(startsWith("he"), endsWith("ld"));
verify("methodWithArguments").with(contains("stab"), matches("m.[cd]k"));
@@ -57,7 +57,7 @@ class DemoMatcherTest {
}
@Test
void should_match_object_arguments() {
public void should_match_object_arguments() {
demoMatcher.callMethodWithObjectArgument();
verify("methodWithArguments").withInOrder(any(BlackBox.class), any(BlackBox.class));
verify("methodWithArguments").withInOrder(nullable(BlackBox.class), nullable(BlackBox.class));
@@ -65,7 +65,7 @@ class DemoMatcherTest {
}
@Test
void should_match_with_times() {
public void should_match_with_times() {
demoMatcher.callMethodWithNumberArguments();
verify("methodWithArguments").with(anyNumber(), any()).times(3);

View File

@@ -3,20 +3,20 @@ package com.alibaba.testable.demo;
import com.alibaba.testable.core.annotation.MockConstructor;
import com.alibaba.testable.core.annotation.MockMethod;
import com.alibaba.testable.demo.model.BlackBox;
import org.junit.jupiter.api.Test;
import org.junit.Test;
import java.util.concurrent.Executors;
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
import static com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT;
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;
import static com.alibaba.testable.core.tool.TestableTool.TEST_CASE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.Assert.assertEquals;
/**
* 演示基本的Mock功能
* Demonstrate basic mock functionality
*/
class DemoMockTest {
public class DemoMockTest {
private DemoMock demoMock = new DemoMock();
@@ -25,34 +25,39 @@ class DemoMockTest {
return new BlackBox("mock_" + text);
}
@MockMethod
private String innerFunc(DemoMock self, String text) {
@MockMethod(targetClass = DemoMock.class)
private String innerFunc(String text) {
return "mock_" + text;
}
@MockMethod
private String trim(String self) {
@MockMethod(targetClass = DemoMock.class)
private String staticFunc() {
return "_MOCK_TAIL";
}
@MockMethod(targetClass = String.class)
private String trim() {
return "trim_string";
}
@MockMethod(targetMethod = "substring")
private String sub(String self, int i, int j) {
@MockMethod(targetClass = String.class, targetMethod = "substring")
private String sub(int i, int j) {
return "sub_string";
}
@MockMethod
private boolean startsWith(String self, String s) {
@MockMethod(targetClass = String.class)
private boolean startsWith(String s) {
return false;
}
@MockMethod
private BlackBox secretBox(BlackBox ignore) {
@MockMethod(targetClass = BlackBox.class)
private BlackBox secretBox() {
return new BlackBox("not_secret_box");
}
@MockMethod
private String callFromDifferentMethod(DemoMock self) {
if (TEST_CASE.equals("should_able_to_get_test_case_name")) {
@MockMethod(targetClass = DemoMock.class)
private String callFromDifferentMethod() {
if ("special_case".equals(MOCK_CONTEXT.get("case"))) {
return "mock_special";
}
switch (SOURCE_METHOD) {
@@ -63,19 +68,20 @@ class DemoMockTest {
@Test
void should_able_to_mock_new_object() {
public void should_able_to_mock_new_object() {
assertEquals("mock_something", demoMock.newFunc());
verify("createBlackBox").with("something");
}
@Test
void should_able_to_mock_member_method() throws Exception {
assertEquals("{ \"res\": \"mock_hello\"}", demoMock.outerFunc("hello"));
public void should_able_to_mock_member_method() throws Exception {
assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoMock.outerFunc("hello"));
verify("innerFunc").with("hello");
verify("staticFunc").with();
}
@Test
void should_able_to_mock_common_method() {
public void should_able_to_mock_common_method() {
assertEquals("trim_string__sub_string__false", demoMock.commonFunc());
verify("trim").withTimes(1);
verify("sub").withTimes(1);
@@ -83,13 +89,13 @@ class DemoMockTest {
}
@Test
void should_able_to_mock_static_method() {
public void should_able_to_mock_static_method() {
assertEquals("not_secret_box", demoMock.getBox().get());
verify("secretBox").withTimes(1);
}
@Test
void should_able_to_get_source_method_name() throws Exception {
public void should_able_to_get_source_method_name() throws Exception {
// synchronous
assertEquals("mock_one_mock_others", demoMock.callerOne() + "_" + demoMock.callerTwo());
// asynchronous
@@ -99,12 +105,14 @@ class DemoMockTest {
}
@Test
void should_able_to_get_test_case_name() throws Exception {
public void should_able_to_get_test_case_name() throws Exception {
MOCK_CONTEXT.put("case", "special_case");
// synchronous
assertEquals("mock_special", demoMock.callerOne());
// asynchronous
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoMock.callerOne()).get());
verify("callFromDifferentMethod").withTimes(2);
MOCK_CONTEXT.clear();
}
}

View File

@@ -2,53 +2,76 @@ package com.alibaba.testable.demo;
import com.alibaba.testable.core.accessor.PrivateAccessor;
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
import org.junit.jupiter.api.Test;
import org.junit.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
/**
* 演示私有成员访问功能
* Demonstrate private member access functionality
*/
@EnablePrivateAccess
class DemoPrivateAccessTest {
public class DemoPrivateAccessTest {
private DemoPrivateAccess demoPrivateAccess = new DemoPrivateAccess();
@Test
void should_able_to_access_private_method() {
assertEquals("hello - 1", demoPrivateAccess.privateFunc("hello", 1));
assertEquals("hello - 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", "hello", 1));
public void should_able_to_access_private_method() {
List<String> list = new ArrayList<String>() {{ add("a"); add("b"); add("c"); }};
assertEquals("member", demoPrivateAccess.privateFunc());
assertEquals("member", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc"));
assertEquals("abc + hello + 1", demoPrivateAccess.privateFuncWithArgs(list, "hello", 1));
assertEquals("abc + hello + 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFuncWithArgs", list, "hello", 1));
}
@Test
void should_able_to_access_private_field() {
public void should_able_to_access_private_field() {
demoPrivateAccess.count = 2;
assertEquals(new Integer(2), demoPrivateAccess.count);
assertEquals(Integer.valueOf(2), demoPrivateAccess.count);
PrivateAccessor.set(demoPrivateAccess, "count", 3);
assertEquals(new Integer(3), PrivateAccessor.get(demoPrivateAccess, "count"));
assertEquals(Integer.valueOf(3), PrivateAccessor.get(demoPrivateAccess, "count"));
}
@Test
void should_able_to_access_private_static_method() {
//assertEquals("hello + 1", DemoPrivateAccess.privateStaticFunc("hello", 1));
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFunc", "hello", 1));
public void should_able_to_access_private_static_method() {
assertEquals("static", DemoPrivateAccess.privateStaticFunc());
assertEquals("static", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFunc"));
assertEquals("hello + 1", DemoPrivateAccess.privateStaticFuncWithArgs("hello", 1));
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", "hello", 1));
}
@Test
void should_able_to_access_private_static_field() {
//DemoPrivateAccess.staticCount = 2;
//assertEquals(new Integer(2), DemoPrivateAccess.staticCount);
public void should_able_to_access_private_static_field() {
DemoPrivateAccess.staticCount = 2;
assertEquals(Integer.valueOf(2), DemoPrivateAccess.staticCount);
PrivateAccessor.setStatic(DemoPrivateAccess.class, "staticCount", 3);
assertEquals(new Integer(3), PrivateAccessor.getStatic(DemoPrivateAccess.class, "staticCount"));
assertEquals(Integer.valueOf(3), PrivateAccessor.getStatic(DemoPrivateAccess.class, "staticCount"));
}
@Test
void should_able_to_update_final_field() {
public void should_able_to_update_final_field() {
demoPrivateAccess.pi = 4.13;
assertEquals(4.13, demoPrivateAccess.pi);
assertEquals(Double.valueOf(4.13), demoPrivateAccess.pi);
PrivateAccessor.set(demoPrivateAccess, "pi", 3.14);
assertEquals(Double.valueOf(3.14), PrivateAccessor.get(demoPrivateAccess, "pi"));
}
@Test
public void should_able_to_use_null_parameter() {
demoPrivateAccess.pi = null;
assertNull(demoPrivateAccess.pi);
assertEquals("null + 1", DemoPrivateAccess.privateStaticFuncWithArgs(null, 1));
PrivateAccessor.set(demoPrivateAccess, "pi", null);
assertNull(PrivateAccessor.get(demoPrivateAccess, "pi"));
assertEquals("null + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", null, 1));
}
}

View File

@@ -2,17 +2,17 @@ package com.alibaba.testable.demo;
import com.alibaba.testable.core.annotation.MockConstructor;
import com.alibaba.testable.core.annotation.MockMethod;
import org.junit.jupiter.api.Test;
import org.junit.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.Assert.assertEquals;
/**
* 演示模板方法可以被Mock
* Demonstrate template method can be mocked
* 演示模板方法Mock场景
* Demonstrate scenario of mocking template method
*/
class DemoTemplateTest {
public class DemoTemplateTest {
private DemoTemplate demoTemplate = new DemoTemplate();
@@ -20,12 +20,12 @@ class DemoTemplateTest {
/* First solution: use generics type */
@MockMethod
private <T> List<T> getList(DemoTemplate self, T value) {
private static <T> List<T> getList(DemoTemplate self, T value) {
return new ArrayList<T>() {{ add((T)(value.toString() + "_mock_list")); }};
}
@MockMethod
private <K, V> Map<K, V> getMap(DemoTemplate self, K key, V value) {
private static <K, V> Map<K, V> getMap(DemoTemplate self, K key, V value) {
return new HashMap<K, V>() {{ put(key, (V)(value.toString() + "_mock_map")); }};
}
@@ -46,12 +46,12 @@ class DemoTemplateTest {
/* Second solution: use object type */
//@MockMethod
//private List<Object> getList(DemoTemplate self, Object value) {
//private static List<Object> getList(DemoTemplate self, Object value) {
// return new ArrayList<Object>() {{ add(value.toString() + "_mock_list"); }};
//}
//
//@MockMethod
//private Map<Object, Object> getMap(DemoTemplate self, Object key, Object value) {
//private static Map<Object, Object> getMap(DemoTemplate self, Object key, Object value) {
// return new HashMap<Object, Object>() {{ put(key, value.toString() + "_mock_map"); }};
//}
//
@@ -70,19 +70,19 @@ class DemoTemplateTest {
@Test
void should_able_to_mock_single_template_method() {
public void should_able_to_mock_single_template_method() {
String res = demoTemplate.singleTemplateMethod();
assertEquals("demo_mock_list", res);
}
@Test
void should_able_to_mock_double_template_method() {
public void should_able_to_mock_double_template_method() {
String res = demoTemplate.doubleTemplateMethod();
assertEquals("testable_mock_map", res);
}
@Test
void should_able_to_mock_new_template_method() {
public void should_able_to_mock_new_template_method() {
Set<?> res = demoTemplate.newTemplateMethod();
assertEquals(2, res.size());
Iterator<?> iterator = res.stream().iterator();

View File

@@ -1,7 +1,7 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.4.10"
kotlin("jvm") version "1.4.10"
}
group = "com.alibaba.testable"
@@ -9,25 +9,25 @@ version = "1.0.0-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8
repositories {
mavenCentral()
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
testImplementation("com.alibaba.testable:testable-all:0.4.1")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.1")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
testImplementation("com.alibaba.testable:testable-all:0.4.11")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.11")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "1.8"
}
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "1.8"
}
}
tasks.withType<Test> {
jvmArgs("-javaagent:${classpath.find { it.name.contains("testable-agent") }!!.absolutePath}")
useJUnitPlatform()
jvmArgs("-javaagent:${classpath.find { it.name.contains("testable-agent") }!!.absolutePath}")
useJUnitPlatform()
}

View File

@@ -1,123 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<artifactId>kotlin-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>kotlin-demo</name>
<description>Demo project for TestableMock</description>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<artifactId>kotlin-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>kotlin-demo</name>
<description>Demo project for TestableMock</description>
<properties>
<java.version>1.8</java.version>
<kotlin.version>1.3.72</kotlin.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>5.6.2</junit.version>
<testable.version>0.4.1</testable.version>
</properties>
<properties>
<java.version>1.8</java.version>
<kotlin.version>1.3.72</kotlin.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>5.6.2</junit.version>
<testable.version>0.4.11</testable.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
<!-- <plugin>-->
<!-- <groupId>org.apache.maven.plugins</groupId>-->
<!-- <artifactId>maven-surefire-plugin</artifactId>-->
<!-- <configuration>-->
<!-- <argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>-->
<!-- </configuration>-->
<!-- </plugin>-->
<plugin>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-maven-plugin</artifactId>
<version>${testable.version}</version>
<executions>
<execution>
<id>prepare</id>
<goals>
<goal>prepare</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.6</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>target/jacoco.exec</dataFile>
<outputDirectory>target/jacoco-ut</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<!-- <configuration>-->
<!-- <argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>-->
<!-- </configuration>-->
</plugin>
<plugin>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-maven-plugin</artifactId>
<version>${testable.version}</version>
<executions>
<execution>
<id>prepare</id>
<goals>
<goal>prepare</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.6</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>target/jacoco.exec</dataFile>
<outputDirectory>target/jacoco-ut</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -4,6 +4,10 @@ import com.alibaba.testable.demo.model.BlackBox
import com.alibaba.testable.demo.model.Box
import com.alibaba.testable.demo.model.Color
/**
* 演示父类变量引用子类对象时的Mock场景
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
*/
class DemoInherit {
/**

View File

@@ -4,7 +4,8 @@ import com.alibaba.testable.demo.model.BlackBox
import java.util.*
/**
* @author flin
* 演示Mock方法调用校验器
* Demonstrate mock method invocation verifier
*/
class DemoMatcher {
/**
@@ -41,8 +42,24 @@ class DemoMatcher {
val longArray = arrayOf(1L, 2L)
methodToBeMocked(1, 2)
methodToBeMocked(1L, 2.0)
// below two invocations are equivalent
methodToBeMocked(listOf(1), setOf(1.0f))
// multiple lines method invocation
methodToBeMocked(object : ArrayList<Int?>() {
init {
add(1)
}
}, object : HashSet<Float?>() {
init {
add(1.0f)
}
})
// below two invocations are equivalent
methodToBeMocked(1.0, mapOf(1 to 1.0f))
methodToBeMocked(1.0, object : HashMap<Int?, Float?>(2) { init { put(1, 1.0f) } })
methodToBeMocked(floatList, floatList)
methodToBeMocked(longArray)
methodToBeMocked(arrayOf(1.0, 2.0))

View File

@@ -5,6 +5,10 @@ import com.alibaba.testable.demo.model.ColorBox
import java.nio.file.Files
import java.nio.file.Paths
/**
* 演示基本的Mock功能
* Demonstrate basic mock functionality
*/
class DemoMock {
/**
@@ -18,7 +22,7 @@ class DemoMock {
* method with member method invoke
*/
fun outerFunc(s: String): String {
return "{ \"res\": \"" + innerFunc(s) + "\"}"
return "{ \"res\": \"" + innerFunc(s) + staticFunc() + "\"}"
}
/**
@@ -52,4 +56,14 @@ class DemoMock {
}
private fun callFromDifferentMethod() = "realOne"
companion object {
private fun staticFunc(): String {
return "_STATIC_TAIL"
}
// fun callStaticFunc(): String {
// return "CALL${staticFunc()}"
// }
}
}

View File

@@ -1,5 +1,9 @@
package com.alibaba.testable.demo
/**
* 演示私有成员访问功能
* Demonstrate private member access functionality
*/
class DemoPrivateAccess {
/**
@@ -15,8 +19,8 @@ class DemoPrivateAccess {
/**
* private member method
*/
private fun privateFunc(s: String, i: Int): String {
return "$s - $i"
private fun privateFunc(list: List<String>, str: String, i: Int): String {
return list.reduce { a: String, s: String -> a + s } + " + " + "$str + $i"
}
companion object {
@@ -29,15 +33,15 @@ class DemoPrivateAccess {
/**
* private static method
*/
private fun privateStaticFunc(s: String, i: Int): String {
return "$s + $i"
private fun privateStaticFunc(str: String, i: Int): String {
return "$str + $i"
}
/**
* private jvm static method
*/
@JvmStatic private fun privateJvmStaticFunc(s: String, i: Int): String {
return "$s * $i"
@JvmStatic private fun privateJvmStaticFunc(list: List<String>, str: String, i: Int): String {
return list.reduce { a: String, s: String -> a + s } + " * " + "$str * $i"
}
}

View File

@@ -4,6 +4,10 @@ import java.util.ArrayList
import java.util.HashMap
import java.util.HashSet
/**
* 演示模板方法的Mock场景
* Demonstrate scenario of mocking template method
*/
class DemoTemplate {
private fun <T> getList(value: T): List<T> {
val l: MutableList<T> = ArrayList()

View File

@@ -44,8 +44,8 @@ internal class DemoMatcherTest {
InvokeVerifier.verify("methodWithArguments").withInOrder(InvokeMatcher.anyInt(), 2)
InvokeVerifier.verify("methodWithArguments").withInOrder(InvokeMatcher.anyLong(), InvokeMatcher.anyNumber())
// Note: Must use `::class.javaObjectType` for primary types check in Kotlin
InvokeVerifier.verify("methodWithArguments").with(1.0, InvokeMatcher.anyMapOf(Int::class.javaObjectType, Float::class.javaObjectType))
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyList(), InvokeMatcher.anySetOf(Float::class.javaObjectType))
InvokeVerifier.verify("methodWithArguments").with(1.0, InvokeMatcher.anyMapOf(Int::class.javaObjectType, Float::class.javaObjectType)).times(2)
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyList(), InvokeMatcher.anySetOf(Float::class.javaObjectType)).times(2)
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyList(), InvokeMatcher.anyListOf(Float::class.javaObjectType))
InvokeVerifier.verify("methodWithArrayArgument").with(InvokeMatcher.anyArrayOf(Long::class.javaObjectType))
InvokeVerifier.verify("methodWithArrayArgument").with(InvokeMatcher.anyArray())
@@ -70,12 +70,12 @@ internal class DemoMatcherTest {
@Test
fun should_match_with_times() {
demoMatcher.callMethodWithNumberArguments()
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(3)
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(4)
demoMatcher.callMethodWithNumberArguments()
var gotError = false
try {
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(4)
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(5)
} catch (e: VerifyFailedError) {
gotError = true
}

View File

@@ -4,7 +4,7 @@ import com.alibaba.testable.core.annotation.MockConstructor
import com.alibaba.testable.core.annotation.MockMethod
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
import com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD
import com.alibaba.testable.core.tool.TestableTool.TEST_CASE
import com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT
import com.alibaba.testable.demo.model.BlackBox
import com.alibaba.testable.demo.model.ColorBox
import org.junit.jupiter.api.Assertions.assertEquals
@@ -22,31 +22,36 @@ internal class DemoMockTest {
@MockConstructor
private fun createBlackBox(text: String) = BlackBox("mock_$text")
@MockMethod
private fun innerFunc(self: DemoMock, text: String) = "mock_$text"
@MockMethod(targetClass = DemoMock::class)
private fun innerFunc(text: String) = "mock_$text"
@MockMethod
private fun trim(self: BlackBox) = "trim_string"
@MockMethod(targetClass = DemoMock::class)
private fun staticFunc(): String {
return "_MOCK_TAIL";
}
@MockMethod(targetMethod = "substring")
private fun sub(self: BlackBox, i: Int, j: Int) = "sub_string"
@MockMethod(targetClass = BlackBox::class)
private fun trim() = "trim_string"
@MockMethod
private fun startsWith(self: BlackBox, s: String) = false
@MockMethod(targetClass = BlackBox::class, targetMethod = "substring")
private fun sub(i: Int, j: Int) = "sub_string"
@MockMethod
private fun secretBox(ignore: BlackBox): BlackBox {
@MockMethod(targetClass = BlackBox::class)
private fun startsWith(s: String) = false
@MockMethod(targetClass = BlackBox::class)
private fun secretBox(): BlackBox {
return BlackBox("not_secret_box")
}
@MockMethod
private fun createBox(ignore: ColorBox, color: String, box: BlackBox): BlackBox {
@MockMethod(targetClass = ColorBox::class)
private fun createBox(color: String, box: BlackBox): BlackBox {
return BlackBox("White_${box.get()}")
}
@MockMethod
private fun callFromDifferentMethod(self: DemoMock): String {
return if (TEST_CASE == "should_able_to_get_test_case_name") {
@MockMethod(targetClass = DemoMock::class)
private fun callFromDifferentMethod(): String {
return if (MOCK_CONTEXT["case"] == "special_case") {
"mock_special"
} else {
when (SOURCE_METHOD) {
@@ -65,10 +70,17 @@ internal class DemoMockTest {
@Test
fun should_able_to_mock_member_method() {
assertEquals("{ \"res\": \"mock_hello\"}", demoMock.outerFunc("hello"))
assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoMock.outerFunc("hello"))
verify("innerFunc").with("hello")
verify("staticFunc").with()
}
// @Test
// fun should_able_to_mock_method_in_companion_object() {
// assertEquals("CALL_MOCK_TAIL", DemoMock.callStaticFunc())
// verify("staticFunc").with()
// }
@Test
fun should_able_to_mock_common_method() {
assertEquals("trim_string__sub_string__false", demoMock.commonFunc())
@@ -97,6 +109,7 @@ internal class DemoMockTest {
@Test
fun should_able_to_get_test_case_name() {
MOCK_CONTEXT["case"] = "special_case"
// synchronous
assertEquals("mock_special", demoMock.callerOne())
// asynchronous
@@ -104,5 +117,6 @@ internal class DemoMockTest {
demoMock.callerOne()
}.get())
verify("callFromDifferentMethod").withTimes(2)
MOCK_CONTEXT.clear()
}
}

View File

@@ -14,7 +14,8 @@ internal class DemoPrivateAccessTest {
@Test
fun should_able_to_access_private_method() {
assertEquals("hello - 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", "hello", 1))
val list = listOf("a", "b", "c");
assertEquals("abc + hello + 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", list, "hello", 1))
}
@Test
@@ -25,8 +26,9 @@ internal class DemoPrivateAccessTest {
@Test
fun should_able_to_access_private_static_method() {
val list = listOf("a", "b", "c");
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateStaticFunc", "hello", 1))
assertEquals("hello * 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateJvmStaticFunc", "hello", 1))
assertEquals("abc * hello * 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateJvmStaticFunc", list, "hello", 1))
}
@Test

View File

@@ -2,17 +2,14 @@ package com.alibaba.testable.demo
import com.alibaba.testable.core.annotation.MockConstructor
import com.alibaba.testable.core.annotation.MockMethod
import com.alibaba.testable.core.annotation.MockWith
import com.alibaba.testable.core.model.MockDiagnose
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import java.util.*
/**
* 演示模板方法可以被Mock
* Demonstrate template method can be mocked
* 演示模板方法Mock场景
* Demonstrate scenario of mocking template method
*/
@MockWith(diagnose = MockDiagnose.ENABLE)
internal class DemoTemplateTest {
private val demoTemplate = DemoTemplate()

13
docs/en-us/README.md Normal file
View File

@@ -0,0 +1,13 @@
TestableMock Introduction
---
The mock method in unit testing is usually to bypass method calls that rely on external resources or irrelevant functions, so that the focus of the test can be keep on the code logic that needs to be verified and guaranteed.
When defining the mock method, developers really care about only one thing: "<u>This call should be replaced with the fake **mock method** during testing</u>".
However, when the current mainstream mock framework implements the mock function, developers have to worry about too many things: how the mock framework is initialized, whether it is compatible with the unit testing framework used, whether the method to be mocked is private or static, whether the object to be mocked is created by `new` operator or injected, how to send the mock object back to the class under test... These non-critical additional tasks greatly distract the fun of using the mock tool.
Therefore, we developed `TestableMock`, **a maverick and lightweight mock tool**.
![mock](https://testable-code.oss-cn-beijing.aliyuncs.com/en-us/mock-simpson.png)

View File

@@ -0,0 +1,9 @@
## About Us
The core development team of `TestableMock` comes from **Alibaba Cloud · Cloud R&D Department** (formerly known as **Alibaba Group R&D Efficiency Department**). We not only internally support Alibaba Group's end-to-end project code, testing, and release process of thousands of BU-level product lines, but also the product R&D team of Alibaba Cloud enterprise-level R&D collaboration platform [云效](https://www.aliyun.com/product/yunxiao). As has been deeply involved in the field of developer tools, we are committed to improving the production experience of developers and building a digital R&D ecosystem for enterprises.
The function of this project is inspired by our summary of the pain points that Java developers often encounter in daily unit testing ([external dependence on Mock is cumbersome](en-us/doc/use-mock.md), [private method is difficult to test](en-us/doc/private-accessor.md), [void type method is difficult to test](en-us/doc/test-void-method.md), and [complex parameters are difficult to construct](en-us/doc/parameter-constructor.md)). Its internal name is `Testable`, and was renamed `TestableMock` for SEO reason when it was open sourced, thus the lightweight mock function is highlighted. Don't be fooled by its name, `TestableMock` is more than just a mocking tool.
From the incubation of hackathon, to internal open source, and then to external open source, `TestableMock` has accumulated a group of developer users in the internal and external communities. At the same time, we are constantly improving the functional richness and stability of `TestableMock` itself. According to the current version release process, we usually update the internal beta version containing the `SNAPSHOT` mark internally, and release the official version to the Maven central repository (except for the severe bug fix version) after at least one day of stable used to ensure the update will bring more convenience and less burden to everyone.
Finally, we are relatively optimistic about the discussion in the community about "whether too powerful test assistance is equivalent to condoning code corruption". Just as the birth of `PowerMock` did not really spawn more bad projects, but actually solved many test problems left over by the Java language. Rather than use "code tricks" to test functions indirectly, it could be worth to just remove the constraints, and let the unit test break in all its fury! 🤠

View File

@@ -0,0 +1,26 @@
Mock Tools Comparison
---
Besides `TestableMock`, there are also several other community mock tools, such as `Mockito`, `Spock`, `PowerMock` and `JMockit`. Comparison as follows:
| Tool | Mechanism | Minimal mock unit | Limitation of method be mocked | Ease of use | IDE support |
| ---- | ---- | ---- | ---- | ---- | ---- |
| Mockito | Dynamic proxy | Class | Except private/static method and constructor | **Easy** | **Very well** |
| Spock | Dynamic proxy | Class | Except private/static method and constructor | Complicate | Just so so |
| PowerMock | Custom class loader | Class | **No limitation, any method works** | Complicate | **Good** |
| JMockit | Runtime bytecode modification | Class | Except constructor (i.e. new operator) | Complicate | Just so so |
| TestableMock | Runtime bytecode modification | Method | **No limitation, any method works** | **Very easy** | Just so so |
`Mockito` is Java's the most classical mock tool, good stability and easy to use, with both IntelliJ and Eclipse have plugin. The shortcoming part is that its mock functionality is sometimes not enough and have to be used in conjunction with other mock tools when necessary.
`Spock` is a highly readable unit testing framework with built-in mock support, it has a good overall consistency. Since it is also based on dynamic proxy implementation, its shortcomings are similar to `Mockito`.
`PowerMock` is a very powerful mock tool. Its basic syntax is compatible with `Mockito`, and extends many missing features of `Mockito`, including support for mocking of private, static and construction methods. However, due to the use of custom class loader, the test coverage of Jacoco will drop to zero in the default `on-the-fly` mode.
`JMockit` is a mock tool whose functionality and convenience are between `Mockito` and `PowerMock`, and it makes up for their respective shortcomings. The project tried to launch a rewritten version of JMockit2 in 2017 but failed to complete, and is currently in an inactive maintenance state.
The functionality of `TestabledMock` is basically the same as that of `PowerMock`, and it is extremely easy to use. You can complete most tasks only by mastering the annotations of `@MockMethod`.
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.

View File

@@ -0,0 +1,8 @@
Feedback Channel
---
If you have any questions or suggestions during the use of `TestableMock`, please go ahead raise them at [Github Issues](https://github.com/alibaba/testable-mock/issues) and we will usually reply within 24 hours.
Description of the situation you meet in detail is highly welcomed. If the problem may be related to a potential BUG of `TestableMock`, please try to provide reproducible source code or examples for shorten the troubleshooting time.
`TestableMock`, with no more untestable code in Java : )

View File

@@ -0,0 +1,74 @@
Frequently Asked Questions
---
#### 1. How to mock the fields initialized by `@Autowired` in the class under test?
Create the class under test object directly, and then use the ability of `TestableMock` to access private members to directly assign values to these fields.
#### 2. Can `TestableMock` be used with other mock tools?
`TestableMock` can be safely used together with other mock tools based on dynamic proxy mechanism, such as `Mockito`, `EasyMock`, `Spock`, etc., all belong to this category.
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?
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.
At this time, follow a principle that the type of the first parameter of the mock method is always the same as the type of the variable that initiated the call.
Therefore, regardless of whether the actually called method comes from the parent class or the subclass, and whether the subclass overrides the method. If the calling variable is of the parent type (or interface type), the first parameter type of the mock method should use the corresponding parent type (or interface) type.
See the use case of the `DemoInheritTest` test class in the Java and Kotlin examples.
#### 4. How to mock generic methods (template methods)?
Just use the same generic parameters directly on the mock method.
See the use case of the `DemoTemplateTest` test class in the Java and Kotlin examples.
> 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?
The `String` type in Kotlin language is actually `kotlin.String` instead of `java.lang.String`. However, when this type is built from bytecode, it will be replaced with Java's `java.lang.String` class, so no matter if the mock target is written as `kotlin.String` or `java.lang.String`, it cannot match the original called method.
In actual scenarios, there are very few scenarios where methods in the `String` class need to be mocked, so `TestableMock` has not dealt with this situation specifically.
#### 6. Will the mock definition still be valid when it is **indirectly called** from other test classes?
Equally effective, the scope of mock is the entire test runtime process.
For example, some private methods and external invocation in the `Aaa` class are mocked, mock method are defined in the test class `AaaTest`. When testing the `Bbb` class in another test class `BbbTest`, some mocked methods are invoked indirectly in the `Aaa` class, the actual call will also be routed to the mock method defined in the `AaaTest` class.
#### 7. Can `TestableMock` be used for testing Android projects?
It can be used in combination with [Roboelectric](https://github.com/robolectric/robolectric) testing framework.
The `Dalvik` and `ART` virtual machines of the Android system use a bytecode system different from the standard JVM, which will affect the normal functionality of `TestableMock`. The `Roboelectric` framework can run Android unit tests on a standard JVM virtual machine, which is much faster than running unit tests through the Android virtual machine. Recently, most Android App unit tests are written with the `Roboelectric` framework.
#### 8. Meet "Attempt to access non-static member in mock method" error during mocking
The current design of `TestableMock` does not allow access to the non-`static` members of the test class in the mock method (because the mock method itself will be dynamically modified to the `static` type during runtime). However, some Java statements include building blocks (like `new ArrayList<String>() {{ append("data"); }}`), lambda expression (like `list.stream().map(i -> i. get)`) and so on, will generate additional member method invocations during compilation, causing mock method execution report above error.
The simplest solution is to declare the mock method itself as a `static` type (so that dynamically generated invocation will also be `static` to avoid the errors), for example, the original method is defined as:
```java
@MockMethod
private int getXxx(Demo self) {}
```
Modify it to
```java
@MockMethod
private static int getXxx(Demo self) {}
```
In the next major iteration (**i.e. `v0.5`**), the mock implementation mechanism will be modified while maintaining the current mock experience. Then, it will be no longer necessary to modify the mock method to a static method, and completely solving this problem.
#### 9. Meet "Command Line is too Long. Shorten command line for ..." error when triggering test in IntelliJ IDE?
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`.
![jar-manifest](https://testable-code.oss-cn-beijing.aliyuncs.com/jar-manifest.png)

View File

@@ -0,0 +1,75 @@
Verify Mock Invocation
---
In the test, in addition to replacing **methods contain external dependencies** with mocks, it is often necessary to verify whether the actual parameters of mock invocation are in accordance with expectations.
The **verifiers** and **matchers** are provided in `TestableMock` to achieve this function. for example:
```java
@Test
public test_case() {
int res = insToTest.methodToTest();
verify("mockMethod").with(123, "abc");
}
```
This use case will check whether the mock method named `mockMethod` has been called when the method under test `methodToTest()` is executed, and whether the parameter values received during the call are `123` and `"abc"` (Assuming that the `mockMethod` method has two parameters).
In addition to this simple verification, `TestableMock` currently supports a variety of **validators**, as well as **matchers** that can fuzzy match parameter characteristics.
The `DemoMatcherTest` test classes in the sample projects `java-demo` and `kotlin-demo` show the usage of these validators and matchers in detail.
## Basic validator
- `with(Object...args)` → verify whether the method has been called by the specified parameters
- `withInOrder(Object... args)` → verify the specified method is called with specified parameters according to the actual calling order
- `withTimes(int expectedCount)` → verify whether the method has been called the specified number of times, ignoring the check of the calling parameters
- `without(Object...args)` → verification method has never been called with specified parameters
- `times(int count)` → use after the `with()` or `withInOrder()` method to verify that the method has been called the specified number of times with the same conditions
## Basic matcher
- `any()` → matches any value, including `null`
- `any(Class<?> clazz)` → match any value of the specified type or subtype
- `anyTypeOf(Class<?>... classes)` → match any type of value in the list
- `anyString()` → matches any string
- `anyNumber()` → matches any number (integer, long, float, double, etc)
- `anyBoolean()` → matches any Boolean value
- `anyByte()` → match any single byte type value
- `anyChar()` → matches any single character type value
- `anyInt()` → matches any integer value
- `anyLong()` → matches any value of long integer type
- `anyFloat()` → match any value of float type
- `anyDouble()` → match any double-precision float number type value
- `anyShort()` → matches any value of short integer type
- `anyArray()` → matches any array
- `anyArrayOf(Class<?> clazz)` → matches any array of the specified type
- `anyList()` → matches any list
- `anyListOf(Class<?> clazz)` → matches any list of the specified type
- `anySet()` → matches any set
- `anySetOf(Class<?> clazz)` → matches any set of the specified type
- `anyMap()` → match any map
- `anyMapOf(Class<?> keyClass, Class<?> valueClass)` → match any map of the specified type
- `anyCollection()` → match any container
- `anyCollectionOf(Class<?> clazz)` → matches any specified type of container
- `anyIterable()` → matches any iterator
- `anyIterableOf(Class<?> clazz)` → matches any iterator of the specified type
- `eq(Object obj)` → match objects equal to the specified value
- `refEq(Object obj)` → match the specified object (not equal to the value, but the same object)
## Null value matcher
- `isNull()` → match `null`
- `notNull()` → matches any value except `null`
- `nullable(Class<?> clazz)` → matches any value of `null` or specified type
## String matcher
- `contains(String substring)` → match a string containing a specific substring
- `matches(String regex)` → match strings that match the specified regular expression
- `endsWith(String suffix)` → match the string ending with the specified substring
- `startsWith(String prefix)` → match the string starting with the specified substring
## Universal matcher
- `any(MatchFunction matcher)` → match the value that matches the specified expression

View File

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

View File

@@ -0,0 +1,34 @@
Private Accessor
---
Nowadays, the debate about whether private methods should be unit tested is gradually disappearing, and the common practice of developers has given factual answers. Indirect testing of private methods through public methods is difficult in many cases. Developers are more willing to modify method visibility to make original private methods testable in test cases.
In addition, before unit testing begin, it is often necessary to initialize specific member fields of the object under test, but sometimes it could be impossible to easily assign values to these private fields. So, is it possible let the code in the unit test case directly access the private methods and member fields of the class under test without breaking the encapsulation of the type under test? `TestableMock` provides two simple solutions.
### Solution 1: Use `@EnablePrivateAccess` annotation
Just add `@EnablePrivateAccess` annotation to the test class, then you have got the following enhancements in the test case:
- Invoke private methods (including static methods) of the class under test
- Read private fields (including static fields) of the class under test
- Modify private fields (including static fields) of the class under test
- Modify the constant fields of the class under test (fields modified with final, including static fields)
When accessing and modifying private and constant members, the IDE may prompt some syntax errors, but the compiler will be able to run the test normally.
For the effect, see the use case in the test class of the `java-demo` sample project `DemoPrivateAccessTest`. (Using compile-time code enhancement, currently only the adaptation of the Java language is implemented)
### Solution 2: Use the `PrivateAccessor` tool class
If you don't want to see the IDE's syntax error reminder, or in a non-Java language JVM project (such as Kotlin language), you can also use the `PrivateAccessor` tool class to directly access private members.
This class provides 6 static methods:
- `PrivateAccessor.get(<ObjectUnderTest>, "<private-field-name>")` ➜ read the private field of the class under test
- `PrivateAccessor.set(<ObjectUnderTest>, "<private-field-name>", <new-value>)` ➜ modify the private field (or constant field) of the class under test
- `PrivateAccessor.invoke(<ObjectUnderTest>, "<private-method-name>", <call-parameters>..)` ➜ call the private method of the class under test
- `PrivateAccessor.getStatic(<ClassUnderTest>, "<private-static-field-name>")` ➜ read the **static** private field of the class under test
- `PrivateAccessor.setStatic(<ClassUnderTest>, "<private-static-field-name>", <new-value>)` ➜ modify the **static** private field (or **static** constant field) of the class under test
- `PrivateAccessor.invokeStatic(<ClassUnderTest>, "<private-static-method-name>", <call-parameters>..)` ➜ call the **static** private method of the class under test
For details, see the use cases in the test classes of the `java-demo` and `kotlin-demo` sample projects `DemoPrivateAccessTest`.

View File

@@ -0,0 +1,106 @@
# Release Note
## 0.4.11
- support accessing private members of class under test in different package path
- validate the number of private method parameters accessed by `PrivateAccessor`
- fix a bug which may cause errors when the mock method contains array parameters
- fix an issue which cause some private members not be found in the IntelliJ build
## 0.4.10
- fix an issue of using mock in lambda expression
- fix the NullPointerException when invoke private method with parameter value `null`
## 0.4.9
- fix an issue cause by improperly bytecode processing while using `targetClass` parameter
- auto validate access target of `PrivateAccessor`, improve resistance to code refactoring
## 0.4.8
- fix an issue of private method invocation failed in assignment statements
- support using mock for testing with `SpringRunner`
- support `targetClass` parameter in `@MockMethod` annotation
## 0.4.7
- fix incorrect stack size caused by `MOCK_CONTEXT` variable initialization
- fix incorrect gradle build path of under Windows operating system
## 0.4.6
- fix an issue of `IINC` bytecode processing
- support `TestableTool.MOCK_CONTEXT` variable to inject extra parameters to mock context
- use `TestableTool.TEST_CASE` variable to distinguish test case is no longer recommended
## 0.4.5
- fix private access compile error in intelliJ 2020.3+ environment
- change javaagent initialization logic to avoid NPE in cross-layer test scenario
- support dump transformed byte code to local file for issue investigation
## 0.4.4
- fix an issue of accessing private method with interface type parameter fail
- fix an issue of mocking static method without parameter fail
## 0.4.3
- support static private member access
## 0.4.2
- support change javaagent global log level via maven plugin
- fix an issue of duplicate test class injection
- fix an issue cause multiple-line method invocation mock fail
## 0.4.1
- deprecate @TestableMock annotation, use @MockMethod and @MockConstructor instead
## 0.4.0
- fix a jvm 9+ compatibility issue cause by default classloader change
- fix a conflict issue when testcase name duplicated between different class
- refactor code structure, module `testable-all` added
- separate constant definition from `TestableTool` to `TestableConst`
## 0.3.2
- support gradle project for both private member access and quick mock
- support access private static field and methods via PrivateAccessor
## 0.3.1
- support detail log of mocking process for diagnosis
## v0.3.0
- add `without()` checker to verify mock method never invoked with specified parameters
- support fuzz matcher when verifying mock invocation
## v0.2.2
- support mock method parameters check
- fix a compatibility issue with jvm 9+
## v0.2.1
- support mock static method
- support mock kotlin companion object method
- support mock invoke by an interface / base class object
## v0.2.0
- use `TestableTool` class to expose test context and verify mock invoke
- add `testable-maven-plugin` module to simplify javaagent configuration
- remove dependence on `@EnableTestable` annotation in `testable-agent`
- rename annotations to reflect the actual use
## v0.1.0
- move generated agent jar to class folder
- support mock method of any object
## 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
## v0.0.4
- use runtime byte code rewrite to invoke testable setup method
- add `TestableUtil` class to fetch current test case and invocation source
## 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
## v0.0.2
- add support of member method mocking by compile time code modification
## v0.0.1
- PoC version
- use compile time code modification to support new operation mocking and private field & method access

95
docs/en-us/doc/setup.md Normal file
View File

@@ -0,0 +1,95 @@
Use TestableMock
---
`TestableMock` is an assist tool for Java unit testing based on source code and bytecode enhancement, including the following functions:
- [Quickly mock arbitrary call](en-us/doc/use-mock.md): quickly replace any method invocation in the class under test with a mock method, solve the cumbersome use of traditional mock tools problem
- [Access private members of the class under test](en-us/doc/private-accessor.md): enable unit tests directly invoke or access private members of the class under test, solve the problems of private member initialization and private method testing
- [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
In the project `pom.xml` file, add `testable-all` dependency and `maven-surefire-plugin` configuration, the specific method is as follows.
It is recommended to add a `property` field that identifies the TestableMock version, in order to simplify version management:
```xml
<properties>
<testable.version>0.4.11</testable.version>
</properties>
```
Add dependence of `TestableMock` inside `dependencies` field:
```xml
<dependencies>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
```
Finally, add the `maven-surefire-plugin` plugin to the `plugins` list in the `build` area (if this plugin is already included, just add the `<argLine>` part of the configuration):
```xml
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
</configuration>
</plugin>
</plugins>
</build>
```
If the project also uses the `on-the-fly` mode of `Jacoco` (default mode) to calculate the unit test coverage, you need to add a `@{argLine}` parameter in the `<argLine>` configuration, after adding it The configuration is as follows:
```xml
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
```
See the [pom.xml](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/pom.xml) file of project `java-demo` and the [pom.xml](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/pom.xml) file of project `kotlin-demo`.
## Use in Gradle project
Add dependence of `TestableMock` in `build.gradle` file:
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.4.11')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.11')
}
```
Then add `javaagent` to "test" configuration
```groovy
test {
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
}
```
See the [build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/build.gradle) file of project `java-demo` and the [build.gradle.kts](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/build.gradle.kts) file of project `kotlin-demo`.
> For Android project tested with `Robolectric` framework, please use the same method to add `TestableMock` dependency as above, and add `javaagent` configuration as follows:
>
> ```groovy
> android {
> testOptions {
> unitTests {
> all {
> jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
> }
> }
> }
> }
> ```
>
> See [issue-43](https://github.com/alibaba/testable-mock/issues/43) for a complete example.

View File

@@ -0,0 +1,94 @@
Test Void Method
---
"How to test void type methods" has always been a topic that many unit testing frameworks are quietly avoiding. Since the existing unit testing methods are mainly to verify the returned results of the tested unit, when the method has no return value, there is no way to test.
From a functional point of view, although the void method does not return any value, its execution will definitely have some potential impact on the outside world. We call it the "side effect" of the method, such as:
1. Initialize external variables (private member variables or global static variables)
2. Assign value to external object
3. Print logs
4. Invoke other external methods
5. ... ...
> A method that does not return any value and does not produce any "side effects" has no meaning to exist.
The essence of these "side effects" can be summarized into two categories: **modify external variables** and **invoke external methods**.
Through the private field accessor and the mock validator of `TestableMock`, the "side effects" can be easily checked.
### 1. Void type method which modify external variables
For example, the following method will modify the private member variable `hashCache` based on the input:
```java
class Demo {
private Map<String, Integer> hashCache = mapOf();
public void updateCache(String domain, String key) {
String cacheKey = domain + "::" + key;
Integer num = hashCache.get(cacheKey);
hashCache.put(cacheKey, count == null ? initHash(key) : nextHash(num, key));
}
... // Other methods omitted
}
```
To test this method, you can use `TestableMock` directly read the value of the private member variable and verify the result:
```java
@EnablePrivateAccess // Enable private member access functionality of TestableMock
class DemoTest {
private Demo demo = new Demo();
@Test
public void testSaveToCache() {
Integer firstVal = demo.initHash("hello"); // Invoke private method
Integer nextVal = demo.nextHash(firstVal, "hello"); // Invoke private method
demo.saveToCache("demo", "hello");
assertEquals(firstVal, demo.hashCache.get("demo::hello")); // Access private variable
demo.saveToCache("demo", "hello");
assertEquals(nextVal, demo.hashCache.get("demo::hello")); // Access private variable
}
}
```
### 2. Void type method which invoke external method
For example, the following method will print information to the console based on input:
```java
class Demo {
public void recordAction(Action action) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss ");
String timeStamp = df.format(new Date());
System.out.println(timeStamp + "[" + action.getType() + "] " + action.getTarget());
}
}
```
To test this method, you can use `TestableMock` to quickly mock out the `System.out.println` method. In the mock method body, you can simply call the original method (equivalent to not affecting the original method function, only used for call recording), or leave it blank (equivalent to removing the side effects of the original method).
After executing the void type method under test, use `InvokeVerifier.verify()` to verify whether the incoming print content meets expectations:
```java
class DemoTest {
private Demo demo = new Demo();
// Intercept `System.out.println` invocation
@MockMethod
public void println(PrintStream ps, String msg) {
// Execute the original call
ps.println(msg);
}
@Test
public void testRecordAction() {
Action action = new Action("click", ":download");
demo.recordAction();
// Verify mock method `println` is invoked, and passing parameters in line with expectations
verify("println").with(matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download"));
}
}
```

View File

@@ -0,0 +1,46 @@
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.
```java
@MockWith(diagnose = MockDiagnose.ENABLE)
class DemoTest {
...
}
```
The output log example is as follows:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
[DIAGNOSE] Found 7 mock methods
[DIAGNOSE] Handling method <init>
[DIAGNOSE] Handling method newFunc
[DIAGNOSE] Line 14, mock method createBlackBox used
[DIAGNOSE] Handling method outerFunc
[DIAGNOSE] Line 22, mock method innerFunc used
[DIAGNOSE] Handling method commonFunc
[DIAGNOSE] Line 29, mock method trim used
[DIAGNOSE] Line 29, mock method sub used
[DIAGNOSE] Line 29, mock method startsWith used
[DIAGNOSE] Handling method getBox
[DIAGNOSE] Line 36, mock method secretBox used
[DIAGNOSE] Handling method callerOne
[DIAGNOSE] Line 43, mock method callFromDifferentMethod used
[DIAGNOSE] Handling method callerTwo
[DIAGNOSE] Line 47, mock method callFromDifferentMethod used
[DIAGNOSE] Handling method innerFunc
[DIAGNOSE] Handling method callFromDifferentMethod
```
The log shows all the mocked invocation and corresponding code line numbers in the class under test.
- Self troubleshooting:
- If there is no output, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies
- If only the first line of `Handling test class` is output, please check whether the test class is in the same package of the class under test, and the name is "<ClassUnderTest>+Test" (required for `0.4.x` version)
- If `Handling source class` and `Handling method xxx` are output, but there is no mock replacement happen at the expected code line, please check whether the mock method definition matches the target method

View File

@@ -0,0 +1,18 @@
Use TestableMock In IDE
---
## Use IntelliJ IDE
IntelliJ IDE supports the `JSR-269` annotation processor and the `maven-surefire-plugin` arguments very well (both are techniques back the `TestableMock`). Usually you don't need any special configuration to make everything work, it's all out of the box.
## Use Eclipse IDE
Since the built-in compilation feature of `Eclipse` is based on a self-made compiler, it is not compatible with the standard `javac` compilation process, which will cause the `@EnablePrivateAccess` annotation to be invalid when running test cases in the IDE. However, the function of accessing the private members of the class under test through the `PrivateAccessor` tool class will not be affected by differences in the compiler.
If the `@EnablePrivateAccess` annotation is used in the project, you can use `mvn test -Dtest=<TestClassName>` and `mvn test -Dtest=<TestClassName>#<TestCaseName>` in the command line of `Eclipse` to run a single test class or test case.
At the same time, because the built-in unit test executor of `Eclipse` completely ignores the configuration of the `pom.xml` file, additional configuration is required to use the Mock function.
Take the use of `JUnit` as an example. You need to pull down from the small triangle next to the run button on the IDE toolbar, select "Run Configurations...", select the task to run the unit test on the left side, and switch to "arguments" Tab on the right side, append a `-javaagent:` parameter in the "VM Options", the following figure is an example, note that the `testable-agent` package should be modified to match the actual situation of the local Maven repository path.
![eclipse-junit-configuration](https://testable-code.oss-cn-beijing.aliyuncs.com/eclipse-junit-configuration.png)

View File

@@ -0,0 +1,42 @@
Testable Maven Plugin
---
For projects built with Maven, in addition to directly modifying the argument of the `maven-surefire-plugin` plugin, the same effect can also be obtained through the `testable-maven-plugin` plugin:
```xml
<plugin>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-maven-plugin</artifactId>
<version>${testable.version}</version>
<executions>
<execution>
<id>prepare</id>
<goals>
<goal>prepare</goal>
</goals>
</execution>
</executions>
</plugin>
```
> When using `testable-maven-plugin`, the `TestableMock` related configuration on `maven-surefire-plugin` should be removed.
The `testable-maven-plugin` can be used with the Jacoco plugin without additional adaptation, so it can make the writing of the `pom.xml` file easier and more readable.
However, it should be noted that when running a single test case through the IDE, those mock function may fail to work.
This is because the IDE usually only runs the `maven-surefire-plugin` plugin when running a single test case, skipping the execution of the `testable-maven-plugin` plugin, resulting in the JavaAgent required for the mock function not being injected into the context.
This problem can be bypassed by configuring the test parameters of the IDE additionally. Take IntelliJ as an example, open the "Edit Configuration..." option of the run menu, as shown in the position ①
![modify-run-configuration](https://testable-code.oss-cn-beijing.aliyuncs.com/modify-run-configuration.png)
Add JavaAgent startup parameters at the end of the "virtual machine parameters" attribute value: `-javaagent:${HOME}/.m2/repository/com/alibaba/testable/testable-agent/xyz/testable-agent-xyzjar`, as shown in the figure position ②
> PS: Please replace `x.y.z` in the path with the actual version number
![add-testable-javaagent](https://testable-code.oss-cn-beijing.aliyuncs.com/add-testable-javaagent.png)
Finally, click to run the unit test, as shown in the position ③
In general, the additional complexity of using `testable-maven-plugin` is still higher than its simplified configuration. At present, modify the configuration of the `maven-surefire-plugin` directly in the `pom.xml` file is still the preferred solution.

167
docs/en-us/doc/use-mock.md Normal file
View File

@@ -0,0 +1,167 @@
Fast Mocking
---
Compared with the class-granularity mocking practices of existing mock tools, `TestableMock` allows developers to directly define a single method and use it for mocking. With the principle of convention over configuration, mock method replacement will automatically happen when the specified method in the test class match an invocation in the class under test.
> In summary, there are two simple rules:
> - Mock non-constructive method, copy the original method definition to the test class, add a `@MockMethod` annotation
> - Mock construction method, copy the original method definition to the test class, replace the return value with the constructed type, the method name is arbitrary, and add a `@MockContructor` annotation
The detail mock method definition convention is as follows:
#### 1. Mock method calls of any class
Define an ordinary method annotated with `@MockMethod` in the test class with exactly the same signature (name, parameter, and return value type) as the method to be mocked, and then add the type of target object (which the method originally belongs to) as `targetMethod` parameter of `@MockMethod` annotation.
At this time, all invocations to that original method in the class under test will be automatically replaced with invocations to the above-mentioned mock method when the unit test is running.
For example, there is a call to `"anything".substring(1, 2)` in the class under test, and we want to change it to a fixed string when running the test, we only need to define the following method in the test class:
```java
// The original method signature is `String substring(int, int)`
// The object `"anything"` that invokes this method is of type `String`
@MockMethod(targetClass = String.class)
private String substring(int i, int j) {
return "sub_string";
}
```
When several methods to be mocked have the same name, you can put the name of the method to be mocked in the `targetMethod` parameter of `@MockMethod` annotation, so that the mock method itself can be named at will.
The following example shows the usage of the `targetMethod` parameter, and its effect is the same as the above example:
```java
// Use `targetMethod` to specify the name of the method that needs to be mocked
// The method itself can now be named arbitrarily, but the method parameters still need to follow the same matching rules
@MockMethod(targetClass = String.class, targetMethod = "substring")
private String use_any_mock_method_name(int i, int j) {
return "sub_string";
}
```
Sometimes, the mock method need to access the member variables in the original object that initiated the invocation, or invoke other methods of the original object. At this point, you can remove the `targetClass` parameter in the `@MockMethod` annotation, and then add a extra parameter whose type is the original object type of the method to the first index of the method parameter list.
The `TestableMock` convention is that when the `targetClass` parameter value of the `@MockMethod` annotation is empty, the first parameter of the mock method is the type of the target method, and the parameter name is arbitrary. In order to facilitate code reading, it is recommended to name this parameter as `self` or `src`. Example as follows:
```java
// Adds a `String` type parameter to the first position the mock method parameter list (parameter name is arbitrary)
// This parameter can be used to get the value and context of the actual invoker at runtime
@MockMethod
private String substring(String self, int i, int j) {
// Call the original method is also allowed
return self.substring(i, j);
}
```
For complete code examples, see the `should_able_to_mock_common_method()` test cases in the `java-demo` and `kotlin-demo` sample projects. (Because Kotlin has made magical changes to the String type, the method under test in the Kotlin example adds a layer of encapsulation to the `BlackBox` class)
#### 2. Mock the member method of the class under test itself
Sometimes, when testing certain methods, it is desirable to mock out some other member methods of the class under test itself.
The solution is the same as the previous case. Just set `targetClass` parameter value to the type of class under test.
For example, there is a private method with the signature `String innerFunc(String)` in the class under test. If we want to replace it during testing, we only need to define the following method in the test class:
```java
// The type to test is `DemoMock`
@MockMethod(targetClass = DemoMock.class)
private String innerFunc(String text) {
return "mock_" + text;
}
```
Similarly, if the method in the above example needs to access the original tested object that initiated the call, it may not use the `targetClass` parameter, but when defining the mock method, add a parameter of type `DemoMock` to the first index of the method parameter list.
For complete code examples, see the `should_able_to_mock_member_method()` test case in the `java-demo` and `kotlin-demo` sample projects.
#### 3. Mock static methods of any class
Mock for static methods is the same as for any ordinary methods.
For example, if the static method `secretBox()` of the `BlackBox` type is invoked in the class under test, and the method signature is `BlackBox secretBox()`, then the mock method is as follows:
```java
@MockMethod(targetClass = BlackBox.class)
private BlackBox secretBox() {
return new BlackBox("not_secret_box");
}
```
For complete code examples, see the `should_able_to_mock_static_method()` test case in the `java-demo` and `kotlin-demo` sample projects.
#### 4. Mock `new` operation of any type
Define an ordinary method annotated with `@MockContructor` in the test class, make the return value type of the method the type of the object to be created, and the method parameters are exactly the same as the constructor parameters to be mocked, the method name is arbitrary.
At this time, all operations in the class under test that use `new` to create the specified class (and use the constructor that is consistent with the mock method parameters) will be replaced with calls to the custom method.
For example, if there is a call to `new BlackBox("something")` in the class under test, and you want to change it during unit testing (usually by replacing it with a mock object, or by replacing it with a temporary object created with test parameters), just define the following mock method:
```java
// The signature of the constructor to be mocked is `BlackBox(String)`
// No need to add additional parameters to the mock method parameter list, and the name of the mock method is arbitrary
@MockContructor
private BlackBox createBlackBox(String text) {
return new BlackBox("mock_" + 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.
#### 5. Identify the current test case and invoke source
In the mock method, you can use the `TestableTool.SOURCE_METHOD` variable to identify **the method name of the class under test before entering the mock method**; in addition, the `TestableTool.MOCK_CONTEXT` variable can **inject additional context parameters into the mock method**, to distinguish and process different calling scenarios.
For example, to verify the impact on the target method under test when the mock method returns different results in the test case:
```java
@Test
public void testDemo() {
MOCK_CONTEXT.set("case", "data-ready");
assertEquals(true, demo());
MOCK_CONTEXT.set("case", "has-error");
assertEquals(false, demo());
MOCK_CONTEXT.clear();
}
```
Take out the injected parameters in the mock method and return different results according to the situation:
```java
@MockMethod
private Data mockDemo() {
switch((String)MOCK_CONTEXT.get("case")) {
case "data-ready":
return new Data();
case "has-error":
throw new NetworkException();
default:
return null;
}
}
```
Note that because `TestableMock` does not (and won't to) rely on any specific test framework, it cannot automatically identify the end position of a single test case, which makes the parameters set to the `TestableTool.MOCK_CONTEXT` variable may exist cross test cases in the same test class. It is recommended to always use `MOCK_CONTEXT.clear()` to clear the context immediately after use. You can also add this statement to the unified position where the test case ends of the specific unit test framework, such as the `@AfterEach` method of JUnit 5.
In the current version, the effect of this variable at runtime is similar to a normal `Map` type member object in the test class, but please try to use this variable instead of a custom object to pass additional mock parameters in order to get better compatibility in the upcoming`v0.5` version.
> The `TestableTool.MOCK_CONTEXT` variable is currently shared within the test class. When the unit test runs in parallel, it is recommended to select the `parallel` type as `classes`
For complete code examples, see the `should_able_to_get_source_method_name()` and `should_able_to_get_test_case_name()` test cases in the `java-demo` and `kotlin-demo` sample projects.
#### 6. Verify the sequence and parameters of the mock method being invoked
In test cases, you can use the `TestableTool.verify()` method, and cooperate with `with()`, `withInOrder()`, `without()`, `withTimes()` and other methods to verify the mock call situation.
For details, please refer to the [Check Mock Call](en-us/doc/matcher.md) document.
#### Additional note
> **Mock convention in version 0.4.x**:
> - The name of the test class must be `<NameOfClassUnderTest> + Test` (and in the same package path), which is usually the by-default naming convention of Java project managed by `Maven` or `Gradle`.
> - Do NOT access any non-`static` members in mock methods. Currently, methods that is decorated by `@MockMethod` or `@MockContructor` annotations will be automatically modified to `static` methods during runtime. (When mock method contains some statement like _lambda function_, _anonymous class_ or _initiation block_, java compiler will generate additional method during compilation, these mock methods also have to be declared as `static` to avoid non-static dynamical method invoked.)
>
> These constraints will change in `0.5` versions of `TestableMock`.

5
docs/en-us/navbar.md Normal file
View File

@@ -0,0 +1,5 @@
* [Home](/en-us/)
* [Feedback And Issue](https://github.com/alibaba/testable-mock/issues)
* Languages
* English
* [中文](/zh-cn/)

19
docs/en-us/sidebar.md Normal file
View File

@@ -0,0 +1,19 @@
- Quick Start
- [Use TestableMock](en-us/doc/setup.md)
- [Fast Mocking](en-us/doc/use-mock.md)
- [Private Accessor](en-us/doc/private-accessor.md)
- [Test Void Method](en-us/doc/test-void-method.md)
- [Parameter constructor](en-us/doc/parameter-constructor.md)
- Usage Guide
- [Verify Mock Invocation](en-us/doc/invoke-matcher.md)
- [Frequently Asked Questions](en-us/doc/frequently-asked-questions.md)
- [Use TestableMock In IDE](en-us/doc/use-in-ide.md)
- [Self-Help Troubleshooting](en-us/doc/troubleshooting.md)
- [Testable Maven Plugin](en-us/doc/use-maven-plugin.md)
- Technical Reference
- [Mock Tools Comparison](en-us/doc/comparation.md)
- [Release Note](en-us/doc/release-note.md)
- [About Us](en-us/doc/about-us.md)
- [Feedback Channel](en-us/doc/feedback.md)

View File

@@ -14,7 +14,7 @@
<div id="app">Loading...</div>
<script>
window.$docsify = {
name: 'Testable',
name: 'TestableMock',
repo: 'https://github.com/alibaba/testable-mock',
loadSidebar: "sidebar.md",
loadNavbar: "navbar.md",
@@ -26,7 +26,7 @@
hook.beforeEach(function (html) {
return html
+ '\n\n----\n\n'
+ '<a href="https://devops.aliyun.com" target="_blank" style="color: inherit; font-weight: normal; text-decoration: none;">Powered by 云效</a>'
+ '<a href="https://www.aliyun.com/product/yunxiao" target="_blank" style="color: inherit; font-weight: normal; text-decoration: none;">Powered by 云效</a>'
})
}
]

View File

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

View File

@@ -0,0 +1,9 @@
## 关于我们
`TestableMock`的核心开发团队来自**阿里云·云研发部**(前身为**阿里集团研发效能部**)。我们不仅对内支持阿里数万名技术小二每天从任务分配、代码提交、功能测试、到产品发布的全生命周期流程,同时也是阿里云企业级研发协同平台[云效](https://www.aliyun.com/product/yunxiao)的产品研发团队,一直深耕在开发者工具领域,致力于改善开发者生产体验和构筑企业数字化研发生态。
这个项目的原始灵感来自于我们对Java开发者在日常单元测试中经常遇到的痛点总结[外部依赖Mock繁琐](zh-cn/doc/use-mock.md)、[私有方法难测试](zh-cn/doc/private-accessor.md)、[无返回值方法难测试](zh-cn/doc/test-void-method.md)、[复杂参数难构造](zh-cn/doc/parameter-constructor.md))。它在内部的名字是`Testable`,开源时为了搜索引擎优化,改名为`TestableMock`并主推轻量级Mock功能。 不要被它的名字所迷惑,`TestableMock`绝不仅仅是Mock。
从黑客马拉松孵化,到内部开源,再到对外开源,`TestableMock`已经积累了一批阿里集团内部和外部社区的开发者用户。与此同时,我们也在不断完善`TestableMock`自身的功能丰富性和稳定性。按照当前的版本发布计划,每次新版本发布前会首先提交包含`SNAPSHOT`标记的内测版到内部仓库在至少稳定使用一天以后才会成为正式版提交到Maven中心仓库紧急BUG修复版本除外以确保工具在为大家带来更多便捷的同时具有足够的可靠性。
最后,关于社区里有对“破坏封装进行单元测试是否等同于纵容代码腐化”的讨论,我们持相对乐观的态度。正如`PowerMock`的诞生并没有真的催生更多烂项目反而切实解决了许多Java语言遗留的测试难题。与其束手束脚的采用“测试技巧”间接测试功能不如索性解除限制就让单元测试来得更猛烈一些吧🤠

View File

@@ -1,3 +1,26 @@
与其他工具比较
主流Mock工具对比
---
`TestableMock`目前主要的Mock工具主要有`Mockito``Spock``PowerMock``JMockit`,基本差异如下:
| 工具 | 原理 | 最小Mock单元 | 对被Mock方法的限制 | 上手难度 | IDE支持 |
| ---- | ---- | ---- | ---- | ---- | ---- |
| Mockito | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | **较容易** | **很好** |
| Spock | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | 较复杂 | 一般 |
| PowerMock | 自定义类加载器 | 类 | **任何方法皆可** | 较复杂 | **较好** |
| JMockit | 运行时字节码修改 | 类 | 不能Mock构造方法(new操作符) | 较复杂 | 一般 |
| TestableMock | 运行时字节码修改 | 方法 | **任何方法皆可** | **很容易** | 一般 |
`Mockito`是Java最老牌的Mock工具稳定性和易用性较好IntelliJ和Eclipse都有专用插件支持。相对不足之处在于Mock功能稍弱在必要情况下需与其他Mock工具配合使用。
`Spock`是一款代码可读性非常高的单元测试框架内置Mock支持具有很好的整体感。由于同样基于动态代理实现其不足点与`Mockito`类似。
`PowerMock`是一款功能十分强大的Mock工具其基本语法与`Mockito`兼容,同时扩展了许多`Mockito`缺失的功能包括对支持对私有、静态和构造方法实施Mock。但由于使用了自定义类加载器会导致Jacoco在默认的`on-the-fly`模式下覆盖率跌零。
`JMockit`是一款功能性与易用性均居于`Mockito``PowerMock`之间的Mock工具较好的弥补了两者各自的不足。该项目在2017年尝试推出JMockit2重写版本但未能完成目前处于不活跃的维护状态。
相比之下,`TestabledMock`的功能与`PowerMock`基本平齐,且极易上手,只需掌握`@MockMethod`注解就可以完成绝大多数任务。
当前`TestableMock`的主要不足在于编写Mock方法时IDE尚无法即时提示方法参数是否正确匹配。若发现匹配效果不符合预期需要通过[自助问题排查](zh-cn/doc/troubleshooting.md)文档提供的方法在运行期进行校验。这个功能未来需要通过扩展主流IDE插件来提供。
此外,由于`TestableMock`独辟蹊径的采用基于单个方法的Mock机制将Mock方法定义与单元测试用例解耦一方面使得Mock方法具有默认可复用性单元测试用例也因此变得更干净纯粹另一方面也导致Mock方法定义变得零散生命周期管理起来相对困难对现有开发者的Mock编写习惯会带来一定改变。

View File

@@ -0,0 +1,8 @@
问题反馈
---
在使用`TestableMock`过程中如有任何问题或建议,请直接在[项目Issue](https://github.com/alibaba/testable-mock/issues)中提出我们将在24小时内回复。
请详细描述遇到的情况,若您觉得问题可能与`TestableMock`的潜在BUG有关请尽量提供可复现的相关源码或示例以便进行针对性定位和排查。
`TestableMock`让Java没有难测的代码 : )

View File

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

View File

@@ -0,0 +1,74 @@
常见使用问题
---
#### 1. 如何Mock被测类中通过`@Autowired`初始化的字段?
直接创建被测类对象,然后利用`TestableMock`访问私有成员的能力直接给这些字段赋值即可。
#### 2. `TestableMock`是否能够与其他Mock工具一起使用
`TestableMock`可与其他基于动态代理机制的Mock工具安全的共同使用譬如`Mockito``Spock``EasyMock`等皆属此范畴。
对于会修改类加载器或被测类字节码的Mock工具譬如`PowerMock``JMockit`,尚无案例证明会与`TestableMock`发生冲突,但从原理来说二者可能存在不兼容风险,请谨慎使用。
#### 3. 父类变量指向子类对象时如何实现Mock方法
在代码中,经常会有使用<u>接口变量或父类变量</u>指向子类实例,调用父类或子类方法的情况。
这时候遵循一个原则Mock方法的首个参数类型**始终与发起调用的变量类型一致**。
因此不论实际被调用方法来自父类还是子类也不论子类是否覆写该方法。若发起调用的变量为父类型或接口类型则Mock方法的首个参数类型都应该使用相应的父类或接口类型。
参见Java和Kotlin示例中`DemoInheritTest`测试类的用例。
#### 4. 如何Mock对于泛型方法(模板方法)
与普通方法的Mock方法相同直接在Mock方法上使用相同的泛型参数即可。
参见Java和Kotlin示例中`DemoTemplateTest`测试类的用例。
> 由于JVM存在泛型擦除机制对于Java项目也可以直接使用`Object`类型替代泛型参数见Java版`DemoTemplateTest`测试类中被注释掉的"第二种写法"示例。
#### 5. 在Kotlin项目对`String`类中的方法进行Mock不生效
Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.String`。但在构建生成自字节码的时候又会被替换为Java的`java.lang.String`因此无论将Mock目标写为`kotlin.String``java.lang.String`均无法正常匹配到原始的被调用方法。
实际场景中需要对`String`类中的方法进行Mock的场景很少`TestableMock`暂未对这种情况做特别处理。
#### 6. 当被Mock的方法被其它测试类**间接调用**时依然有效吗?
同样有效Mock的作用范围是整个测试运行过程。
例如测试类`AaaTest`中Mock了`Aaa`类的某些私有方法(或者某些外部方法调用);在另一个测试类`BbbTest`中测试`Bbb`类时,某些方法间接用到了`Aaa`类被Mock过的方法或调用此时实际调用的同样会是`AaaTest`类中定义的Mock方法。
#### 7. `TestableMock`能否用于Android项目的测试
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。
Android系统的`Dalvik``ART`虚拟机采用了与标准JVM不同的字节码体系会影响`TestableMock`的正常工作。`Roboelectric`框架能在普通JVM虚拟机上运行Android单元测试其速度比通过Android虚拟机运行单元测试快非常多绝大多数Android App的单元测试都在使用`Roboelectric`框架。
#### 8. 使用Mock时候遇到"Attempt to access non-static member in mock method"错误?
当前`TestableMock`的设计不允许在Mock方法中访问测试类的非`static`成员因为Mock方法自身会在运行期被动态修改为`static`类型。然而有些Java语句包括构造块譬如`new ArrayList<String>() {{ append("data"); }}`)、匿名函数(譬如`list.stream().map(i -> i.get)`等等会在编译过程中生成额外的成员方法调用导致Mock方法执行报错。
最简单的解决办法是将Mock方法本身也声明为`static`类型(这样动态生成的调用也会是`static`的,避免了以上错误),例如原方法定义为:
```java
@MockMethod
private int getXxx(Demo self) {}
```
将其修改为:
```java
@MockMethod
private static int getXxx(Demo self) {}
```
在下一个大迭代版本(**即`0.5`版本**将会在保持当前Mock体验的前提下对Mock的实现机制进行修改不再需要修改Mock方法为静态方法从而彻底解决此类报错问题。
#### 9. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
这个问题是由于系统ClassPath包含太多路径所致与是否使用`TestableMock`无关。但需要注意的是IntelliJ提供了两种辅助解决机制`JAR manifest``classpath file`,若测试中使用了`TestableMock`,请选择`JAR manifest`
![jar-manifest](https://testable-code.oss-cn-beijing.aliyuncs.com/jar-manifest.png)

View File

@@ -3,7 +3,7 @@
在测试中除了需要将某些含有外部依赖的方法替换为Mock经常还会需要验证该方法被调用时的参数是否符合预期。
在TestableMock中提供了校验器verifier和匹配器matcher来实现这一功能。譬如
`TestableMock`中提供了校验器verifier和匹配器matcher来实现这一功能。譬如
```java
@Test
@@ -15,7 +15,7 @@ public test_case() {
这个用例会检查在执行被测方法`methodToTest()`时,名称是`mockMethod`的Mock方法是否有被调用过且调用时收到的参数值是否为`123``"abc"`假设被Mock的`mockMethod`方法有两个参数)。
除了这种简单校验以外TestableMock当前已经支持了多种**校验器**,以及能够模糊匹配参数特征的**匹配器**。
除了这种简单校验以外,`TestableMock`当前已经支持了多种**校验器**,以及能够模糊匹配参数特征的**匹配器**。
在示例项目`java-demo``kotlin-demo`中的`DemoMatcherTest`测试类详细展示了这些校验器和匹配器的用法。

View File

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

View File

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

View File

@@ -1,39 +1,84 @@
# Release Note
## 0.4.11
- 支持测试类访问与自身包路径不同的被测类的私有成员
- 增加`PrivateAccessor`访问的私有方法参数数目检查,提高抗代码重构能力
- 修复被Mock方法包含数组参数可能导致出错的BUG (issue-48)
- 修复一处会导致在IntelliJ中构建找不到私有成员的问题
## 0.4.10
- 修复在Lambda函数中使用Mock出错的BUGissue-44
- 修复调用私有方法时参数值不能为null的问题issue-27
## 0.4.9
- 修复发起调用的对象不是局部或成员变量时Mock出错的BUG (issue-40)
- 增加`PrivateAccessor`访问目标有效性检查,提高抗代码重构能力 (issue-21)
## 0.4.8
- 修复赋值语句中的私有方法调用无法访问的BUG (issue-33)
- 支持在包含`SpringRunner`的测试中使用Mock (issue-30)
- `@MockMethod`注解支持`targetClass`参数 (issue-24)
## 0.4.7
- 修复由于`MOCK_CONTEXT`引入的堆栈大小错误
- 修复Windows下的`Gradle`构建路径错误 (issue-25)
## 0.4.6
- 修复一处`IINC`字节码处理异常
- 支持使用`TestableTool.MOCK_CONTEXT`变量为Mock方法注入额外上下文参数 (issue-17)
- 不再推荐使用`TestableTool.TEST_CASE`变量来区分测试用例
## 0.4.5
- 修复IntelliJ 2020.3+环境下的私有成员访问编译期错误
- 修复潜在的跨用例初始化空指针异常 (issue-20)
- 支持将运行期修改后的字节码Dump到本地文件
## 0.4.4
- 修复无法访问参数类型包含接口的私有方法的BUG (issue-15)
- 修复Mock无参数的静态方法会出错的BUG (issue-16)
## 0.4.3
- 完善了对私有静态成员的直接访问能力
## 0.4.2
- 支持通过Maven插件设置`TestableAgent`的全局日志级别
- 修复测试类可能被重复注入多余字节码的BUG
- 修复一处导致跨行的方法调用Mock报错的BUG
## 0.4.1
- deprecate @TestableMock annotation, use @MockMethod and @MockConstructor instead
- `@TestableMock`注解已弃用,推荐使用`@MockMethod``@MockConstructor`注解
## 0.4.0
- fix a jvm 9+ compatibility issue cause by default classloader change
- fix a conflict issue when testcase name duplicated between different class
- refactor code structure, module `testable-all` added
- separate constant definition from `TestableTool` to `TestableConst`
- 修复由于默认类加载器变化导致JVM 9+运行时Mock出错的BUG
- 修复多个测试类包含同名Mock方法时潜在的串号问题
- 重构项目结构,增加用于简化依赖配置的`testable-all`模块
- `TestableTool`中与用户无关的内容迁移到了`TestableConst`类型
## 0.3.2
- support grable project for both private member access and quick mock
- support access private static field and methods via PrivateAccessor
- 支持在Gradle项目中使用私有成员访问和快速Mock功能
- 支持通过`PrivateAccessor`工具类访问私有静态成员
## 0.3.1
- support detail log of mocking process for diagnosis
- 支持使用`@MockWith`注解打印详细Mock执行过程
## v0.3.0
- add `without()` checker to verify mock method never invoked with specified parameters
- support fuzz matcher when verifying mock invocation
- 增加`without()`校验器用于匹配指定Mock方法从未被调用的场景
- 支持对Mock调用参数验证时使用模糊匹配
## v0.2.2
- support mock method parameters check
- fix a compatibility issue with jvm 9+
- 支持对Mock调用参数进行校验
- 修复JVM 9+的兼容性问题
## v0.2.1
- support mock static method
- support mock kotlin companion object method
- support mock invoke by interface / base class object
- 支持Mock静态方法
- 支持Mock Kotlin Companion对象的方法
- 支持Mock接口或父类引用指向子类实例时的调用
## v0.2.0
- use `TestableTool` class to expose test context and verify mock invoke
- add `testable-maven-plugin` module to simplify javaagent configuration
- remove dependence on EnableTestable annotation in `testable-agent`
- rename annotations to reflect the actual use
- 支持通过`TestableTool`工具类识别当前的调用上下文
- 增加`testable-maven-plugin`模块用于简化运行时JavaAgent配置
- 不再需要使用`@EnableTestable`注释显式标记测试类
- 重名了现有的各种注解,以便与更好的与实际功能对应
## v0.1.0
- move generated agent jar to class folder

View File

@@ -3,9 +3,10 @@
`TestableMock`是基于源码和字节码增强的Java单元测试辅助工具包含以下功能
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
- [快速Mock任意调用](zh-cn/doc/use-mock.md)使被测类的任意方法调用快速替换为Mock方法实现"指哪换哪"解决传统Mock工具使用繁琐的问题
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
- [辅助测试void方法](zh-cn/doc/test-void-method.md)利用Mock校验器对方法的内部逻辑进行检查解决无返回值方法难以实施单元测试的问题
- [快速构造参数对象](zh-cn/doc/parameter-constructor.md):生成任意多层嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题
## 在Maven项目中使用
@@ -15,7 +16,7 @@
```xml
<properties>
<testable.version>0.4.1</testable.version>
<testable.version>0.4.11</testable.version>
</properties>
```
@@ -27,7 +28,7 @@
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
<scope>test</scope>
</dependency>
</dependencies>
```
@@ -58,12 +59,12 @@
## 在Gradle项目中使用
`build.gradle`文件中添加TestableMock依赖
`build.gradle`文件中添加`TestableMock`依赖:
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.4.1')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.1')
testImplementation('com.alibaba.testable:testable-all:0.4.11')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.11')
}
```
@@ -76,3 +77,19 @@ test {
```
参见项目`java-demo`的[build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/build.gradle)和`kotlin-demo`的[build.gradle.kts](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/build.gradle.kts)文件。
> 若是基于`Robolectric`框架的Android项目则添加`TestableMock`依赖方法同上添加javaagent配置方法如下
>
> ```groovy
> android {
> testOptions {
> unitTests {
> all {
> jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
> }
> }
> }
> }
> ```
>
> 完整示例参考[issue-43](https://github.com/alibaba/testable-mock/issues/43)

View File

@@ -16,7 +16,7 @@
这些"副作用"的本质归纳来说可分为两类:**修改外部变量**和**调用外部方法**。
通过TestableMock的私有字段访问和Mock校验器可以很方便的实现对"副作用"的结果检查。
通过`TestableMock`的私有字段访问和Mock校验器可以很方便的实现对"副作用"的结果检查。
### 1. 修改外部变量的void方法
@@ -36,7 +36,7 @@ class Demo {
}
```
若要测试此方法可以利用TestableMock直接读取私有成员变量的值对结果进行校验
若要测试此方法,可以利用`TestableMock`直接读取私有成员变量的值,对结果进行校验:
```java
@EnablePrivateAccess // 启用TestableMock的私有成员访问功能
@@ -69,7 +69,7 @@ class Demo {
}
```
若要测试此方法可以利用TestableMock快速Mock掉`System.out.println`方法。在Mock方法体里可以继续执行原调用相当于并不影响本来方法功能仅用于做调用记录也可以直接留空相当于去除了原方法的副作用
若要测试此方法,可以利用`TestableMock`快速Mock掉`System.out.println`方法。在Mock方法体里可以继续执行原调用相当于并不影响本来方法功能仅用于做调用记录也可以直接留空相当于去除了原方法的副作用
在执行完被测的void类型方法以后`InvokeVerifier.verify()`校验传入的打印内容是否符合预期:
@@ -89,7 +89,7 @@ class DemoTest {
Action action = new Action("click", ":download");
demo.recordAction();
// 验证Mock方法`println`被调用,且传入参数符合预期
verify("println").matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download");
verify("println").with(matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download"));
}
}
```

View File

@@ -38,3 +38,9 @@ class DemoTest {
```
该日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
简单排查方法:
- 若没有任何输出,请检查`pom.xml``build.gradle`配置是否正确引入了TestableMock依赖
- 若只输出了第一行`Handling test class`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test"`0.4.x`版本要求)
- 若输出了`Handling source class`以及`Handling method xxx`但预期的代码行位置没有发生Mock替换请检查Mock方法定义是否未与目标方法匹配

View File

@@ -0,0 +1,18 @@
在IDE中运行单元测试
---
## 使用IntelliJ IDE
IntelliJ IDE对`TestableMock`所用到的`JSR-269`注释处理器以及`maven-surefire-plugin`插件的附加参数均支持良好。通常无需特殊配置,可开箱即用。
## 使用Eclipse IDE
由于`Eclipse`内置的自动编译功能基于三方编译器实现,与标准`javac`编译过程不兼容会导致在IDE中运行测试用例时`@EnablePrivateAccess`注解无效。不过,通过`PrivateAccessor`工具类访问被测类私有成员的功能不会受编译器差异影响。
若项目中使用了`@EnablePrivateAccess`注解,可在`Eclipse`的命令行中使用`mvn test -Dtest=<测试类名>``mvn test -Dtest=<测试类名>#<测试用例名>`来运行单个测试类或测试用例。
同时,由于`Eclipse`内置的单元测试执行器完全忽略`pom.xml`文件的配置因此若需使用Mock功能需进行额外配置。
以使用`JUnit`为例方法为从IDE工具栏的运行按钮旁边的小三角处下拉选择"Run Configurations...",左侧选择要运行单元测试的任务,在右侧切换到"arguments"标签页,在"VM Options"里添加`-javaagent:`参数,下图为示例,注意应修改`testable-agent`包为与实际情况匹配的本地Maven仓库路径。
![eclipse-junit-configuration](https://testable-code.oss-cn-beijing.aliyuncs.com/eclipse-junit-configuration.png)

View File

@@ -19,18 +19,26 @@
</plugin>
```
> 当使用`testable-maven-plugin`插件时,应该移除`maven-surefire-plugin`插件上的TestableMock相关配置,同时可以从项目的`pom.xml`文件中移除对`com.alibaba.testable:testable-agent`包的显示依赖
**注意:**当使用`testable-maven-plugin`插件时,应该移除`maven-surefire-plugin`插件上的TestableMock相关配置
`testable-maven-plugin`插件能够与Jacoco插件直接同时使用无需额外适配因此能使`pom.xml`文件编写起来更简单且美观。
但是当通过IDE运行单个测试用例时Mock功能会失效
> 还有一种特殊情况,当`jacoco`插件是通过`maven`命令行参数引入的时候,若要使用`TestableMock`功能,则也必须通过命令行参数引入`testable-maven-plugin`插件。详见[issue-14](https://github.com/alibaba/testable-mock/issues/14)
这是由于IDE运行单个测试用例时通常都只会运行`maven-surefire-plugin`插件,跳过了`testable-maven-plugin`插件执行导致Mock功能所需的JavaAgent没有随测试注入
但需要注意的是,使用`testable-maven-plugin`插件后通过IntelliJ IDE运行单个测试用例时Mock功能会失效
该问题可以通过额外配置IDE的测试参数绕过。以IntelliJ为例在单元测试配置的"虚拟机参数VM Option"属性值末尾添加JavaAgent启动参数`-javaagent:${HOME}/.m2/repository/com/alibaba/testable/testable-agent/x.y.z/testable-agent-x.y.z.jar`
这是由于IntelliJ IDE运行单个测试用例时只会运行`maven-surefire-plugin`插件,跳过了`testable-maven-plugin`插件执行导致Mock功能所需的JavaAgent未随测试注入。
该问题可以通过额外配置IDE的测试参数绕过。以IntelliJ为例打开运行菜单的"编辑配置..."选型,如图中位置①
![modify-run-configuration](https://testable-code.oss-cn-beijing.aliyuncs.com/modify-run-configuration.png)
在"虚拟机参数"属性值末尾添加JavaAgent启动参数`-javaagent:${HOME}/.m2/repository/com/alibaba/testable/testable-agent/x.y.z/testable-agent-x.y.z.jar`,如图中位置②
> PS请将路径中的`x.y.z`替换成实际使用的版本号
![idea-vm-option](https://testable-code.oss-cn-beijing.aliyuncs.com/idea-vm-option.png)
![add-testable-javaagent](https://testable-code.oss-cn-beijing.aliyuncs.com/add-testable-javaagent.png)
这样实际上还是该了`maven-surefire-plugin`插件的配置,因此目前除了需要考虑美观因素的场景以外,直接在`pom.xml`文件中修改`maven-surefire-plugin`插件配置依然是更加实用的方案。
最后点击运行单元测试,如图中位置③
总体来说,使用`testable-maven-plugin`带来的额外复杂性依然高于其对配置的简化作用。目前而言,直接在`pom.xml`文件中修改`maven-surefire-plugin`插件配置还是相对推荐的实用方案。

View File

@@ -3,82 +3,98 @@
相比以往Mock工具以类为粒度的Mock方式`TestableMock`允许用户直接定义需要Mock的单个方法并遵循约定优于配置的原则按照规则自动在测试运行时替换被测方法中的指定方法调用。
> 归纳起来就两条:
> - Mock非构造方法拷贝原方法定义到测试类加`@MockMethod`注解
> - Mock构造方法拷贝原方法定义到测试类返回值换成构造的类型方法名随意加`@MockContructor`注解
具体的Mock方法定义约定如下
#### 1. 覆写任意类的方法调用
在测试类里定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数
在测试类里定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,并在注解的`targetClass`参数指定该方法原本所属对象类型。
此时被测类中所有对该需覆写方法的调用将在单元测试运行时将自动被替换为对上述自定义Mock方法的调用。
**注意**:当遇到待覆写方法有重名时,可以将需覆写的方法名写到`@MockMethod`注解的`targetMethod`参数里这样Mock方法自身就可以随意命名了。
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在测试类定义如下方法:
```java
// 原方法签名为`String substring(int, int)`
// 调用此方法的对象`"anything"`类型为`String`
// 则Mock方法签名在其参数列表首位增加一个类型为`String`的参数(名字随意)
// 此参数可用于获得当时的实际调用者的值和上下文
@MockMethod
private String substring(String self, int i, int j) {
@MockMethod(targetClass = String.class)
private String substring(int i, int j) {
return "sub_string";
}
```
当遇到待覆写方法有重名时,可以将需覆写的方法名写到`@MockMethod`注解的`targetMethod`参数里这样Mock方法自身就可以随意命名了。
下面这个例子展示了`targetMethod`参数的用法,其效果与上述示例相同:
```java
// 使用`targetMethod`指定需Mock的方法名
// 此方法本身现在可以随意命名,但方法参数依然需要遵循相同的匹配规则
@MockMethod(targetMethod = "substring")
private String use_any_mock_method_name(String self, int i, int j) {
@MockMethod(targetClass = String.class, targetMethod = "substring")
private String use_any_mock_method_name(int i, int j) {
return "sub_string";
}
```
有时在Mock方法里会需要访问发起调用的原始对象中的成员变量或是调用原始对象的其他方法。此时可以将`@MockMethod`注解中的`targetClass`参数去除,然后在方法参数列表首位增加一个类型为该方法原本所属对象类型的参数。
`TestableMock`约定,当`@MockMethod`注解的`targetClass`参数值为空时Mock方法的首位参数即为目标方法所属类型参数名称随意。通常为了便于代码阅读建议将此参数统一命名为`self``src`。举例如下:
```java
// Mock方法在参数列表首位增加一个类型为`String`的参数(名字随意)
// 此参数可用于获得当时的实际调用者的值和上下文
@MockMethod
private String substring(String self, int i, int j) {
// 可以直接调用原方法此时Mock方法仅用于记录调用常见于对void方法的测试
return self.substring(i, j);
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_common_method()`测试用例。(由于Kotlin对String类型进行了魔改故Kotlin示例中将被测方法在`BlackBox`类里加了一层封装)
#### 2. 覆写被测类自身的成员方法
有时候在对某些方法进行测试时希望将被测类自身的另外一些成员方法Mock掉。
有时候在对某些方法进行测试时希望将被测类自身的另外一些成员方法Mock掉(比如这个方法里有许多外部依赖或耗时操作)
操作方法与前一种情况相同,Mock方法的第一个参数类型需与被测类相同,即可实现对被测类自身(不论是公有或私有)成员方法的覆写。
法与前一种情况完全相同,只需将`targetClass`参数赋值为被测类,即可实现对被测类自身(不论是公有或私有)成员方法的覆写。
例如,被测类中有一个签名为`String innerFunc(String)`的私有方法,我们希望在测试的时候将它替换掉,则只需在测试类定义如下方法:
```java
// 被测类型是`DemoMock`
// 因此在定义Mock方法时在目标方法参数首位加一个类型为`DemoMock`的参数(名字随意)
@MockMethod
private String innerFunc(DemoMock self, String text) {
@MockMethod(targetClass = DemoMock.class)
private String innerFunc(String text) {
return "mock_" + text;
}
```
同样的,上述示例中的方法如需访问发起调用的原始被测对象,也可不使用`targetClass`参数而是在定义Mock方法时在方法参数列表首位加一个类型为`DemoMock`的参数(名字随意)。
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_member_method()`测试用例。
#### 3. 覆写任意类的静态方法
对于静态方法的Mock与普通方法相同。但需要注意的是静态方法的Mock方法被调用时传入的第一个参数实际值始终是`null`
对于静态方法的Mock与普通方法相同。
例如,在被测类中调用了`BlackBox`类型中的静态方法`secretBox()`方法签名为`BlackBox secretBox()`则Mock方法如下
例如,在被测类中调用了`BlackBox`类型中的静态方法`secretBox()`方法签名为`BlackBox secretBox()`则Mock方法如下
```java
// 目标静态方法定义在`BlackBox`类型中
// 在定义Mock方法时在目标方法参数首位加一个类型为`BlackBox`的参数(名字随意)
// 此参数仅用于标识目标类型,实际传入值将始终为`null`
@MockMethod
private BlackBox secretBox(BlackBox ignore) {
@MockMethod(targetClass = BlackBox.class)
private BlackBox secretBox() {
return new BlackBox("not_secret_box");
}
```
对于静态方法的Mock通常不使用方法参数列表的首位加参数来表示目标类型。但这种方法也依然适用只是实际传入的第一个参数值将始终是`null`
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_static_method()`测试用例。
#### 4. 覆写任意类的new操作
在测试类里定义一个`@MockContructor`注解的普通方法,使该方法返回值类型为要被创建的对象类型且方法参数与要Mock的构造函数参数完全一致方法名称随意。
在测试类里定义一个返回值类型为要被创建的对象类型且方法参数与要Mock的构造函数参数完全一致方法名称随意,然后加上`@MockContructor`注解
此时被测类中所有用`new`创建指定类的操作并使用了与Mock方法参数一致的构造函数将被替换为对该自定义方法的调用。
@@ -86,20 +102,53 @@ private BlackBox secretBox(BlackBox ignore) {
```java
// 要覆写的构造函数签名为`BlackBox(String)`
// 无需在Mock方法参数列表增加额外参数Mock方法的名称随意起
// Mock方法返回`BlackBox`类型对象,方法的名称随意起
@MockContructor
private BlackBox createBlackBox(String text) {
return new BlackBox("mock_" + text);
}
```
> 也可以依然使用`@MockMethod`注解,并配置`targetMethod`参数值为"`<init>`",其余同上。效果与使用`@MockContructor`注解相同
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_new_object()`测试用例。
#### 5. 识别当前测试用例和调用来源
在Mock方法中可以通过`TestableTool.TEST_CASE``TestableTool.SOURCE_METHOD`来识别**当前运行的测试用例名称**和**进入该Mock方法前的被测类方法名称**,从而区分处理不同的调用场景。
在Mock方法中通过`TestableTool.SOURCE_METHOD`变量可以识别**进入该Mock方法前的被测类方法名称**;此外,还可以借助`TestableTool.MOCK_CONTEXT`变量为Mock方法注入“**额外的上下文参数**”,从而区分处理不同的调用场景。
例如在测试用例中验证当被Mock方法返回不同结果时对被测目标方法的影响
```java
@Test
public void testDemo() {
MOCK_CONTEXT.put("case", "data-ready");
assertEquals(true, demo());
MOCK_CONTEXT.put("case", "has-error");
assertEquals(false, demo());
MOCK_CONTEXT.clear();
}
```
在Mock方法中取出注入的参数根据情况返回不同结果
```java
@MockMethod
private Data mockDemo() {
switch((String)MOCK_CONTEXT.get("case")) {
case "data-ready":
return new Data();
case "has-error":
throw new NetworkException();
default:
return null;
}
}
```
注意,由于`TestableMock`并不依赖(也不希望依赖)任何特定测试框架,因而无法自动识别单个测试用例的结束位置,这使得设置到`TestableTool.MOCK_CONTEXT`变量的参数可能会在同测试类中跨测试用例存在。建议总是在使用后及时使用`MOCK_CONTEXT.clear()`清空上下文也可将这行语句添加到单元测试框架特定的测试用例结束的统一位置比如JUnit 5的`@AfterEach`方法。
在当前版本中,此变量在运行期的效果类似于一个在测试类中的普通`Map`类型成员对象但请尽量使用此变量而非自定义对象传递附加的Mock参数以便在将来升级至`v0.5`版本时获得更好的兼容性。
> `TestableTool.MOCK_CONTEXT`变量的值是在测试类内共享的,当单元测试并行运行时,建议请选择`parallel`类型为`classes`
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_get_source_method_name()``should_able_to_get_test_case_name()`测试用例。
@@ -108,3 +157,13 @@ private BlackBox createBlackBox(String text) {
在测试用例中可用通过`TestableTool.verify()`方法,配合`with()``withInOrder()``without()``withTimes()`等方法实现对Mock调用情况的验证。
详见[校验Mock调用](zh-cn/doc/matcher.md)文档。
#### 特别说明
> **0.4.x 版本的Mock约定**
> - 测试类与被测类的包路径应相同,且名称为`被测类名+Test`(通常采用`Maven`或`Gradle`构建的Java项目均符合这种惯例
> - Mock方法即包含`@MockMethod`或`@MockContructor`注解的方法)会在运行期被自动修改为`static`方法请勿在Mock方法的定义中访问任何非静态成员。
>
> 这两项约束会在`0.5`版本中去除
>
> 当Mock方法内容较复杂包含Lambda语句、构造块、匿名类等编译器会在构建期生成额外的非静态临时方法导致"Bad type in operand stack"错误。如果有遇到此类错误请将Mock方法显式加上`static`修饰即可解决。这个问题会在`0.5`版本中彻底解决。

View File

@@ -1,2 +1,5 @@
* [首页](/zh-cn/)
* [问题和反馈](https://github.com/alibaba/testable-mock/issues)
* 语言
* [English](/en-us/)
* 中文

View File

@@ -1,15 +1,19 @@
- 快速上手
- [使用TestableMock](zh-cn/doc/setup.md)
- [直接访问私有成员](zh-cn/doc/private-accessor.md)
- [快速Mock任意方法](zh-cn/doc/use-mock.md)
- [直接访问私有成员](zh-cn/doc/private-accessor.md)
- [测试无返回值的方法](zh-cn/doc/test-void-method.md)
- [快速构造复杂入参](zh-cn/doc/parameter-constructor.md)
- 使用参考
- 使用指南
- [校验Mock调用](zh-cn/doc/invoke-matcher.md)
- [常见使用问题](zh-cn/doc/frequency-asked-questions.md)
- [常见使用问题](zh-cn/doc/frequently-asked-questions.md)
- [在IDE运行单元测试](zh-cn/doc/use-in-ide.md)
- [自助问题排查](zh-cn/doc/troubleshooting.md)
- [Testable Maven插件](zh-cn/doc/use-maven-plugin.md)
- [使用Maven插件](zh-cn/doc/use-maven-plugin.md)
- 技术参考
- [业界工具对比](zh-cn/doc/comparation.md)
- [Release Note](zh-cn/doc/release-note.md)
- [主流Mock工具对比](zh-cn/doc/comparation.md)
- [版本更新](zh-cn/doc/release-note.md)
- [关于我们](zh-cn/doc/about-us.md)
- [问题反馈](zh-cn/doc/feedback.md)

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.4.1</version>
<version>0.4.11</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-agent</artifactId>

View File

@@ -1,8 +1,7 @@
package com.alibaba.testable.agent;
import com.alibaba.testable.agent.transformer.TestableClassTransformer;
import com.alibaba.testable.core.util.LogUtil;
import com.alibaba.testable.core.model.MockDiagnose;
import com.alibaba.testable.agent.util.GlobalConfig;
import java.lang.instrument.Instrumentation;
@@ -13,8 +12,10 @@ import java.lang.instrument.Instrumentation;
public class PreMain {
private static final String AND = "&";
private static final String DEBUG = "debug";
private static final String VERBOSE = "verbose";
private static final String LOG_LEVEL = "logLevel";
private static final String DUMP_PATH = "dumpPath";
private static final String PKG_PREFIX = "pkgPrefix";
private static final String EQUAL = "=";
public static void premain(String agentArgs, Instrumentation inst) {
parseArgs(agentArgs);
@@ -26,10 +27,19 @@ public class PreMain {
return;
}
for (String a : args.split(AND)) {
if (a.equals(DEBUG)) {
LogUtil.setDefaultLevel(LogUtil.LEVEL_DIAGNOSE);
} else if (a.equals(VERBOSE)) {
LogUtil.setDefaultLevel(LogUtil.LEVEL_VERBOSE);
int i = a.indexOf(EQUAL);
if (i > 0) {
String k = a.substring(0, i);
String v = a.substring(i + 1);
if (k.equals(LOG_LEVEL)) {
GlobalConfig.setLogLevel(v);
} else if (k.equals(DUMP_PATH)) {
GlobalConfig.setDumpPath(v);
} else if (k.equals(PKG_PREFIX)) {
GlobalConfig.setPkgPrefix(v);
}
} else {
GlobalConfig.setLogLevel(a);
}
}
}

View File

@@ -9,15 +9,20 @@ public class ConstPool {
public static final String SLASH = "/";
public static final String TEST_POSTFIX = "Test";
public static final String TESTABLE_INJECT_REF = "_testableInternalRef";
public static final String FIELD_TARGET_METHOD = "targetMethod";
public static final String FIELD_TARGET_CLASS = "targetClass";
public static final String MOCK_WITH = "com.alibaba.testable.core.annotation.MockWith";
public static final String MOCK_METHOD = "com.alibaba.testable.core.annotation.MockMethod";
public static final String MOCK_CONSTRUCTOR = "com.alibaba.testable.core.annotation.MockConstructor";
public static final String TESTABLE_MOCK = "com.alibaba.testable.core.annotation.TestableMock";
public static final String CGLIB_CLASS_INFIX = "$$EnhancerBy";
public static final String KOTLIN_POSTFIX_COMPANION = "$Companion";
public static final String KOTLIN_PREFIX_ACCESS = "access$";
/**
* Name of the constructor method
*/

View File

@@ -1,17 +1,34 @@
package com.alibaba.testable.agent.handler;
import com.alibaba.testable.core.util.LogUtil;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import java.io.IOException;
import java.util.Iterator;
/**
* @author flin
*/
abstract public class BaseClassHandler implements Opcodes {
protected boolean wasTransformed(ClassNode cn, String refName, String refDescriptor) {
Iterator<FieldNode> iterator = cn.fields.iterator();
if (iterator.hasNext()) {
if (refName.equals(iterator.next().name)) {
// avoid duplicate injection
LogUtil.verbose("Duplicate injection found, ignore " + cn.name);
return true;
}
}
// TODO: `ACC_STATIC` should be removed in v0.5 to shorten the life cycle of this variable
cn.fields.add(new FieldNode(ACC_PRIVATE | ACC_STATIC, refName, refDescriptor, null, null));
return false;
}
public byte[] getBytes(byte[] classFileBuffer) throws IOException {
ClassReader cr = new ClassReader(classFileBuffer);
ClassNode cn = new ClassNode();

View File

@@ -2,13 +2,14 @@ package com.alibaba.testable.agent.handler;
import com.alibaba.testable.agent.constant.ConstPool;
import com.alibaba.testable.agent.model.MethodInfo;
import com.alibaba.testable.agent.model.ModifiedInsnNodes;
import com.alibaba.testable.agent.tool.ImmutablePair;
import com.alibaba.testable.agent.util.BytecodeUtil;
import com.alibaba.testable.agent.util.ClassUtil;
import com.alibaba.testable.core.util.LogUtil;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -18,6 +19,8 @@ import java.util.Set;
*/
public class SourceClassHandler extends BaseClassHandler {
private static final String TESTABLE_MARK_FIELD = "__testable";
private final List<MethodInfo> injectMethods;
private final Set<Integer> invokeOps = new HashSet<Integer>() {{
add(Opcodes.INVOKEVIRTUAL);
@@ -36,13 +39,16 @@ public class SourceClassHandler extends BaseClassHandler {
*/
@Override
protected void transform(ClassNode cn) {
if (wasTransformed(cn, TESTABLE_MARK_FIELD, "I")) {
return;
}
Set<MethodInfo> memberInjectMethods = new HashSet<MethodInfo>();
Set<MethodInfo> newOperatorInjectMethods = new HashSet<MethodInfo>();
for (MethodInfo mi : injectMethods) {
if (mi.getName().equals(ConstPool.CONSTRUCTOR)) {
newOperatorInjectMethods.add(mi);
for (MethodInfo im : injectMethods) {
if (im.getName().equals(ConstPool.CONSTRUCTOR)) {
newOperatorInjectMethods.add(im);
} else {
memberInjectMethods.add(mi);
memberInjectMethods.add(im);
}
}
for (MethodNode m : cn.methods) {
@@ -54,42 +60,68 @@ public class SourceClassHandler extends BaseClassHandler {
Set<MethodInfo> newOperatorInjectMethods) {
LogUtil.diagnose(" Handling method %s", mn.name);
AbstractInsnNode[] instructions = mn.instructions.toArray();
List<MethodInfo> memberInjectMethodList = new ArrayList<MethodInfo>(memberInjectMethods);
if (instructions.length == 0) {
// native method (issue-52)
return;
}
int i = 0;
int maxStackDiff = 0;
do {
if (invokeOps.contains(instructions[i].getOpcode())) {
MethodInsnNode node = (MethodInsnNode)instructions[i];
String memberInjectMethodName = getMemberInjectMethodName(memberInjectMethodList, node);
if (memberInjectMethodName != null) {
// it's a member or static method and an inject method for it exist
int rangeStart = getMemberMethodStart(instructions, i);
if (rangeStart >= 0) {
instructions = replaceMemberCallOps(cn, mn, memberInjectMethodName, instructions,
node.owner, node.getOpcode(), rangeStart, i);
i = rangeStart;
}
} else if (ConstPool.CONSTRUCTOR.equals(node.name)) {
// it's a new operation
if (ConstPool.CONSTRUCTOR.equals(node.name)) {
LogUtil.verbose(" Line %d, constructing \"%s\" as \"%s\"", getLineNum(instructions, i),
node.owner, node.desc);
String newOperatorInjectMethodName = getNewOperatorInjectMethodName(newOperatorInjectMethods, node);
if (newOperatorInjectMethodName != null) {
// and an inject method for it exist
// it's a new operation and an inject method for it exist
int rangeStart = getConstructorStart(instructions, node.owner, i);
if (rangeStart >= 0) {
instructions = replaceNewOps(cn, mn, newOperatorInjectMethodName, instructions, rangeStart, i);
ModifiedInsnNodes modifiedInsnNodes = replaceNewOps(cn, mn, newOperatorInjectMethodName,
instructions, rangeStart, i);
instructions = modifiedInsnNodes.nodes;
maxStackDiff = Math.max(maxStackDiff, modifiedInsnNodes.stackDiff);
i = rangeStart;
}
}
} else {
LogUtil.verbose(" Line %d, invoking \"%s\" as \"%s\"", getLineNum(instructions, i),
node.name, node.desc);
MethodInfo mockMethod = getMemberInjectMethodName(memberInjectMethods, node);
if (mockMethod != null) {
// it's a member or static method and an inject method for it exist
int rangeStart = getMemberMethodStart(instructions, i);
if (rangeStart >= 0) {
ModifiedInsnNodes modifiedInsnNodes = replaceMemberCallOps(cn, mn, mockMethod,
instructions, node.owner, node.getOpcode(), rangeStart, i);
instructions = modifiedInsnNodes.nodes;
maxStackDiff = Math.max(maxStackDiff, modifiedInsnNodes.stackDiff);
i = rangeStart;
} else {
LogUtil.warn("Potential missed mocking at %s:%s", mn.name, getLineNum(instructions, i));
}
}
}
}
i++;
} while (i < instructions.length);
mn.maxStack += maxStackDiff;
}
private String getMemberInjectMethodName(List<MethodInfo> memberInjectMethodList, MethodInsnNode node) {
for (MethodInfo m : memberInjectMethodList) {
/**
* find the mock method fit for specified method node
* @param memberInjectMethods mock methods available
* @param node method node to match for
* @return mock method info
*/
private MethodInfo getMemberInjectMethodName(Set<MethodInfo> memberInjectMethods, MethodInsnNode node) {
for (MethodInfo m : memberInjectMethods) {
String nodeOwner = ClassUtil.fitCompanionClassName(node.owner);
if (m.getClazz().equals(nodeOwner) && m.getName().equals(node.name) && m.getDesc().equals(node.desc)) {
return m.getMockName();
String nodeName = ClassUtil.fitKotlinAccessorName(node.name);
// Kotlin accessor method will append a extra type parameter
String nodeDesc = nodeName.equals(node.name) ? node.desc : ClassUtil.removeFirstParameter(node.desc);
if (m.getClazz().equals(nodeOwner) && m.getName().equals(nodeName) && m.getDesc().equals(nodeDesc)) {
return m;
}
}
return null;
@@ -119,28 +151,12 @@ public class SourceClassHandler extends BaseClassHandler {
}
private int getMemberMethodStart(AbstractInsnNode[] instructions, int rangeEnd) {
int stackLevel = ClassUtil.getParameterTypes(((MethodInsnNode)instructions[rangeEnd]).desc).size();
int stackLevel = getInitialStackLevel((MethodInsnNode)instructions[rangeEnd]);
if (stackLevel < 0) {
return rangeEnd;
}
for (int i = rangeEnd - 1; i >= 0; i--) {
switch (instructions[i].getOpcode()) {
case Opcodes.INVOKESPECIAL:
case Opcodes.INVOKEVIRTUAL:
case Opcodes.INVOKEINTERFACE:
stackLevel += stackEffectOfInvocation(instructions[i]) + 1;
if (((MethodInsnNode)instructions[i]).name.equals(ConstPool.CONSTRUCTOR)) {
// constructor must be INVOKESPECIAL and implicitly pop 1 more stack
stackLevel++;
}
break;
case Opcodes.INVOKESTATIC:
case Opcodes.INVOKEDYNAMIC:
stackLevel += stackEffectOfInvocation(instructions[i]);
break;
case -1:
// reach LineNumberNode or LabelNode
return i + 1;
default:
stackLevel -= BytecodeUtil.stackEffect(instructions[i].getOpcode());
}
stackLevel += getStackLevelChange(instructions[i]);
if (stackLevel < 0) {
return i;
}
@@ -148,26 +164,56 @@ public class SourceClassHandler extends BaseClassHandler {
return -1;
}
private int stackEffectOfInvocation(AbstractInsnNode instruction) {
String desc = ((MethodInsnNode)instruction).desc;
private int getInitialStackLevel(MethodInsnNode instruction) {
int stackLevel = ClassUtil.getParameterTypes((instruction).desc).size();
switch (instruction.getOpcode()) {
case Opcodes.INVOKESPECIAL:
case Opcodes.INVOKEVIRTUAL:
case Opcodes.INVOKEINTERFACE:
return stackLevel;
case Opcodes.INVOKESTATIC:
case Opcodes.INVOKEDYNAMIC:
return stackLevel - 1;
default:
return 0;
}
}
private int getStackLevelChange(AbstractInsnNode instruction) {
switch (instruction.getOpcode()) {
case Opcodes.INVOKESPECIAL:
case Opcodes.INVOKEVIRTUAL:
case Opcodes.INVOKEINTERFACE:
return stackEffectOfInvocation(((MethodInsnNode)instruction).desc) + 1;
case Opcodes.INVOKESTATIC:
return stackEffectOfInvocation(((MethodInsnNode)instruction).desc);
case Opcodes.INVOKEDYNAMIC:
return stackEffectOfInvocation(((InvokeDynamicInsnNode)instruction).desc);
case -1:
// either LabelNode or LineNumberNode
return 0;
default:
return -BytecodeUtil.stackEffect(instruction.getOpcode());
}
}
private int stackEffectOfInvocation(String desc) {
return ClassUtil.getParameterTypes(desc).size() - (ClassUtil.getReturnType(desc).isEmpty() ? 0 : 1);
}
private AbstractInsnNode[] replaceNewOps(ClassNode cn, MethodNode mn, String newOperatorInjectMethodName,
private ModifiedInsnNodes replaceNewOps(ClassNode cn, MethodNode mn, String newOperatorInjectMethodName,
AbstractInsnNode[] instructions, int start, int end) {
LogUtil.diagnose(" Line %d, mock method %s used", getLineNum(instructions, start),
LogUtil.diagnose(" Line %d, mock method \"%s\" used", getLineNum(instructions, start),
newOperatorInjectMethodName);
String classType = ((TypeInsnNode)instructions[start]).desc;
String constructorDesc = ((MethodInsnNode)instructions[end]).desc;
String testClassName = ClassUtil.getTestClassName(cn.name);
mn.instructions.insertBefore(instructions[start], new FieldInsnNode(GETSTATIC, testClassName,
ConstPool.TESTABLE_INJECT_REF, ClassUtil.toByteCodeClassName(testClassName)));
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKESTATIC, testClassName,
newOperatorInjectMethodName, getConstructorInjectDesc(constructorDesc, classType), false));
mn.instructions.remove(instructions[start]);
mn.instructions.remove(instructions[start + 1]);
mn.instructions.remove(instructions[end]);
return mn.instructions.toArray();
return new ModifiedInsnNodes(mn.instructions.toArray(), 0);
}
private int getLineNum(AbstractInsnNode[] instructions, int start) {
@@ -184,36 +230,50 @@ public class SourceClassHandler extends BaseClassHandler {
ClassUtil.toByteCodeClassName(classType);
}
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, String substitutionMethod,
AbstractInsnNode[] instructions, String ownerClass,
int opcode, int start, int end) {
LogUtil.diagnose(" Line %d, mock method %s used", getLineNum(instructions, start), substitutionMethod);
mn.maxStack++;
MethodInsnNode method = (MethodInsnNode)instructions[end];
private ModifiedInsnNodes replaceMemberCallOps(ClassNode cn, MethodNode mn, MethodInfo mockMethod,
AbstractInsnNode[] instructions, String ownerClass,
int opcode, int start, int end) {
LogUtil.diagnose(" Line %d, mock method \"%s\" used", getLineNum(instructions, start),
mockMethod.getMockName());
boolean shouldAppendTypeParameter = !mockMethod.getDesc().equals(mockMethod.getMockDesc());
String testClassName = ClassUtil.getTestClassName(cn.name);
mn.instructions.insertBefore(instructions[start], new FieldInsnNode(GETSTATIC, testClassName,
ConstPool.TESTABLE_INJECT_REF, ClassUtil.toByteCodeClassName(testClassName)));
if (Opcodes.INVOKESTATIC == opcode || isCompanionMethod(ownerClass, opcode)) {
// append a null value if it was a static invoke or in kotlin companion class
mn.instructions.insertBefore(instructions[start], new InsnNode(ACONST_NULL));
if (shouldAppendTypeParameter) {
// append a null value if it was a static invoke or in kotlin companion class
mn.instructions.insertBefore(instructions[start], new InsnNode(ACONST_NULL));
}
if (ClassUtil.isCompanionClassName(ownerClass)) {
// for kotlin companion class, remove the byte code of reference to "companion" static field
mn.instructions.remove(instructions[end - 1]);
}
} else if (!shouldAppendTypeParameter) {
// remove extra ops code of the mocked instance, which was used as first parameter of mock method
ImmutablePair<Integer, Integer> range = findRangeOfInvokerInstance(instructions, start, end);
for (int i = range.left; i <= range.right; i++) {
mn.instructions.remove(instructions[i]);
}
}
// method with @MockMethod will be modified as public access, so INVOKEVIRTUAL is used
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,
substitutionMethod, addFirstParameter(method.desc, ClassUtil.fitCompanionClassName(ownerClass)), false));
// method with @MockMethod will be modified as public static access, so INVOKESTATIC is used
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKESTATIC, testClassName,
mockMethod.getMockName(), mockMethod.getMockDesc(), false));
mn.instructions.remove(instructions[end]);
return mn.instructions.toArray();
return new ModifiedInsnNodes(mn.instructions.toArray(), 1);
}
private ImmutablePair<Integer, Integer> findRangeOfInvokerInstance(AbstractInsnNode[] nodes, int start, int end) {
int accumulatedLevelChange = 0;
int edgeIndex = start;
for (int i = start; i < end; i++) {
accumulatedLevelChange -= getStackLevelChange(nodes[i]);
if (accumulatedLevelChange == 1) {
edgeIndex = i;
}
}
return ImmutablePair.of(start, edgeIndex);
}
private boolean isCompanionMethod(String ownerClass, int opcode) {
return Opcodes.INVOKEVIRTUAL == opcode && ClassUtil.isCompanionClassName(ownerClass);
}
private String addFirstParameter(String desc, String ownerClass) {
return "(" + ClassUtil.toByteCodeClassName(ownerClass) + desc.substring(1);
}
}

View File

@@ -4,9 +4,11 @@ import com.alibaba.testable.agent.constant.ConstPool;
import com.alibaba.testable.agent.tool.ImmutablePair;
import com.alibaba.testable.agent.util.AnnotationUtil;
import com.alibaba.testable.agent.util.ClassUtil;
import com.alibaba.testable.core.util.LogUtil;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.*;
import java.util.ArrayList;
import javax.lang.model.type.NullType;
import java.util.List;
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
@@ -19,16 +21,19 @@ public class TestClassHandler extends BaseClassHandler {
private static final String CLASS_TESTABLE_TOOL = "com/alibaba/testable/core/tool/TestableTool";
private static final String CLASS_TESTABLE_UTIL = "com/alibaba/testable/core/util/TestableUtil";
private static final String CLASS_INVOKE_RECORD_UTIL = "com/alibaba/testable/core/util/InvokeRecordUtil";
private static final String CLASS_MOCK_CONTEXT = "com/alibaba/testable/agent/model/MockContext";
private static final String REF_TESTABLE_CONTEXT = "_testableContextReference";
private static final String FIELD_TEST_CASE = "TEST_CASE";
private static final String FIELD_SOURCE_METHOD = "SOURCE_METHOD";
private static final String FIELD_MOCK_CONTEXT = "MOCK_CONTEXT";
private static final String FIELD_PARAMETERS = "parameters";
private static final String METHOD_CURRENT_TEST_CASE_NAME = "currentTestCaseName";
private static final String METHOD_CURRENT_SOURCE_METHOD_NAME = "currentSourceMethodName";
private static final String METHOD_MARK_TEST_CASE_BEGIN = "markTestCaseBegin";
private static final String METHOD_RECORD_MOCK_INVOKE = "recordMockInvoke";
private static final String SIGNATURE_CURRENT_TEST_CASE_NAME = "(Ljava/lang/Object;)Ljava/lang/String;";
private static final String SIGNATURE_CURRENT_TEST_CASE_NAME = "(Ljava/lang/String;)Ljava/lang/String;";
private static final String SIGNATURE_CURRENT_SOURCE_METHOD_NAME = "()Ljava/lang/String;";
private static final String SIGNATURE_VOID_METHOD_WITHOUT_PARAMETER = "()V";
private static final String SIGNATURE_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;Z)V";
private static final String SIGNATURE_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;ZZ)V";
private static final String SIGNATURE_PARAMETERS = "Ljava/util/Map;";
/**
* Handle bytecode of test class
@@ -36,47 +41,100 @@ public class TestClassHandler extends BaseClassHandler {
*/
@Override
protected void transform(ClassNode cn) {
for (MethodNode m : cn.methods) {
transformMethod(cn, m);
}
cn.fields.add(new FieldNode(ACC_PUBLIC | ACC_STATIC, ConstPool.TESTABLE_INJECT_REF,
ClassUtil.toByteCodeClassName(cn.name), null, null));
}
private void transformMethod(ClassNode cn, MethodNode mn) {
handleAnnotation(cn, mn);
handleInstruction(mn);
}
private void handleAnnotation(ClassNode cn, MethodNode mn) {
List<String> visibleAnnotationNames = new ArrayList<String>();
if (mn.visibleAnnotations == null) {
// let's assume test case should has a annotation, e.g. @Test or whatever
if (wasTransformed(cn, REF_TESTABLE_CONTEXT, ClassUtil.toByteCodeClassName(CLASS_MOCK_CONTEXT))) {
return;
}
for (AnnotationNode n : mn.visibleAnnotations) {
visibleAnnotationNames.add(n.desc);
}
if (visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD)) ||
visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.TESTABLE_MOCK)) ||
visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR))) {
mn.access &= ~ACC_PRIVATE;
mn.access &= ~ACC_PROTECTED;
mn.access |= ACC_PUBLIC;
injectInvokeRecorder(mn);
} else if (couldBeTestMethod(mn)) {
injectTestableRef(cn, mn);
for (MethodNode mn : cn.methods) {
if (mn.name.equals(ConstPool.CONSTRUCTOR)) {
initMockContextReference(cn, mn);
} else {
handleMockMethod(cn, mn);
handleInstruction(cn, mn);
}
}
}
private void handleInstruction(MethodNode mn) {
private void initMockContextReference(ClassNode cn, MethodNode mn) {
InsnList il = new InsnList();
il.add(new TypeInsnNode(NEW, CLASS_MOCK_CONTEXT));
il.add(new InsnNode(DUP));
il.add(new MethodInsnNode(INVOKESPECIAL, CLASS_MOCK_CONTEXT, "<init>", "()V", false));
il.add(new FieldInsnNode(PUTSTATIC, cn.name, REF_TESTABLE_CONTEXT,
ClassUtil.toByteCodeClassName(CLASS_MOCK_CONTEXT)));
mn.instructions.insertBefore(mn.instructions.get(0), il);
mn.maxStack++;
}
private void handleMockMethod(ClassNode cn, MethodNode mn) {
if (isMockMethod(mn)) {
toPublicStatic(cn, mn);
injectInvokeRecorder(mn);
}
}
private void toPublicStatic(ClassNode cn, MethodNode mn) {
mn.access &= ~ACC_PRIVATE;
mn.access &= ~ACC_PROTECTED;
mn.access |= ACC_PUBLIC;
if ((mn.access & ACC_STATIC) == 0) {
mn.access |= ACC_STATIC;
// remove `this` reference
LocalVariableNode thisRef = null;
for (LocalVariableNode vn : mn.localVariables) {
if (vn.index == 0) {
thisRef = vn;
} else {
vn.index--;
}
}
if (thisRef != null) {
mn.localVariables.remove(thisRef);
} else {
LogUtil.error("Fail to find `this` reference in non-static method " + getName(cn, mn));
return;
}
for (AbstractInsnNode in : mn.instructions) {
if (in instanceof IincInsnNode) {
((IincInsnNode)in).var--;
} else if (in instanceof VarInsnNode) {
if (((VarInsnNode)in).var > 0) {
((VarInsnNode)in).var--;
} else if (in.getOpcode() == ALOAD) {
LogUtil.error("Attempt to access non-static member in mock method " + getName(cn, mn));
return;
}
}
}
mn.maxLocals--;
}
}
private String getName(ClassNode cn, MethodNode mn) {
return cn.name + ":" + mn.name;
}
private boolean isMockMethod(MethodNode mn) {
if (mn.visibleAnnotations == null) {
return false;
}
for (AnnotationNode an : mn.visibleAnnotations) {
if (ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD).equals(an.desc) ||
ClassUtil.toByteCodeClassName(ConstPool.TESTABLE_MOCK).equals(an.desc) ||
ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR).equals(an.desc)) {
return true;
}
}
return false;
}
private void handleInstruction(ClassNode cn, MethodNode mn) {
AbstractInsnNode[] instructions = mn.instructions.toArray();
int i = 0;
do {
if (instructions[i].getOpcode() == GETSTATIC) {
FieldInsnNode fieldInsnNode = (FieldInsnNode)instructions[i];
if (isTestableUtilField(fieldInsnNode)) {
instructions = replaceTestableUtilField(mn, instructions, fieldInsnNode.name, i);
instructions = replaceTestableUtilField(cn, mn, instructions, fieldInsnNode.name, i);
}
}
i++;
@@ -85,19 +143,24 @@ public class TestClassHandler extends BaseClassHandler {
private boolean isTestableUtilField(FieldInsnNode fieldInsnNode) {
return fieldInsnNode.owner.equals(CLASS_TESTABLE_TOOL) &&
(fieldInsnNode.name.equals(FIELD_TEST_CASE) || fieldInsnNode.name.equals(FIELD_SOURCE_METHOD));
(fieldInsnNode.name.equals(FIELD_TEST_CASE) || fieldInsnNode.name.equals(FIELD_SOURCE_METHOD) ||
fieldInsnNode.name.equals(FIELD_MOCK_CONTEXT));
}
private AbstractInsnNode[] replaceTestableUtilField(MethodNode mn, AbstractInsnNode[] instructions,
private AbstractInsnNode[] replaceTestableUtilField(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions,
String fieldName, int pos) {
InsnList il = new InsnList();
if (FIELD_TEST_CASE.equals(fieldName)) {
il.add(new VarInsnNode(ALOAD, 0));
il.add(new LdcInsnNode(ClassUtil.toDotSeparatedName(cn.name)));
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_CURRENT_TEST_CASE_NAME,
SIGNATURE_CURRENT_TEST_CASE_NAME, false));
} else if (FIELD_SOURCE_METHOD.equals(fieldName)) {
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_CURRENT_SOURCE_METHOD_NAME,
SIGNATURE_CURRENT_SOURCE_METHOD_NAME, false));
} else if (FIELD_MOCK_CONTEXT.equals(fieldName)) {
il.add(new FieldInsnNode(GETSTATIC, cn.name, REF_TESTABLE_CONTEXT,
ClassUtil.toByteCodeClassName(CLASS_MOCK_CONTEXT)));
il.add(new FieldInsnNode(GETFIELD, CLASS_MOCK_CONTEXT, FIELD_PARAMETERS, SIGNATURE_PARAMETERS));
}
if (il.size() > 0) {
mn.instructions.insert(instructions[pos], il);
@@ -110,8 +173,8 @@ public class TestClassHandler extends BaseClassHandler {
InsnList il = new InsnList();
List<Byte> types = ClassUtil.getParameterTypes(mn.desc);
int size = types.size();
int parameterOffset = 1;
mn.maxStack += 1;
int parameterOffset = 0;
mn.maxStack += 2;
il.add(getIntInsn(size));
il.add(new TypeInsnNode(ANEWARRAY, ClassUtil.CLASS_OBJECT));
for (int i = 0; i < size; i++) {
@@ -132,6 +195,11 @@ public class TestClassHandler extends BaseClassHandler {
} else {
il.add(new InsnNode(ICONST_0));
}
if (isTargetClassInParameter(mn)) {
il.add(new InsnNode(ICONST_1));
} else {
il.add(new InsnNode(ICONST_0));
}
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_INVOKE_RECORD_UTIL, METHOD_RECORD_MOCK_INVOKE,
SIGNATURE_INVOKE_RECORDER_METHOD, false));
mn.instructions.insertBefore(mn.instructions.get(0), il);
@@ -139,18 +207,32 @@ public class TestClassHandler extends BaseClassHandler {
private boolean isMockForConstructor(MethodNode mn) {
for (AnnotationNode an : mn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.MOCK_CONSTRUCTOR)) {
return true;
}
String method = AnnotationUtil.getAnnotationParameter
(an, ConstPool.FIELD_TARGET_METHOD, null, String.class);
if (ConstPool.CONSTRUCTOR.equals(method)) {
String annotationName = toDotSeparateFullClassName(an.desc);
if (ConstPool.MOCK_CONSTRUCTOR.equals(annotationName)) {
return true;
} else if (ConstPool.MOCK_METHOD.equals(annotationName)) {
String method = AnnotationUtil.getAnnotationParameter
(an, ConstPool.FIELD_TARGET_METHOD, null, String.class);
if (ConstPool.CONSTRUCTOR.equals(method)) {
return true;
}
}
}
return false;
}
private boolean isTargetClassInParameter(MethodNode mn) {
for (AnnotationNode an : mn.visibleAnnotations) {
if (ConstPool.MOCK_METHOD.equals(toDotSeparateFullClassName(an.desc))) {
Type type = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_CLASS, null, Type.class);
if (type != null && !type.getClassName().equals(NullType.class.getName())) {
return false;
}
}
}
return true;
}
private static ImmutablePair<Integer, Integer> getLoadParameterByteCode(Byte type) {
switch (type) {
case ClassUtil.TYPE_BYTE:
@@ -189,26 +271,4 @@ public class TestClassHandler extends BaseClassHandler {
}
}
private void injectTestableRef(ClassNode cn, MethodNode mn) {
InsnList il = new InsnList();
// Initialize "_testableInternalRef"
il.add(new VarInsnNode(ALOAD, 0));
il.add(new FieldInsnNode(PUTSTATIC, cn.name, ConstPool.TESTABLE_INJECT_REF,
ClassUtil.toByteCodeClassName(cn.name)));
// Invoke "markTestCaseBegin"
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_MARK_TEST_CASE_BEGIN,
SIGNATURE_VOID_METHOD_WITHOUT_PARAMETER, false));
mn.instructions.insertBefore(mn.instructions.getFirst(), il);
}
/**
* Different unit test framework may have different @Test annotation
* but they should always NOT private, protected or static
* and has neither parameter nor return value
*/
private boolean couldBeTestMethod(MethodNode mn) {
return (mn.access & (ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC)) == 0 &&
mn.desc.equals(SIGNATURE_VOID_METHOD_WITHOUT_PARAMETER);
}
}

View File

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

View File

@@ -13,20 +13,25 @@ public class MethodInfo {
* name of the source method
*/
private final String name;
/**
* parameter and return value of the source method
*/
private final String desc;
/**
* name of the mock method
*/
private final String mockName;
/**
* parameter and return value of the source method
* parameter and return value of the mock method
*/
private final String desc;
private final String mockDesc;
public MethodInfo(String clazz, String name, String mockName, String desc) {
public MethodInfo(String clazz, String name, String desc, String mockName, String mockDesc) {
this.clazz = clazz;
this.name = name;
this.mockName = mockName;
this.desc = desc;
this.mockName = mockName;
this.mockDesc = mockDesc;
}
public String getClazz() {
@@ -37,12 +42,16 @@ public class MethodInfo {
return name;
}
public String getDesc() {
return desc;
}
public String getMockName() {
return mockName;
}
public String getDesc() {
return desc;
public String getMockDesc() {
return mockDesc;
}
@Override
@@ -54,16 +63,18 @@ public class MethodInfo {
if (!clazz.equals(that.clazz)) { return false; }
if (!name.equals(that.name)) { return false; }
if (!desc.equals(that.desc)) { return false; }
if (!mockName.equals(that.mockName)) { return false; }
return desc.equals(that.desc);
return mockDesc.equals(that.mockDesc);
}
@Override
public int hashCode() {
int result = clazz.hashCode();
result = 31 * result + name.hashCode();
result = 31 * result + mockName.hashCode();
result = 31 * result + desc.hashCode();
result = 31 * result + mockName.hashCode();
result = 31 * result + mockDesc.hashCode();
return result;
}
}

View File

@@ -0,0 +1,13 @@
package com.alibaba.testable.agent.model;
import java.util.HashMap;
import java.util.Map;
/**
* @author flin
*/
public class MockContext {
public Map<String, Object> parameters = new HashMap<String, Object>();
}

View File

@@ -0,0 +1,16 @@
package com.alibaba.testable.agent.model;
import org.objectweb.asm.tree.AbstractInsnNode;
public class ModifiedInsnNodes {
public AbstractInsnNode[] nodes;
public int stackDiff;
public ModifiedInsnNodes(AbstractInsnNode[] nodes, int stackDiff) {
this.nodes = nodes;
this.stackDiff = stackDiff;
}
}

View File

@@ -3,24 +3,31 @@ package com.alibaba.testable.agent.transformer;
import com.alibaba.testable.agent.constant.ConstPool;
import com.alibaba.testable.agent.handler.SourceClassHandler;
import com.alibaba.testable.agent.handler.TestClassHandler;
import com.alibaba.testable.agent.model.CachedMockParameter;
import com.alibaba.testable.agent.tool.ImmutablePair;
import com.alibaba.testable.agent.model.MethodInfo;
import com.alibaba.testable.agent.tool.ComparableWeakRef;
import com.alibaba.testable.agent.tool.ImmutablePair;
import com.alibaba.testable.agent.util.AnnotationUtil;
import com.alibaba.testable.agent.util.ClassUtil;
import com.alibaba.testable.core.util.LogUtil;
import com.alibaba.testable.agent.util.GlobalConfig;
import com.alibaba.testable.agent.util.StringUtil;
import com.alibaba.testable.core.model.MockDiagnose;
import com.alibaba.testable.core.util.LogUtil;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import javax.lang.model.type.NullType;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import static com.alibaba.testable.agent.constant.ConstPool.DOT;
import static com.alibaba.testable.agent.constant.ConstPool.SLASH;
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
/**
@@ -29,8 +36,7 @@ import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassNa
public class TestableClassTransformer implements ClassFileTransformer {
private static final String FIELD_DIAGNOSE = "diagnose";
private final Map<ComparableWeakRef<String>, CachedMockParameter> loadedClass =
new WeakHashMap<ComparableWeakRef<String>, CachedMockParameter>();
private static final String COMMA = ",";
/**
* Just avoid spend time to scan those surely non-user classes
@@ -43,9 +49,8 @@ public class TestableClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classFileBuffer) {
if (isSystemClass(className) || loadedClass.containsKey(new ComparableWeakRef<String>(className))) {
if (isSystemClass(className)) {
// Ignore system class and reloaded class
LogUtil.verbose("Ignore class: " + (className == null ? "<lambda>" : className));
return null;
}
LogUtil.verbose("Handle class: " + className);
@@ -53,22 +58,41 @@ public class TestableClassTransformer implements ClassFileTransformer {
try {
if (shouldTransformAsSourceClass(className)) {
// it's a source class with testable enabled
LogUtil.diagnose("Handling source class %s", className);
List<MethodInfo> injectMethods = getTestableMockMethods(ClassUtil.getTestClassName(className));
LogUtil.diagnose("Handling source class %s", className);
bytes = new SourceClassHandler(injectMethods).getBytes(classFileBuffer);
resetMockContext();
dumpByte(className, bytes);
} else if (shouldTransformAsTestClass(className)) {
// it's a test class with testable enabled
LogUtil.diagnose("Handling test class %s", className);
bytes = new TestClassHandler().getBytes(classFileBuffer);
resetMockContext();
dumpByte(className, bytes);
}
} catch (IOException e) {
} catch (Throwable t) {
LogUtil.warn("Failed to transform class " + className);
LogUtil.diagnose(t.toString());
} finally {
LogUtil.resetLogLevel();
}
return bytes;
}
private void dumpByte(String className, byte[] bytes) {
String dumpDir = GlobalConfig.getDumpPath();
if (dumpDir == null || dumpDir.isEmpty() || !new File(dumpDir).isDirectory()) {
return;
}
try {
String dumpFile = StringUtil.joinPath(dumpDir, className.replaceAll(SLASH, DOT) + ".class");
LogUtil.verbose("Dump class: " + dumpFile);
FileOutputStream stream = new FileOutputStream(dumpFile);
stream.write(bytes);
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private boolean shouldTransformAsSourceClass(String className) {
return hasMockAnnotation(ClassUtil.getTestClassName(className));
}
@@ -82,14 +106,25 @@ public class TestableClassTransformer implements ClassFileTransformer {
if (null == className) {
return true;
}
for (String prefix : WHITELIST_PREFIXES) {
if (className.startsWith(prefix)) {
return false;
String whitePrefix = GlobalConfig.getPkgPrefix();
if (whitePrefix != null) {
for (String prefix : whitePrefix.split(COMMA)) {
if (className.startsWith(prefix)) {
// Only consider package in provided list as non-system class
return false;
}
}
}
for (String prefix : BLACKLIST_PREFIXES) {
if (className.startsWith(prefix)) {
return true;
return true;
} else {
for (String prefix : WHITELIST_PREFIXES) {
if (className.startsWith(prefix)) {
return false;
}
}
for (String prefix : BLACKLIST_PREFIXES) {
if (className.startsWith(prefix)) {
return true;
}
}
}
return false;
@@ -111,37 +146,52 @@ public class TestableClassTransformer implements ClassFileTransformer {
}
private void checkMethodAnnotation(ClassNode cn, List<MethodInfo> methodInfos, MethodNode mn) {
ImmutablePair<String, String> methodDescPair = extractFirstParameter(mn.desc);
if (methodDescPair == null || mn.visibleAnnotations == null) {
if (mn.visibleAnnotations == null) {
return;
}
for (AnnotationNode an : mn.visibleAnnotations) {
String fullClassName = toDotSeparateFullClassName(an.desc);
if (fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
addMockConstructor(cn, methodInfos, mn);
LogUtil.verbose(" Mock constructor \"%s\" as \"(%s)V\" for \"%s\"", mn.name,
ClassUtil.extractParameters(mn.desc), ClassUtil.getReturnType(mn.desc));
addMockConstructor(methodInfos, cn, mn);
} else if (fullClassName.equals(ConstPool.MOCK_METHOD) ||
fullClassName.equals(ConstPool.TESTABLE_MOCK)) {
LogUtil.verbose(" Mock method \"%s\" as \"%s\"", mn.name, mn.desc);
String targetMethod = AnnotationUtil.getAnnotationParameter(
an, ConstPool.FIELD_TARGET_METHOD, mn.name, String.class);
if (targetMethod.equals(ConstPool.CONSTRUCTOR)) {
addMockConstructor(cn, methodInfos, mn);
if (ConstPool.CONSTRUCTOR.equals(targetMethod)) {
addMockConstructor(methodInfos, cn, mn);
} else {
addMockMethod(methodInfos, mn, methodDescPair, targetMethod);
MethodInfo mi = getMethodInfo(mn, an, targetMethod);
if (mi != null) {
methodInfos.add(mi);
}
}
break;
}
}
}
private void addMockMethod(List<MethodInfo> methodInfos, MethodNode mn,
ImmutablePair<String, String> methodDescPair, String targetMethod) {
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
methodInfos.add(new MethodInfo(targetClass, targetMethod, mn.name, methodDescPair.right));
private MethodInfo getMethodInfo(MethodNode mn, AnnotationNode an, String targetMethod) {
Type targetType = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_CLASS, null, Type.class);
if (targetType == null || targetType.getClassName().equals(NullType.class.getName())) {
// "targetClass" unset, use first parameter as target class type
ImmutablePair<String, String> methodDescPair = extractFirstParameter(mn.desc);
if (methodDescPair == null) {
return null;
}
return new MethodInfo(methodDescPair.left, targetMethod, methodDescPair.right, mn.name, mn.desc);
} else {
// "targetClass" found, use it as target class type
String slashSeparatedName = ClassUtil.toSlashSeparatedName(targetType.getClassName());
return new MethodInfo(slashSeparatedName, targetMethod, mn.desc, mn.name, mn.desc);
}
}
private void addMockConstructor(ClassNode cn, List<MethodInfo> methodInfos, MethodNode mn) {
private void addMockConstructor(List<MethodInfo> methodInfos, ClassNode cn, MethodNode mn) {
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
methodInfos.add(new MethodInfo(sourceClassName, ConstPool.CONSTRUCTOR, mn.name, mn.desc));
methodInfos.add(new MethodInfo(sourceClassName, ConstPool.CONSTRUCTOR, mn.desc, mn.name, mn.desc));
}
/**
@@ -150,11 +200,6 @@ public class TestableClassTransformer implements ClassFileTransformer {
* @return found annotation or not
*/
private boolean hasMockAnnotation(String className) {
CachedMockParameter cache = loadedClass.get(new ComparableWeakRef<String>(className));
if (cache != null) {
setupMockContext(cache.getMockWith());
return cache.isClassExist();
}
try {
ClassNode cn = new ClassNode();
new ClassReader(className).accept(cn, 0);
@@ -162,7 +207,6 @@ public class TestableClassTransformer implements ClassFileTransformer {
for (AnnotationNode an : cn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.MOCK_WITH)) {
setupMockContext(an);
loadedClass.put(new ComparableWeakRef<String>(className), CachedMockParameter.exist(an));
return true;
}
}
@@ -174,7 +218,6 @@ public class TestableClassTransformer implements ClassFileTransformer {
if (fullClassName.equals(ConstPool.MOCK_METHOD) ||
fullClassName.equals(ConstPool.TESTABLE_MOCK) ||
fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
loadedClass.put(new ComparableWeakRef<String>(className), CachedMockParameter.exist());
return true;
}
}
@@ -184,21 +227,17 @@ public class TestableClassTransformer implements ClassFileTransformer {
// Usually class not found, return without record
return false;
}
loadedClass.put(new ComparableWeakRef<String>(className), CachedMockParameter.notExist());
return false;
}
private void setupMockContext(AnnotationNode an) {
MockDiagnose diagnose = AnnotationUtil.getAnnotationParameter(an, FIELD_DIAGNOSE, null, MockDiagnose.class);
if (diagnose != null) {
LogUtil.enableDiagnose(diagnose == MockDiagnose.ENABLE);
LogUtil.setLevel(diagnose == MockDiagnose.ENABLE ? LogUtil.LogLevel.LEVEL_DIAGNOSE :
(diagnose == MockDiagnose.VERBOSE ? LogUtil.LogLevel.LEVEL_VERBOSE : LogUtil.LogLevel.LEVEL_MUTE));
}
}
private void resetMockContext() {
LogUtil.resetLogLevel();
}
/**
* Split desc to "first parameter" and "desc of rest parameters"
* @param desc method desc
@@ -206,7 +245,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
private ImmutablePair<String, String> extractFirstParameter(String desc) {
// assume first parameter is a class
int pos = desc.indexOf(";");
return pos < 0 ? null : ImmutablePair.of(desc.substring(1, pos + 1), "(" + desc.substring(pos + 1));
return pos < 0 ? null : ImmutablePair.of(desc.substring(2, pos), "(" + desc.substring(pos + 1));
}
}

View File

@@ -29,7 +29,11 @@ public class AnnotationUtil {
Class<? extends Enum> enumClazz = (Class<? extends Enum>)clazz;
return (T)Enum.valueOf(enumClazz, values[1]);
}
return clazz.cast(an.values.get(i + 1));
try {
return clazz.cast(an.values.get(i + 1));
} catch (ClassCastException e) {
return defaultValue;
}
}
}
}

View File

@@ -1,14 +1,12 @@
package com.alibaba.testable.agent.util;
import com.alibaba.testable.agent.constant.ConstPool;
import com.alibaba.testable.agent.tool.ComparableWeakRef;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
@@ -63,7 +61,7 @@ public class ClassUtil {
* @return is companion class or not
*/
public static boolean isCompanionClassName(String name) {
return name.endsWith("$Companion");
return name.endsWith(ConstPool.KOTLIN_POSTFIX_COMPANION);
}
/**
@@ -72,7 +70,18 @@ public class ClassUtil {
* @return original name
*/
public static String fitCompanionClassName(String name) {
return name.replaceAll("\\$Companion$", "");
return isCompanionClassName(name) ?
name.substring(0, name.length() - ConstPool.KOTLIN_POSTFIX_COMPANION.length()) : name;
}
/**
* fit kotlin accessor method name to original name
* @param name a accessor name (which could be a common kotlin method)
* @return original name
*/
public static String fitKotlinAccessorName(String name) {
return name.startsWith(ConstPool.KOTLIN_PREFIX_ACCESS) ?
name.substring(ConstPool.KOTLIN_PREFIX_ACCESS.length()) : name;
}
/**
@@ -81,6 +90,11 @@ public class ClassUtil {
* @return test class name
*/
public static String getTestClassName(String sourceClassName) {
// check the class if generated by CGLIB
int idx = sourceClassName.indexOf(ConstPool.CGLIB_CLASS_INFIX);
if (idx != -1) {
sourceClassName = sourceClassName.substring(0, idx);
}
return sourceClassName + ConstPool.TEST_POSTFIX;
}
@@ -101,17 +115,23 @@ public class ClassUtil {
public static List<Byte> getParameterTypes(String desc) {
List<Byte> parameterTypes = new ArrayList<Byte>();
boolean travelingClass = false;
boolean travelingArray = false;
for (byte b : desc.getBytes()) {
if (travelingClass) {
if (b == CLASS_END) {
travelingClass = false;
travelingArray = false;
}
} else {
if (isPrimaryType(b)) {
parameterTypes.add(b);
// should treat primary array as class (issue-48)
parameterTypes.add(travelingArray ? TYPE_CLASS : b);
travelingArray = false;
} else if (b == TYPE_CLASS) {
travelingClass = true;
parameterTypes.add(b);
} else if (b == TYPE_ARRAY) {
travelingArray = true;
} else if (b == PARAM_END) {
break;
}
@@ -120,6 +140,16 @@ public class ClassUtil {
return parameterTypes;
}
/**
* extract parameter part of method desc
* @param desc method description
* @return parameter value
*/
public static String extractParameters(String desc) {
int returnTypeEdge = desc.lastIndexOf(PARAM_END);
return desc.substring(1, returnTypeEdge);
}
/**
* parse method desc, fetch return value types
* @param desc method description
@@ -195,6 +225,15 @@ public class ClassUtil {
return toSlashSeparatedName(className).substring(1, className.length() - 1);
}
/**
* remove first parameter from method descriptor
* @param desc original descriptor
* @return descriptor without first parameter
*/
public static String removeFirstParameter(String desc) {
return "(" + desc.substring(desc.indexOf(";") + 1);
}
private static String toDescriptor(Byte type, String objectType) {
return "(" + (char)type.byteValue() + ")L" + objectType + ";";
}

View File

@@ -0,0 +1,46 @@
package com.alibaba.testable.agent.util;
import com.alibaba.testable.core.util.LogUtil;
/**
* @author flin
*/
public class GlobalConfig {
private static final String MUTE = "mute";
private static final String DEBUG = "debug";
private static final String VERBOSE = "verbose";
private static String dumpPath = null;
private static String pkgPrefix = null;
public static boolean setLogLevel(String level) {
if (level.equals(MUTE)) {
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_MUTE);
return true;
} else if (level.equals(DEBUG)) {
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_DIAGNOSE);
return true;
} else if (level.equals(VERBOSE)) {
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_VERBOSE);
return true;
}
return false;
}
public static void setDumpPath(String path) {
dumpPath = path;
}
public static String getDumpPath() {
return dumpPath;
}
public static String getPkgPrefix() {
return pkgPrefix;
}
public static void setPkgPrefix(String pkgPrefix) {
GlobalConfig.pkgPrefix = pkgPrefix;
}
}

View File

@@ -1,5 +1,7 @@
package com.alibaba.testable.agent.util;
import java.io.File;
/**
* @author flin
*/
@@ -19,4 +21,14 @@ public class StringUtil {
return sb.toString();
}
/**
* join a path text and a file name to full file path
* @param folder path text
* @param file file name
* @return joined full file path
*/
public static String joinPath(String folder, String file) {
return (folder.endsWith(File.separator) ? folder : (folder + File.separator)) + file;
}
}

View File

@@ -0,0 +1,23 @@
package com.alibaba.testable.agent.transformer;
import com.alibaba.testable.agent.tool.ImmutablePair;
import com.alibaba.testable.core.accessor.PrivateAccessor;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class TestableClassTransformerTest {
private TestableClassTransformer testableClassTransformer = new TestableClassTransformer();
@Test
void should_split_parameters() {
ImmutablePair<String, String> parameters =
PrivateAccessor.invoke(testableClassTransformer, "extractFirstParameter", "()");
assertNull(parameters);
parameters = PrivateAccessor.invoke(testableClassTransformer, "extractFirstParameter", "(Lcom.alibaba.demo.Class;ILjava.lang.String;Z)");
assertNotNull(parameters);
assertEquals("com.alibaba.demo.Class", parameters.left);
assertEquals("(ILjava.lang.String;Z)", parameters.right);
}
}

View File

@@ -15,6 +15,12 @@ class ClassUtilTest {
assertEquals(3, ClassUtil.getParameterTypes("(Ljava/lang/String;[I[Ljava/lang/String;)V").size());
}
@Test
void should_able_to_extract_parameter() {
assertEquals("", ClassUtil.extractParameters("()I"));
assertEquals("Ljava/lang/String;", ClassUtil.extractParameters("(Ljava/lang/String;)I"));
}
@Test
void should_able_to_get_return_type() {
assertEquals("", ClassUtil.getReturnType("(Ljava/lang/String;)V"));

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.4.1</version>
<version>0.4.11</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-all</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.4.1</version>
<version>0.4.11</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-core</artifactId>

View File

@@ -1,5 +1,6 @@
package com.alibaba.testable.core.annotation;
import javax.lang.model.type.NullType;
import java.lang.annotation.*;
/**
@@ -18,4 +19,10 @@ public @interface MockMethod {
*/
String targetMethod() default "";
/**
* explicit set target class instead of adding to parameter list
* @return target class type
*/
Class<?> targetClass() default NullType.class;
}

View File

@@ -14,6 +14,11 @@ public enum MockDiagnose {
/**
* Print diagnose logs
*/
ENABLE
ENABLE,
/**
* Print verbose log
*/
VERBOSE
}

View File

@@ -1,5 +1,7 @@
package com.alibaba.testable.core.tool;
import java.util.Map;
/**
* @author flin
*/
@@ -7,7 +9,9 @@ public class TestableTool {
/**
* Name of current test case method
* @deprecated prefer using `MOCK_CONTEXT` to distinguish test cases
*/
@Deprecated
public static String TEST_CASE;
/**
@@ -15,4 +19,9 @@ public class TestableTool {
*/
public static String SOURCE_METHOD;
/**
* Inject extra mock parameters
*/
public static Map<String, Object> MOCK_CONTEXT;
}

View File

@@ -25,8 +25,9 @@ public class InvokeRecordUtil {
* Record mock method invoke event
* @param args invocation parameters
* @param isConstructor whether mocked method is constructor
* @param isTargetClassInParameter whether use first parameter as target class
*/
public static void recordMockInvoke(Object[] args, boolean isConstructor) {
public static void recordMockInvoke(Object[] args, boolean isConstructor, boolean isTargetClassInParameter) {
StackTraceElement mockMethodTraceElement = Thread.currentThread().getStackTrace()[INDEX_OF_TEST_CLASS];
String mockMethodName = mockMethodTraceElement.getMethodName();
String testClass = mockMethodTraceElement.getClassName();
@@ -37,7 +38,7 @@ public class InvokeRecordUtil {
records.add(args);
LogUtil.verbose(" Mock constructor invoked \"%s\"", identify);
} else {
records.add(slice(args, 1));
records.add(isTargetClassInParameter ? slice(args, 1) : args);
LogUtil.verbose(" Mock method invoked \"%s\"", identify);
}
INVOKE_RECORDS.put(identify, records);

View File

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

View File

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

View File

@@ -15,7 +15,8 @@ public class TypeUtil {
public static Class<?>[] getClassesFromObjects(Object[] parameterObjects) {
Class<?>[] cs = new Class[parameterObjects.length];
for (int i = 0; i < cs.length; i++) {
cs[i] = parameterObjects[i].getClass();
Object pObj = parameterObjects[i];
cs[i] = (pObj == null) ? null : pObj.getClass();
}
return cs;
}
@@ -41,7 +42,7 @@ public class TypeUtil {
/**
* type equals
* @param classesLeft class to be compared
* @param classesRight class to compare
* @param classesRight class to compare (item can be null)
* @return whether all class equals
*/
private static boolean typeEquals(Class<?>[] classesLeft, Class<?>[] classesRight) {
@@ -49,8 +50,10 @@ public class TypeUtil {
return false;
}
for (int i = 0; i < classesLeft.length; i++) {
if (!classesLeft[i].equals(classesRight[i]) &&
!fuzzyEqual(classesLeft[i], classesRight[i])) {
if (classesRight[i] == null) {
return !classesLeft[i].isPrimitive();
}
if (!classesLeft[i].isAssignableFrom(classesRight[i]) && !fuzzyEqual(classesLeft[i], classesRight[i])) {
return false;
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.4.1</version>
<version>0.4.11</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-maven-plugin</artifactId>

View File

@@ -1,5 +1,6 @@
package com.alibaba.testable;
import com.google.common.base.Strings;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
@@ -24,13 +25,33 @@ public class TestableMojo extends AbstractMojo
/**
* Maven project.
*/
@Parameter(property = "project", readonly = true)
@Parameter(property = "project", required = true, readonly = true)
private MavenProject project;
/**
* Map of plugin artifacts.
*/
@Parameter(property = "plugin.artifactMap", required = true, readonly = true)
private Map<String, Artifact> pluginArtifactMap;
/**
* JavaAgent log level (mute/debug/verbose)
*/
@Parameter
private String logLevel;
/**
* Path to dump transformed class file
*/
@Parameter
private String dumpPath;
/**
* Package prefixes of class need to be mocked (comma split)
*/
@Parameter
private String pkgPrefix;
/**
* Name of the Testable Agent artifact.
*/
@@ -57,8 +78,22 @@ public class TestableMojo extends AbstractMojo
getLog().error("failed to fetch project properties");
return;
}
String extraArgs = "";
if (!Strings.isNullOrEmpty(logLevel)) {
extraArgs += "&logLevel=" + logLevel;
}
if (!Strings.isNullOrEmpty(dumpPath)) {
extraArgs += "&dumpPath=" + dumpPath;
}
if (!Strings.isNullOrEmpty(pkgPrefix)) {
extraArgs += "&pkgPrefix=" + pkgPrefix;
}
final String oldArgs = projectProperties.getProperty(testArgsPropertyKey);
final String newArgs = (oldArgs == null) ? getAgentJarArgs().trim() : (oldArgs + getAgentJarArgs());
String newArgs = (oldArgs == null) ? getAgentJarArgs().trim() : (oldArgs + getAgentJarArgs());
if (!extraArgs.isEmpty()) {
newArgs += ("=" + extraArgs.substring(1));
}
getLog().info(testArgsPropertyKey + " set to " + newArgs);
projectProperties.setProperty(testArgsPropertyKey, newArgs);
}
@@ -67,6 +102,7 @@ public class TestableMojo extends AbstractMojo
final Artifact testableAgentArtifact = pluginArtifactMap.get(AGENT_ARTIFACT_NAME);
if (testableAgentArtifact == null) {
getLog().error("failed to find testable agent jar");
return "";
}
return " -javaagent:" + testableAgentArtifact.getFile().getAbsolutePath();
}

View File

@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.4.1</version>
<version>0.4.11</version>
<packaging>pom</packaging>
<name>testable-parent</name>
<description>Unit test enhancement toolkit</description>
@@ -42,7 +42,7 @@
<plugin.gpg.version>1.6</plugin.gpg.version>
<plugin.staging.version>1.6.8</plugin.staging.version>
<plugin.maven.version>3.6.0</plugin.maven.version>
<testable.version>0.4.1</testable.version>
<testable.version>0.4.11</testable.version>
</properties>
<profiles>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.4.1</version>
<version>0.4.11</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-processor</artifactId>

View File

@@ -4,14 +4,16 @@ import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
import com.alibaba.testable.processor.constant.ConstPool;
import com.alibaba.testable.processor.model.TestableContext;
import com.alibaba.testable.processor.translator.EnablePrivateAccessTranslator;
import com.alibaba.testable.processor.util.JavacUtil;
import com.alibaba.testable.processor.util.TestableLogger;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Names;
import com.sun.tools.javac.util.Pair;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
@@ -19,7 +21,6 @@ import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import java.util.Set;
@@ -29,31 +30,25 @@ import java.util.Set;
@SupportedAnnotationTypes("com.alibaba.testable.processor.annotation.EnablePrivateAccess")
public class EnablePrivateAccessProcessor extends AbstractProcessor {
private static final String SRC_CLASS = "srcClass";
private TestableContext cx;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
Context context = getJavacProcessingContext(processingEnv);
if (context == null) {
JavacProcessingEnvironment javacProcessingContext = getJavacProcessingEnvironment(processingEnv);
if (javacProcessingContext == null) {
cx = new TestableContext(new TestableLogger(processingEnv.getMessager()), processingEnv.getFiler());
cx.logger.info("Skip testable compile time processing");
} else {
cx = new TestableContext(new TestableLogger(processingEnv.getMessager()), processingEnv.getFiler(),
processingEnv.getElementUtils(), processingEnv.getTypeUtils(), JavacTrees.instance(processingEnv),
TreeMaker.instance(context), Names.instance(context));
processingEnv.getElementUtils(), processingEnv.getTypeUtils(), JavacTrees.instance(javacProcessingContext),
TreeMaker.instance(javacProcessingContext.getContext()), Names.instance(javacProcessingContext.getContext()));
}
cx.logger.info("Testable processor initialized");
}
private Context getJavacProcessingContext(ProcessingEnvironment processingEnv) {
try {
return ((JavacProcessingEnvironment)processingEnv).getContext();
} catch (Exception e) {
return null;
}
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (cx.names == null) {
@@ -61,8 +56,10 @@ public class EnablePrivateAccessProcessor extends AbstractProcessor {
}
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(EnablePrivateAccess.class);
for (Element element : elements) {
if (element.getKind().isClass() && isTestClass(element.getSimpleName())) {
processClassElement((Symbol.ClassSymbol)element);
if (element.getKind().isClass()) {
Symbol.ClassSymbol testClass = (Symbol.ClassSymbol)element;
String sourceClassName = getSourceClassName(testClass);
processClassElement(testClass, sourceClassName);
}
}
return true;
@@ -74,14 +71,32 @@ public class EnablePrivateAccessProcessor extends AbstractProcessor {
return SourceVersion.values()[SourceVersion.values().length - 1];
}
private boolean isTestClass(Name name) {
return name.toString().endsWith(ConstPool.TEST_POSTFIX);
private String getSourceClassName(Symbol.ClassSymbol testClass) {
for (Attribute.Compound annotation : testClass.getMetadata().getDeclarationAttributes()) {
if (ConstPool.ENABLE_PRIVATE_ACCESS.equals(annotation.type.tsym.toString())) {
for (Pair<Symbol.MethodSymbol, Attribute> p : annotation.values) {
if (SRC_CLASS.equals(p.fst.name.toString())) {
return p.snd.getValue().toString();
}
}
}
}
return null;
}
private void processClassElement(Symbol.ClassSymbol clazz) {
JCTree tree = cx.trees.getTree(clazz);
String pkgName = ((Symbol.PackageSymbol)clazz.owner).fullname.toString();
tree.accept(new EnablePrivateAccessTranslator(pkgName, clazz.getSimpleName().toString(), cx));
private JavacProcessingEnvironment getJavacProcessingEnvironment(ProcessingEnvironment processingEnv) {
try {
return JavacUtil.getJavacProcessingEnvironment(processingEnv);
} catch (Exception e) {
return null;
}
}
private void processClassElement(Symbol.ClassSymbol testClass, String sourceClassName) {
if (cx.trees != null) {
JCTree tree = cx.trees.getTree(testClass);
tree.accept(new EnablePrivateAccessTranslator(cx, testClass, sourceClassName));
}
}
}

View File

@@ -1,5 +1,6 @@
package com.alibaba.testable.processor.annotation;
import javax.lang.model.type.NullType;
import java.lang.annotation.*;
/**
@@ -11,4 +12,11 @@ import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Documented
public @interface EnablePrivateAccess {
/**
* explicit specify the source class to be tested
* @return
*/
Class<?> srcClass() default NullType.class;
}

View File

@@ -5,6 +5,7 @@ package com.alibaba.testable.processor.constant;
*/
public final class ConstPool {
public static final String ENABLE_PRIVATE_ACCESS = "com.alibaba.testable.processor.annotation.EnablePrivateAccess";
public static final String TESTABLE_PRIVATE_ACCESSOR = "com.alibaba.testable.core.accessor.PrivateAccessor";
public static final String TEST_POSTFIX = "Test";

View File

@@ -0,0 +1,21 @@
package com.alibaba.testable.processor.exception;
/**
* @author flin
*/
public class MemberNotExistException extends RuntimeException {
public MemberNotExistException(String type, String className, String target) {
super(String.format("%s \"%s\" not exist in class \"%s\"", type, target, className));
}
public MemberNotExistException(String type, String className, String target, int count) {
super(String.format("%s \"%s\" with %d %s not exist in class \"%s\"",
type, target, count, parameters(count), className));
}
private static String parameters(int count) {
return count > 1 ? "parameters" : "parameter";
}
}

View File

@@ -37,4 +37,32 @@ public class PrivateAccessStatementGenerator extends BaseGenerator {
params.addAll(expr.args);
return cx.treeMaker.Apply(List.<JCExpression>nil(), invoker, params.toList());
}
public JCExpression fetchStaticGetterStatement(JCFieldAccess access) {
JCFieldAccess getter = cx.treeMaker.Select(nameToExpression(ConstPool.TESTABLE_PRIVATE_ACCESSOR),
cx.names.fromString("getStatic"));
JCExpression classField = cx.treeMaker.Select(access.selected, cx.names.fromString("class"));
return cx.treeMaker.Apply(List.<JCExpression>nil(), getter, List.of(classField,
cx.treeMaker.Literal(access.name.toString())));
}
public JCExpression fetchStaticSetterStatement(JCAssign assign) {
JCFieldAccess setter = cx.treeMaker.Select(nameToExpression(ConstPool.TESTABLE_PRIVATE_ACCESSOR),
cx.names.fromString("setStatic"));
JCExpression selected = ((JCFieldAccess)assign.lhs).selected;
JCExpression classField = cx.treeMaker.Select(selected, cx.names.fromString("class"));
return cx.treeMaker.Apply(List.<JCExpression>nil(), setter, List.of(classField,
cx.treeMaker.Literal(((JCFieldAccess)assign.lhs).name.toString()), assign.rhs));
}
public JCExpression fetchStaticInvokeStatement(JCMethodInvocation expr) {
JCFieldAccess invoker = cx.treeMaker.Select(nameToExpression(ConstPool.TESTABLE_PRIVATE_ACCESSOR),
cx.names.fromString("invokeStatic"));
JCExpression selected = ((JCFieldAccess)expr.meth).selected;
JCExpression classField = cx.treeMaker.Select(selected, cx.names.fromString("class"));
ListBuffer<JCExpression> params = ListBuffer.of(classField);
params.add(cx.treeMaker.Literal(((JCFieldAccess)expr.meth).name.toString()));
params.addAll(expr.args);
return cx.treeMaker.Apply(List.<JCExpression>nil(), invoker, params.toList());
}
}

View File

@@ -0,0 +1,30 @@
package com.alibaba.testable.processor.model;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author flin
*/
public class MemberRecord {
/**
* Record private and final fields
*/
public final List<String> privateOrFinalFields = new ArrayList<String>();
/**
* Record non-private fields
*/
public final List<String> nonPrivateNorFinalFields = new ArrayList<String>();
/**
* Record private methods and possible parameter counts (negative number means large or equals)
*/
public final Map<String, List<Integer>> privateMethods = new HashMap<String, List<Integer>>();
/**
* Record non-private methods and possible parameter counts (negative number means large or equals)
*/
public final Map<String, List<Integer>> nonPrivateMethods = new HashMap<String, List<Integer>>();
}

View File

@@ -0,0 +1,20 @@
package com.alibaba.testable.processor.model;
public enum MemberType {
/**
* Private member or final member
*/
PRIVATE_OR_FINAL,
/**
* Static private member or Static final member
*/
STATIC_PRIVATE,
/**
* Non-private member
*/
NON_PRIVATE
}

View File

@@ -1,17 +1,27 @@
package com.alibaba.testable.processor.translator;
import com.alibaba.testable.processor.constant.ConstPool;
import com.alibaba.testable.processor.generator.PrivateAccessStatementGenerator;
import com.alibaba.testable.processor.model.MemberRecord;
import com.alibaba.testable.processor.model.MemberType;
import com.alibaba.testable.processor.model.TestableContext;
import com.alibaba.testable.processor.util.PathUtil;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Name;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.alibaba.testable.processor.constant.ConstPool.TEST_POSTFIX;
/**
* Travel AST
@@ -20,64 +30,60 @@ import java.net.URLClassLoader;
*/
public class EnablePrivateAccessTranslator extends BaseTranslator {
private static final String IDEA_PATHS_SELECTOR = "idea.paths.selector";
private static final String USER_DIR = "user.dir";
private static final String GRADLE_CLASS_FOLDER = "/build/classes/java/main/";
private static final String MAVEN_CLASS_FOLDER = "/target/classes/";
/**
* Name of source class
*/
private final String sourceClassName;
private final Name sourceClassName;
/**
* Fields of source class instance in the test class
*/
private final ListBuffer<Name> sourceClassIns = new ListBuffer<Name>();
/**
* Record private and final fields
* Member information of source class
*/
private final ListBuffer<String> privateOrFinalFields = new ListBuffer<String>();
/**
* Record private methods
*/
private final ListBuffer<String> privateMethods = new ListBuffer<String>();
private final MemberRecord memberRecord = new MemberRecord();
private final PrivateAccessStatementGenerator privateAccessStatementGenerator;
private final PrivateAccessChecker privateAccessChecker;
public EnablePrivateAccessTranslator(String pkgName, String testClassName, TestableContext cx) {
this.sourceClassName = testClassName.substring(0, testClassName.length() - ConstPool.TEST_POSTFIX.length());
public EnablePrivateAccessTranslator(TestableContext cx, Symbol.ClassSymbol clazz, String srcClassName) {
String sourceClassFullName;
if (srcClassName == null) {
String testClassFullName = clazz.fullname.toString();
sourceClassFullName = testClassFullName.substring(0, testClassFullName.length() - TEST_POSTFIX.length());
} else {
sourceClassFullName = srcClassName;
}
String sourceClassShortName = sourceClassFullName.substring(sourceClassFullName.lastIndexOf('.') + 1);
this.privateAccessStatementGenerator = new PrivateAccessStatementGenerator(cx);
this.sourceClassName = cx.names.fromString(sourceClassShortName);
try {
Class<?> cls = null;
String sourceClassFullName = pkgName + "." + sourceClassName;
try {
cls = Class.forName(sourceClassFullName);
} catch (ClassNotFoundException e) {
// fit for gradle build
String path = "file:" + System.getProperty("user.dir") + "/build/classes/java/main/";
cls = new URLClassLoader(new URL[]{new URL(path)}).loadClass(sourceClassFullName);
}
Class<?> cls = getSourceClass(clazz, sourceClassFullName);
if (cls == null) {
System.err.println("Failed to load source class: " + sourceClassFullName);
return;
}
Field[] fields = cls.getDeclaredFields();
for (Field f : fields) {
if (Modifier.isFinal(f.getModifiers()) || Modifier.isPrivate(f.getModifiers())) {
privateOrFinalFields.add(f.getName());
}
}
Method[] methods = cls.getDeclaredMethods();
for (Method m : methods) {
if (Modifier.isPrivate(m.getModifiers())) {
privateMethods.add(m.getName());
}
cx.logger.error("Failed to load source class: " + sourceClassFullName);
} else {
findAllPrivateMembers(cls);
}
} catch (Exception e) {
e.printStackTrace();
}
this.privateAccessChecker = new PrivateAccessChecker(cx, sourceClassShortName, memberRecord);
}
/**
* var = d.privateMethod(args) → var = PrivateAccessor.invoke(d, "privateMethod", args)
*/
@Override
public void visitVarDef(JCVariableDecl jcVariableDecl) {
jcVariableDecl.init = checkAndExchange(jcVariableDecl.init);
super.visitVarDef(jcVariableDecl);
if (jcVariableDecl.vartype.getClass().equals(JCIdent.class) &&
((JCIdent)jcVariableDecl.vartype).name.toString().equals(sourceClassName)) {
if (jcVariableDecl.vartype instanceof JCIdent &&
((JCIdent)jcVariableDecl.vartype).name.equals(sourceClassName)) {
sourceClassIns.add(jcVariableDecl.name);
}
}
@@ -89,10 +95,15 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
@Override
public void visitExec(JCExpressionStatement jcExpressionStatement) {
// visitExec could be an assign statement to a private field
if (jcExpressionStatement.expr.getClass().equals(JCAssign.class) &&
isPrivateField((JCAssign)jcExpressionStatement.expr)) {
jcExpressionStatement.expr = privateAccessStatementGenerator.fetchSetterStatement(
(JCAssign)jcExpressionStatement.expr);
if (jcExpressionStatement.expr instanceof JCAssign) {
MemberType memberType = checkSetterType((JCAssign)jcExpressionStatement.expr);
if (memberType.equals(MemberType.PRIVATE_OR_FINAL)) {
jcExpressionStatement.expr = privateAccessStatementGenerator.fetchSetterStatement(
(JCAssign)jcExpressionStatement.expr);
} else if (memberType.equals(MemberType.STATIC_PRIVATE)) {
jcExpressionStatement.expr = privateAccessStatementGenerator.fetchStaticSetterStatement(
(JCAssign)jcExpressionStatement.expr);
}
}
// visitExec could be an invoke
jcExpressionStatement.expr = checkAndExchange(jcExpressionStatement.expr);
@@ -129,36 +140,137 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
@Override
protected JCExpression checkAndExchange(JCExpression expr) {
// check is accessing a private field of source class
if (expr.getClass().equals(JCFieldAccess.class) &&
isPrivateField((JCFieldAccess)expr)) {
expr = privateAccessStatementGenerator.fetchGetterStatement((JCFieldAccess)expr);
if (expr instanceof JCFieldAccess) {
MemberType memberType = checkGetterType((JCFieldAccess)expr);
if (memberType.equals(MemberType.PRIVATE_OR_FINAL)) {
expr = privateAccessStatementGenerator.fetchGetterStatement((JCFieldAccess)expr);
} else if (memberType.equals(MemberType.STATIC_PRIVATE)) {
expr = privateAccessStatementGenerator.fetchStaticGetterStatement((JCFieldAccess)expr);
}
}
// check is invoking a private method of source class
if (expr.getClass().equals(JCMethodInvocation.class) &&
isPrivateMethod((JCMethodInvocation)expr)) {
expr = privateAccessStatementGenerator.fetchInvokeStatement((JCMethodInvocation)expr);
if (expr instanceof JCMethodInvocation) {
JCMethodInvocation invocation = (JCMethodInvocation)expr;
MemberType memberType = checkInvokeType(invocation);
if (memberType.equals(MemberType.PRIVATE_OR_FINAL)) {
expr = privateAccessStatementGenerator.fetchInvokeStatement(invocation);
} else if (memberType.equals(MemberType.STATIC_PRIVATE)) {
expr = privateAccessStatementGenerator.fetchStaticInvokeStatement(invocation);
}
privateAccessChecker.validate((JCMethodInvocation)expr);
}
// check the casted expression
if (expr instanceof JCTypeCast) {
JCTypeCast typeCast = (JCTypeCast)expr;
typeCast.expr = checkAndExchange(typeCast.expr);
}
return expr;
}
private boolean isPrivateField(JCFieldAccess access) {
return access.selected.getClass().equals(JCIdent.class) &&
sourceClassIns.contains(((JCIdent)access.selected).name) &&
privateOrFinalFields.contains(access.name.toString());
private Class<?> getSourceClass(Symbol.ClassSymbol clazz, String sourceClassFullName)
throws MalformedURLException, ClassNotFoundException {
Class<?> cls;
try {
// maven build goes here
cls = Class.forName(sourceClassFullName);
} catch (ClassNotFoundException e) {
if (System.getProperty(IDEA_PATHS_SELECTOR) != null) {
// fit for intellij build
String sourceFileWrapperString = clazz.sourcefile.toString();
String sourceFilePath = sourceFileWrapperString.substring(
sourceFileWrapperString.lastIndexOf("[") + 1, sourceFileWrapperString.indexOf("]"));
int indexOfSrc = sourceFilePath.lastIndexOf(File.separator + "src" + File.separator);
String basePath = sourceFilePath.substring(0, indexOfSrc);
try {
String targetFolderPath = PathUtil.fitPathString(basePath + MAVEN_CLASS_FOLDER);
cls = loadClass(targetFolderPath, sourceClassFullName);
} catch (ClassNotFoundException e2) {
String buildFolderPath = PathUtil.fitPathString(basePath + GRADLE_CLASS_FOLDER);
cls = loadClass(buildFolderPath, sourceClassFullName);
}
} else {
// fit for gradle build
String path = PathUtil.fitPathString("file:" + System.getProperty(USER_DIR) + GRADLE_CLASS_FOLDER);
cls = loadClass(path, sourceClassFullName);
}
}
return cls;
}
private boolean isPrivateField(JCAssign assign) {
return assign.lhs.getClass().equals(JCFieldAccess.class) &&
((JCFieldAccess)(assign).lhs).selected.getClass().equals(JCIdent.class) &&
sourceClassIns.contains(((JCIdent)((JCFieldAccess)(assign).lhs).selected).name) &&
privateOrFinalFields.contains(((JCFieldAccess)(assign).lhs).name.toString());
private Class<?> loadClass(String targetFolderPath, String sourceClassFullName)
throws ClassNotFoundException, MalformedURLException {
return new URLClassLoader(new URL[] {new URL(targetFolderPath)}).loadClass(sourceClassFullName);
}
private boolean isPrivateMethod(JCMethodInvocation expr) {
return expr.meth.getClass().equals(JCFieldAccess.class) &&
((JCFieldAccess)(expr).meth).selected.getClass().equals(JCIdent.class) &&
sourceClassIns.contains(((JCIdent)((JCFieldAccess)(expr).meth).selected).name) &&
privateMethods.contains(((JCFieldAccess)(expr).meth).name.toString());
private void findAllPrivateMembers(Class<?> cls) {
Field[] fields = cls.getDeclaredFields();
for (Field f : fields) {
if (Modifier.isFinal(f.getModifiers()) || Modifier.isPrivate(f.getModifiers())) {
memberRecord.privateOrFinalFields.add(f.getName());
} else {
memberRecord.nonPrivateNorFinalFields.add(f.getName());
}
}
Method[] methods = cls.getDeclaredMethods();
for (final Method m : methods) {
if (Modifier.isPrivate(m.getModifiers())) {
checkAndAdd(memberRecord.privateMethods, m.getName(), getParameterLength(m));
} else {
checkAndAdd(memberRecord.nonPrivateMethods, m.getName(), getParameterLength(m));
}
}
}
private void checkAndAdd(Map<String, List<Integer>> map, String key, final int value) {
if (map.containsKey(key)) {
map.get(key).add(value);
} else {
map.put(key, new ArrayList<Integer>() {{ add(value); }});
}
}
private int getParameterLength(Method m) {
int length = m.getParameterTypes().length;
if (length == 0) {
return 0;
}
if (m.getParameterTypes()[length - 1].getName().startsWith("[")) {
return -(length - 1);
} else {
return length;
}
}
private MemberType checkGetterType(JCFieldAccess access) {
if (access.selected instanceof JCIdent && memberRecord.privateOrFinalFields.contains(access.name.toString())) {
return checkSourceClassOrIns(((JCIdent)access.selected).name);
}
return MemberType.NON_PRIVATE;
}
private MemberType checkSetterType(JCAssign assign) {
if (assign.lhs instanceof JCFieldAccess && ((JCFieldAccess)(assign).lhs).selected instanceof JCIdent &&
memberRecord.privateOrFinalFields.contains(((JCFieldAccess)(assign).lhs).name.toString())) {
return checkSourceClassOrIns(((JCIdent)((JCFieldAccess)(assign).lhs).selected).name);
}
return MemberType.NON_PRIVATE;
}
private MemberType checkInvokeType(JCMethodInvocation expr) {
if (expr.meth instanceof JCFieldAccess && ((JCFieldAccess)(expr).meth).selected instanceof JCIdent &&
memberRecord.privateMethods.containsKey(((JCFieldAccess)(expr).meth).name.toString())) {
return checkSourceClassOrIns(((JCIdent)((JCFieldAccess)(expr).meth).selected).name);
}
return MemberType.NON_PRIVATE;
}
private MemberType checkSourceClassOrIns(Name name) {
if (sourceClassName.equals(name)) {
return MemberType.STATIC_PRIVATE;
} else if (sourceClassIns.contains(name)) {
return MemberType.PRIVATE_OR_FINAL;
}
return MemberType.NON_PRIVATE;
}
}

View File

@@ -0,0 +1,96 @@
package com.alibaba.testable.processor.translator;
import com.alibaba.testable.processor.exception.MemberNotExistException;
import com.alibaba.testable.processor.model.MemberRecord;
import com.alibaba.testable.processor.model.TestableContext;
import com.sun.tools.javac.tree.JCTree;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* Validate parameter of PrivateAccessor methods to prevent broken by refactor
*
* @author flin
*/
public class PrivateAccessChecker {
private static final String CLASS_NAME_PRIVATE_ACCESSOR = "PrivateAccessor";
private static final String CLASS_NAME_PRIVATE_ACCESSOR_FULL = "com.alibaba.testable.core.accessor.PrivateAccessor";
private static final List<String> FIELD_ACCESS_METHOD = Arrays.asList(new String[]
{ "get", "set", "getStatic", "setStatic" }.clone());
private static final List<String> FIELD_INVOKE_METHOD = Arrays.asList(new String[]
{ "invoke", "invokeStatic" }.clone());
private static final String TYPE_FIELD = "Field";
private static final String TYPE_METHOD = "Method";
private final TestableContext cx;
private final String className;
private final MemberRecord sourceMembers;
public PrivateAccessChecker(TestableContext cx, String className, MemberRecord memberRecord) {
this.cx = cx;
this.className = className;
this.sourceMembers = memberRecord;
}
public void validate(JCTree.JCMethodInvocation invocation) {
if (invocation.meth instanceof JCTree.JCFieldAccess && invocation.args.length() >= 2) {
JCTree.JCFieldAccess fieldAccess = (JCTree.JCFieldAccess)invocation.meth;
if (invocation.args.get(1) instanceof JCTree.JCLiteral && isPrivateAccessor(fieldAccess)) {
Object target = ((JCTree.JCLiteral)invocation.args.get(1)).getValue();
if (target instanceof String) {
String methodName = fieldAccess.name.toString();
if (FIELD_ACCESS_METHOD.contains(methodName)) {
if (sourceMembers.nonPrivateNorFinalFields.contains(target)) {
cx.logger.warn("Field " + className + "::" + target + " is neither private nor final.");
} else if (!sourceMembers.privateOrFinalFields.contains(target)) {
throw new MemberNotExistException(TYPE_FIELD, className, (String)target);
}
} else if (FIELD_INVOKE_METHOD.contains(methodName)) {
int parameterCount = invocation.args.length() - 2;
// Because of override, check private method list first
if (sourceMembers.privateMethods.containsKey(target) &&
checkParameterCount(sourceMembers.privateMethods, (String)target, parameterCount)) {
// Let it go
} else if (sourceMembers.nonPrivateMethods.containsKey(target) &&
checkParameterCount(sourceMembers.nonPrivateMethods, (String)target, parameterCount)) {
cx.logger.warn("Method " + className + "::" + target + " is not private.");
} else {
throw new MemberNotExistException(TYPE_METHOD, className, (String)target, parameterCount);
}
}
}
}
}
}
private boolean isPrivateAccessor(JCTree.JCFieldAccess fieldAccess) {
return isPrivateAccessorWithShortRef(fieldAccess) || isPrivateAccessorWithFullRef(fieldAccess);
}
private boolean isPrivateAccessorWithShortRef(JCTree.JCFieldAccess fieldAccess) {
return fieldAccess.selected instanceof JCTree.JCIdent &&
((JCTree.JCIdent)fieldAccess.selected).name.toString().equals(CLASS_NAME_PRIVATE_ACCESSOR);
}
private boolean isPrivateAccessorWithFullRef(JCTree.JCFieldAccess fieldAccess) {
return fieldAccess.selected instanceof JCTree.JCFieldAccess &&
fieldAccess.selected.toString().equals(CLASS_NAME_PRIVATE_ACCESSOR_FULL);
}
private boolean checkParameterCount(Map<String, List<Integer>> methods, String target, int parameterCount) {
for (Integer expectCount : methods.get(target)) {
if (countMatch(parameterCount, expectCount)) {
return true;
}
}
return false;
}
private boolean countMatch(int parameterCount, Integer expectCount) {
return expectCount == parameterCount || (expectCount < 0 && parameterCount >= -expectCount);
}
}

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