Compare commits

..

157 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
金戟
2dea6f9480 update docs and bump version to 0.4.1 2020-12-12 00:05:53 +08:00
金戟
2bdad3c1f6 separete description of PrivateAccessor from @EnablePrivateAccess 2020-12-11 21:36:30 +08:00
金戟
3f672e45da do deprecate instead of removal 2020-12-11 21:19:16 +08:00
金戟
328c8540a8 use MockMethod and MockConstructor replace TestableMock annotation 2020-12-11 20:29:49 +08:00
金戟
857b5e2295 bump to v0.4.0 2020-12-11 12:22:22 +08:00
金戟
1ec2c48c4b releasing config needed in parent pom 2020-12-10 20:09:27 +08:00
金戟
0ae39d9deb green light the demo package 2020-12-10 20:02:23 +08:00
金戟
dad2ea3a76 update log level when default log level changed 2020-12-10 19:22:22 +08:00
金戟
260fe94a49 fix a classloader filter issue for java 9+ 2020-12-10 19:21:33 +08:00
金戟
dfb1e2c094 revert maven plugin pom change 2020-12-10 16:11:14 +08:00
金戟
c8115f9623 add verbose log when test start 2020-12-10 15:47:55 +08:00
金戟
4def468d85 suggest invoking original method 2020-12-10 09:17:44 +08:00
金戟
efe57feeda maven plugin will not follow the parent version 2020-12-10 08:54:39 +08:00
金戟
5150868e8c move CONSTRUCTOR to TestableConst 2020-12-10 08:45:01 +08:00
金戟
757becc08b update doc about generics type method and add kotlin demo 2020-12-09 23:37:26 +08:00
金戟
857cbac81c add demo for template method mocking 2020-12-09 20:03:19 +08:00
金戟
495c08f716 extra common configure to parent pom 2020-12-08 16:22:14 +08:00
金戟
da650eb14d add testable all module 2020-12-08 15:51:51 +08:00
金戟
bd0ec5f4b7 reduce log reset frequency 2020-12-07 23:26:18 +08:00
金戟
65807eece7 avoid test case naming conflict between classes 2020-12-05 10:16:22 +08:00
金戟
b74b21cae6 set to snapshot 2020-12-05 09:39:20 +08:00
金戟
ad63de628d refactor log methods 2020-12-05 09:29:14 +08:00
金戟
08aa73b4bd use global log level 2020-12-04 22:50:02 +08:00
金戟
dd7499f3cf bump version to 0.3.2 2020-12-01 23:54:04 +08:00
金戟
1d98e3c618 support invoke private companion object method in kotlin 2020-12-01 23:49:48 +08:00
金戟
001001bfd3 fully support gradle build and test 2020-12-01 20:20:44 +08:00
金戟
472f90e7e4 add static access methods to private accessor 2020-11-30 22:57:37 +08:00
金戟
1af6b8c8b6 fit private access for gradle build 2020-11-30 21:32:13 +08:00
金戟
1bf6ab3b21 fit test case name fetcher for gradle build 2020-11-29 10:39:55 +08:00
金戟
9ab737ce57 add a gradle demo 2020-11-28 22:51:09 +08:00
金戟
35dbc1f497 also look current thread for test case name 2020-11-28 22:42:25 +08:00
金戟
92f9c27561 dump to snapshot version 2020-11-28 20:36:20 +08:00
金戟
7b273affe3 testable-agent dependence is required in order to have agent jar downloaded 2020-11-28 19:52:20 +08:00
金戟
b4d6a8357e remove dependence on internal class 2020-11-28 19:50:00 +08:00
金戟
ff29172a93 moe descriptions and more faq 2020-11-28 17:03:39 +08:00
金戟
d94daf6910 highlight java code 2020-11-26 07:14:08 +08:00
金戟
fb62ab63cc add usage of verify void method 2020-11-26 07:04:41 +08:00
金戟
d75fdcd4e9 update docs and release 0.3.1 2020-11-24 23:18:12 +08:00
金戟
df9da81f75 use docsify generate doc page 2020-11-22 22:12:51 +08:00
金戟
a8ed46e60d add introduction doc 2020-11-22 10:25:44 +08:00
金戟
47115b22af remove spring framework from demos 2020-11-22 00:46:50 +08:00
金戟
b8b4844c38 add testcase for inherition 2020-11-21 22:53:32 +08:00
金戟
2ae34e0b41 update release note 2020-11-18 11:03:53 +08:00
金戟
401b5f83d7 support enable diagnose log by test class 2020-11-18 10:31:33 +08:00
金戟
5770a702cc add test for times method 2020-11-18 10:29:44 +08:00
金戟
15c7e9c48d add matcher doc, add kotlin matcher test case 2020-11-17 19:47:21 +08:00
金戟
0657c19da3 set to next snapshot version 2020-11-17 19:45:31 +08:00
139 changed files with 5276 additions and 1590 deletions

15
.gitignore vendored
View File

@@ -1,10 +1,10 @@
# maven ignore
target/
*.jar
*.war
*.zip
*.tar
*.tar.gz
# gradle ignore
build/
out/
.gradle/
# eclipse ignore
.settings/
@@ -34,3 +34,8 @@ Thumbs.db
# others
note/
*.jar
*.war
*.zip
*.tar
*.tar.gz

View File

@@ -1,17 +1,38 @@
# TestableMock
基于代码和字节码增强的Java单元测试辅助工具包含以下功能
换种思路写Mock让单元测试更简单。
- 使单元测试能直接调用和访问被测类的私有成员,解决私有方法无法测试的问题
- 使被测类的任意方法调用快速替换为Mock实现"指哪换哪"解决传统Mock工具使用繁琐的问题
无需初始化不挑测试框架甭管要换的是私有方法、静态方法、构造方法还是其他任何类的任何方法也甭管要换的对象是怎么创建的。写好Mock定义加个`@MockMethod`注解,一切统统搞定。
- 文档https://alibaba.github.io/testable-mock/
- 国内文档镜像http://freyrlin.gitee.io/testable-mock/
阅读[这里](https://mp.weixin.qq.com/s/KyU6Eu7mDkZU8FspfSqfMw)了解更多故事。
> 特别说明
> 1. 如遇到"Attempt to access 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` 功能稳定,一个崭新的开始
## 目录结构
```bash
|-- testable-core 核心组件,提供注解和工具类
|-- testable-processor ➜ 编译期代码预处理组件,提供测试辅助功能
|-- testable-agent ➜ JavaAgent组件提供Mock测试相关功能
|-- testable-maven-plugin ➜ Maven插件组件用于简化JavaAgent注入
|-- testable-parent提供各子模块的公共父pom文件
|-- testable-all ➜ 依赖聚合,便于一次性引用所有子模块功能
|-- testable-processor ➜ 编译期代码预处理模块,提供测试辅助功能
|-- testable-agent ➜ JavaAgent模块提供Mock测试相关功能
|-- testable-core ➜ 基础功能模块提供Mock相关注解和工具类
|-- testable-maven-plugin ➜ Maven插件模块用于简化JavaAgent注入
|-- demo
| |-- java-demo ➜ Java语言的示例代码
| `-- kotlin-demo ➜ Kotlin语言的示例代码
@@ -25,3 +46,12 @@
```bash
mvn clean install
```
## 本地生成文档
```bash
docsify serve docs
```
> 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

@@ -0,0 +1,27 @@
plugins {
id 'java'
}
group = 'com.alibaba.testable'
version = '1.0.0-SNAPSHOT'
sourceCompatibility = '8'
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
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}"
useJUnit()
}

View File

@@ -1,105 +1,84 @@
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>java-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>java-demo</name>
<description>Demo project for Spring Boot</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>java-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>java-demo</name>
<description>Demo project for TestableMock</description>
<properties>
<java.version>1.8</java.version>
<testable.version>0.3.0</testable.version>
</properties>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.13.1</junit.version>
<testable.version>0.4.11</testable.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependencies>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-processor</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
<build>
<plugins>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
<exclusion>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</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>
</plugins>
</build>
<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>
</plugins>
</build>
</project>

View File

@@ -1,12 +0,0 @@
package com.alibaba.testable.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -0,0 +1,62 @@
package com.alibaba.testable.demo;
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
*/
public class DemoInherit {
/**
* call method overridden by sub class via parent class variable
*/
public Box putIntoBox() {
Box box = new BlackBox("");
box.put("data");
return box;
}
/**
* call method overridden by sub class via sub class variable
*/
public BlackBox putIntoBlackBox() {
BlackBox box = new BlackBox("");
box.put("data");
return box;
}
/**
* call method defined in parent class via parent class variable
*/
public String getFromBox() {
Box box = new BlackBox("data");
return box.get();
}
/**
* call method defined in parent class via sub class variable
*/
public String getFromBlackBox() {
BlackBox box = new BlackBox("data");
return box.get();
}
/**
* call method defined in interface via interface variable
*/
public String getColorViaColor() {
Color color = new BlackBox("");
return color.getColor();
}
/**
* call method defined in interface via sub class variable
*/
public String getColorViaBox() {
BlackBox box = new BlackBox("");
return box.getColor();
}
}

View File

@@ -1,8 +1,7 @@
package com.alibaba.testable.demo.service;
package com.alibaba.testable.demo;
import com.alibaba.testable.demo.model.BlackBox;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
@@ -10,10 +9,10 @@ import java.util.HashSet;
import java.util.List;
/**
* @author flin
* 演示Mock方法调用校验器
* Demonstrate mock method invocation verifier
*/
@Service
public class DemoMatcherService {
public class DemoMatcher {
/**
* Method to be mocked
@@ -46,9 +45,9 @@ public class DemoMatcherService {
List<Float> floatList = new ArrayList<>();
floatList.add(1.0F);
floatList.add(2.0F);
Long[] longArray = new Long[]{1L, 2L};
methodToBeMocked(1, 2);
methodToBeMocked(1L, 2.0);
Long[] longArray = new Long[]{1L, 2L};
methodToBeMocked(new ArrayList<Integer>(){{ add(1); }}, new HashSet<Float>(){{ add(1.0F); }});
methodToBeMocked(1.0, new HashMap<Integer, Float>(2){{ put(1, 1.0F); }});
methodToBeMocked(floatList, floatList);

View File

@@ -1,14 +1,16 @@
package com.alibaba.testable.demo.service;
package com.alibaba.testable.demo;
import com.alibaba.testable.demo.model.BlackBox;
import com.alibaba.testable.demo.model.Box;
import org.springframework.stereotype.Service;
import sun.net.www.http.HttpClient;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Collectors;
@Service
public class DemoMockService {
/**
* 演示基本的Mock功能
* Demonstrate basic mock functionality
*/
public class DemoMock {
/**
* method with new operation
@@ -22,7 +24,7 @@ public class DemoMockService {
* method with member method invoke
*/
public String outerFunc(String s) throws Exception {
return "{ \"res\": \"" + innerFunc(s) + "\"}";
return "{ \"res\": \"" + innerFunc(s) + staticFunc() + "\"}";
}
/**
@@ -39,15 +41,6 @@ public class DemoMockService {
return BlackBox.secretBox();
}
/**
* method with override method invoke
*/
public Box putBox() {
Box box = new BlackBox("");
box.put("data");
return box;
}
/**
* two methods invoke same private method
*/
@@ -59,8 +52,12 @@ public class DemoMockService {
return callFromDifferentMethod();
}
private static String staticFunc() {
return "_STATIC_TAIL";
}
private String innerFunc(String s) throws Exception {
return HttpClient.New(new URL("http:/xxx/" + s)).getURLFile();
return Files.readAllLines(Paths.get("/a-not-exist-file")).stream().collect(Collectors.joining());
}
private String callFromDifferentMethod() {

View File

@@ -0,0 +1,52 @@
package com.alibaba.testable.demo;
import java.util.List;
/**
* 演示私有成员访问功能
* Demonstrate private member access functionality
*/
public class DemoPrivateAccess {
/**
* a private static field
*/
private static int staticCount;
/**
* a private member field
*/
private int count;
/**
* a constant field
*/
public final Double pi = 3.14;
/**
* private static method without arguments
*/
private static String privateStaticFunc() {
return "static";
}
/**
* private static method with arguments
*/
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

@@ -0,0 +1,39 @@
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) {
List<T> l = new ArrayList<>();
l.add(value);
return l;
}
private <K, V> Map<K, V> getMap(K key, V value) {
Map<K, V> m = new HashMap<>();
m.put(key, value);
return m;
}
public String singleTemplateMethod() {
List<String> list = getList("demo");
return list.get(0);
}
public String doubleTemplateMethod() {
Map<String, String> map = getMap("hello", "testable");
return map.get("hello");
}
public Set<?> newTemplateMethod() {
Set<String> set = new HashSet<>();
set.add("world");
return set;
}
}

View File

@@ -1,24 +1,23 @@
package com.alibaba.testable.demo.model;
public class BlackBox implements Box {
private String data;
@Override
public void put(String something) {
data = something;
}
public class BlackBox extends Box implements Color {
public BlackBox(String data) {
this.data = data;
}
public String get() {
return data;
}
public static BlackBox secretBox() {
return new BlackBox("secret");
}
@Override
public void put(String something) {
data = something;
}
@Override
public String getColor() {
return "black";
}
}

View File

@@ -1,7 +1,13 @@
package com.alibaba.testable.demo.model;
public interface Box {
abstract public class Box {
void put(String something);
protected String data;
abstract public void put(String something);
public String get() {
return data;
}
}

View File

@@ -0,0 +1,7 @@
package com.alibaba.testable.demo.model;
public interface Color {
String getColor();
}

View File

@@ -1,27 +0,0 @@
package com.alibaba.testable.demo.service;
import org.springframework.stereotype.Service;
@Service
public class DemoPrivateAccessService {
private int count;
public final Double pi = 3.14;
/**
* private method
*/
private String privateFunc(String s, int i) {
return s + " - " + i;
}
/**
* method with private field access
*/
public String privateFieldAccessFunc() {
count += 2;
return String.valueOf(count);
}
}

View File

@@ -1,13 +0,0 @@
package com.alibaba.testable.demo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DemoApplicationTests {
@Test
void contextLoads() {
}
}

View File

@@ -0,0 +1,93 @@
package com.alibaba.testable.demo;
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.Test;
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
import static org.junit.Assert.assertEquals;
/**
* 演示父类变量引用子类对象时的Mock场景
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
*/
public class DemoInheritTest {
private DemoInherit demoInherit = new DemoInherit();
@MockMethod(targetMethod = "put")
private void put_into_box(Box self, String something) {
self.put("put_" + something + "_into_box");
}
@MockMethod(targetMethod = "put")
private void put_into_blackbox(BlackBox self, String something) {
self.put("put_" + something + "_into_blackbox");
}
@MockMethod(targetMethod = "get")
private String get_from_box(Box self) {
return "get_from_box";
}
@MockMethod(targetMethod = "get")
private String get_from_blackbox(BlackBox self) {
return "get_from_blackbox";
}
@MockMethod(targetMethod = "getColor")
private String get_color_from_color(Color self) {
return "color_from_color";
}
@MockMethod(targetMethod = "getColor")
private String get_color_from_blackbox(BlackBox self) {
return "color_from_blackbox";
}
@Test
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
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
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
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
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
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

@@ -0,0 +1,84 @@
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.Test;
import static com.alibaba.testable.core.matcher.InvokeMatcher.*;
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
import static org.junit.Assert.fail;
/**
* 演示Mock方法调用校验器
* Demonstrate mock method invocation verifier
*/
public class DemoMatcherTest {
private DemoMatcher demoMatcher = new DemoMatcher();
@MockMethod(targetMethod = "methodToBeMocked")
private void methodWithoutArgument(DemoMatcher self) {}
@MockMethod(targetMethod = "methodToBeMocked")
private void methodWithArguments(DemoMatcher self, Object a1, Object a2) {}
@MockMethod(targetMethod = "methodToBeMocked")
private void methodWithArrayArgument(DemoMatcher self, Object[] a) {}
@Test
public void should_match_no_argument() {
demoMatcher.callMethodWithoutArgument();
verify("methodWithoutArgument").withTimes(1);
demoMatcher.callMethodWithoutArgument();
verify("methodWithoutArgument").withTimes(2);
}
@Test
public void should_match_number_arguments() {
demoMatcher.callMethodWithNumberArguments();
verify("methodWithArguments").without(anyString(), 2);
verify("methodWithArguments").withInOrder(anyInt(), 2);
verify("methodWithArguments").withInOrder(anyLong(), anyNumber());
verify("methodWithArguments").with(1.0, anyMapOf(Integer.class, Float.class));
verify("methodWithArguments").with(anyList(), anySetOf(Float.class));
verify("methodWithArguments").with(anyList(), anyListOf(Float.class));
verify("methodWithArrayArgument").with(anyArrayOf(Long.class));
verify("methodWithArrayArgument").with(anyArray());
}
@Test
public void should_match_string_arguments() {
demoMatcher.callMethodWithStringArgument();
verify("methodWithArguments").with(startsWith("he"), endsWith("ld"));
verify("methodWithArguments").with(contains("stab"), matches("m.[cd]k"));
verify("methodWithArrayArgument").with(anyArrayOf(String.class));
}
@Test
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));
verify("methodWithArguments").withInOrder(isNull(), notNull());
}
@Test
public void should_match_with_times() {
demoMatcher.callMethodWithNumberArguments();
verify("methodWithArguments").with(anyNumber(), any()).times(3);
demoMatcher.callMethodWithNumberArguments();
boolean gotError = false;
try {
verify("methodWithArguments").with(anyNumber(), any()).times(4);
} catch (VerifyFailedError e) {
gotError = true;
}
if (!gotError) {
fail();
}
}
}

View File

@@ -0,0 +1,118 @@
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.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 org.junit.Assert.assertEquals;
/**
* 演示基本的Mock功能
* Demonstrate basic mock functionality
*/
public class DemoMockTest {
private DemoMock demoMock = new DemoMock();
@MockConstructor
private BlackBox createBlackBox(String text) {
return new BlackBox("mock_" + text);
}
@MockMethod(targetClass = DemoMock.class)
private String innerFunc(String text) {
return "mock_" + text;
}
@MockMethod(targetClass = DemoMock.class)
private String staticFunc() {
return "_MOCK_TAIL";
}
@MockMethod(targetClass = String.class)
private String trim() {
return "trim_string";
}
@MockMethod(targetClass = String.class, targetMethod = "substring")
private String sub(int i, int j) {
return "sub_string";
}
@MockMethod(targetClass = String.class)
private boolean startsWith(String s) {
return false;
}
@MockMethod(targetClass = BlackBox.class)
private BlackBox secretBox() {
return new BlackBox("not_secret_box");
}
@MockMethod(targetClass = DemoMock.class)
private String callFromDifferentMethod() {
if ("special_case".equals(MOCK_CONTEXT.get("case"))) {
return "mock_special";
}
switch (SOURCE_METHOD) {
case "callerOne": return "mock_one";
default: return "mock_others";
}
}
@Test
public void should_able_to_mock_new_object() {
assertEquals("mock_something", demoMock.newFunc());
verify("createBlackBox").with("something");
}
@Test
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
public void should_able_to_mock_common_method() {
assertEquals("trim_string__sub_string__false", demoMock.commonFunc());
verify("trim").withTimes(1);
verify("sub").withTimes(1);
verify("startsWith").withTimes(1);
}
@Test
public void should_able_to_mock_static_method() {
assertEquals("not_secret_box", demoMock.getBox().get());
verify("secretBox").withTimes(1);
}
@Test
public void should_able_to_get_source_method_name() throws Exception {
// synchronous
assertEquals("mock_one_mock_others", demoMock.callerOne() + "_" + demoMock.callerTwo());
// asynchronous
assertEquals("mock_one_mock_others",
Executors.newSingleThreadExecutor().submit(() -> demoMock.callerOne() + "_" + demoMock.callerTwo()).get());
verify("callFromDifferentMethod").withTimes(4);
}
@Test
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

@@ -0,0 +1,77 @@
package com.alibaba.testable.demo;
import com.alibaba.testable.core.accessor.PrivateAccessor;
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
import org.junit.Test;
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
public class DemoPrivateAccessTest {
private DemoPrivateAccess demoPrivateAccess = new DemoPrivateAccess();
@Test
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
public void should_able_to_access_private_field() {
demoPrivateAccess.count = 2;
assertEquals(Integer.valueOf(2), demoPrivateAccess.count);
PrivateAccessor.set(demoPrivateAccess, "count", 3);
assertEquals(Integer.valueOf(3), PrivateAccessor.get(demoPrivateAccess, "count"));
}
@Test
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
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(Integer.valueOf(3), PrivateAccessor.getStatic(DemoPrivateAccess.class, "staticCount"));
}
@Test
public void should_able_to_update_final_field() {
demoPrivateAccess.pi = 4.13;
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

@@ -0,0 +1,93 @@
package com.alibaba.testable.demo;
import com.alibaba.testable.core.annotation.MockConstructor;
import com.alibaba.testable.core.annotation.MockMethod;
import org.junit.Test;
import java.util.*;
import static org.junit.Assert.assertEquals;
/**
* 演示模板方法的Mock场景
* Demonstrate scenario of mocking template method
*/
public class DemoTemplateTest {
private DemoTemplate demoTemplate = new DemoTemplate();
/* 第一种写法:使用泛型定义 */
/* First solution: use generics type */
@MockMethod
private static <T> List<T> getList(DemoTemplate self, T value) {
return new ArrayList<T>() {{ add((T)(value.toString() + "_mock_list")); }};
}
@MockMethod
private static <K, V> Map<K, V> getMap(DemoTemplate self, K key, V value) {
return new HashMap<K, V>() {{ put(key, (V)(value.toString() + "_mock_map")); }};
}
@MockConstructor
private <T> HashSet<T> newHashSet() {
HashSet<T> set = new HashSet<>();
set.add((T)"insert_mock");
return set;
}
@MockMethod
private <E> boolean add(Set s, E e) {
s.add(e.toString() + "_mocked");
return true;
}
/* 第二种写法使用Object类型 */
/* Second solution: use object type */
//@MockMethod
//private static List<Object> getList(DemoTemplate self, Object value) {
// return new ArrayList<Object>() {{ add(value.toString() + "_mock_list"); }};
//}
//
//@MockMethod
//private static Map<Object, Object> getMap(DemoTemplate self, Object key, Object value) {
// return new HashMap<Object, Object>() {{ put(key, value.toString() + "_mock_map"); }};
//}
//
//@MockConstructor
//private HashSet newHashSet() {
// HashSet<Object> set = new HashSet<>();
// set.add("insert_mock");
// return set;
//}
//
//@MockMethod
//private boolean add(Set s, Object e) {
// s.add(e.toString() + "_mocked");
// return true;
//}
@Test
public void should_able_to_mock_single_template_method() {
String res = demoTemplate.singleTemplateMethod();
assertEquals("demo_mock_list", res);
}
@Test
public void should_able_to_mock_double_template_method() {
String res = demoTemplate.doubleTemplateMethod();
assertEquals("testable_mock_map", res);
}
@Test
public void should_able_to_mock_new_template_method() {
Set<?> res = demoTemplate.newTemplateMethod();
assertEquals(2, res.size());
Iterator<?> iterator = res.stream().iterator();
assertEquals("insert_mock", iterator.next());
assertEquals("world_mocked", iterator.next());
}
}

View File

@@ -1,60 +0,0 @@
package com.alibaba.testable.demo.service;
import com.alibaba.testable.core.annotation.TestableMock;
import com.alibaba.testable.demo.model.BlackBox;
import org.junit.jupiter.api.Test;
import static com.alibaba.testable.core.matcher.InvokeMatcher.*;
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
class DemoMatcherServiceTest {
private DemoMatcherService demo = new DemoMatcherService();
@TestableMock(targetMethod = "methodToBeMocked")
private void methodWithoutArgument(DemoMatcherService self) {}
@TestableMock(targetMethod = "methodToBeMocked")
private void methodWithArguments(DemoMatcherService self, Object a1, Object a2) {}
@TestableMock(targetMethod = "methodToBeMocked")
private void methodWithArrayArgument(DemoMatcherService self, Object[] a) {}
@Test
void should_match_no_argument() {
demo.callMethodWithoutArgument();
verify("methodWithoutArgument").withTimes(1);
demo.callMethodWithoutArgument();
verify("methodWithoutArgument").withTimes(2);
}
@Test
void should_match_number_arguments() {
demo.callMethodWithNumberArguments();
verify("methodWithArguments").without(anyString(), 2);
verify("methodWithArguments").withInOrder(anyInt(), 2);
verify("methodWithArguments").withInOrder(anyLong(), anyNumber());
verify("methodWithArguments").with(1.0, anyMapOf(Integer.class, Float.class));
verify("methodWithArguments").with(anyList(), anySetOf(Float.class));
verify("methodWithArguments").with(anyList(), anyListOf(Float.class));
verify("methodWithArrayArgument").with(anyArrayOf(Long.class));
verify("methodWithArrayArgument").with(anyArray());
}
@Test
void should_match_string_arguments() {
demo.callMethodWithStringArgument();
verify("methodWithArguments").with(startsWith("he"), endsWith("ld"));
verify("methodWithArguments").with(contains("stab"), matches("m.[cd]k"));
verify("methodWithArrayArgument").with(anyArrayOf(String.class));
}
@Test
void should_match_object_arguments() {
demo.callMethodWithObjectArgument();
verify("methodWithArguments").withInOrder(any(BlackBox.class), any(BlackBox.class));
verify("methodWithArguments").withInOrder(nullable(BlackBox.class), nullable(BlackBox.class));
verify("methodWithArguments").withInOrder(isNull(), notNull());
}
}

View File

@@ -1,116 +0,0 @@
package com.alibaba.testable.demo.service;
import com.alibaba.testable.core.annotation.TestableMock;
import com.alibaba.testable.demo.model.BlackBox;
import com.alibaba.testable.demo.model.Box;
import org.junit.jupiter.api.Test;
import java.util.concurrent.Executors;
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
import static com.alibaba.testable.core.tool.TestableTool.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
class DemoMockServiceTest {
@TestableMock(targetMethod = CONSTRUCTOR)
private BlackBox createBlackBox(String text) {
return new BlackBox("mock_" + text);
}
@TestableMock
private String innerFunc(DemoMockService self, String text) {
return "mock_" + text;
}
@TestableMock
private String trim(String self) {
return "trim_string";
}
@TestableMock(targetMethod = "substring")
private String sub(String self, int i, int j) {
return "sub_string";
}
@TestableMock
private boolean startsWith(String self, String s) {
return false;
}
@TestableMock
private BlackBox secretBox(BlackBox ignore) {
return new BlackBox("not_secret_box");
}
@TestableMock
private void put(Box self, String something) {
self.put("put_" + something + "_mocked");
}
@TestableMock
private String callFromDifferentMethod(DemoMockService self) {
if (TEST_CASE.equals("should_able_to_get_test_case_name")) {
return "mock_special";
}
switch (SOURCE_METHOD) {
case "callerOne": return "mock_one";
default: return "mock_others";
}
}
private DemoMockService demoService = new DemoMockService();
@Test
void should_able_to_mock_new_object() throws Exception {
assertEquals("mock_something", demoService.newFunc());
verify("createBlackBox").with("something");
}
@Test
void should_able_to_mock_member_method() throws Exception {
assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello"));
verify("innerFunc").with("hello");
}
@Test
void should_able_to_mock_common_method() throws Exception {
assertEquals("trim_string__sub_string__false", demoService.commonFunc());
verify("trim").withTimes(1);
verify("sub").withTimes(1);
verify("startsWith").withTimes(1);
}
@Test
void should_able_to_mock_static_method() throws Exception {
assertEquals("not_secret_box", demoService.getBox().get());
verify("secretBox").withTimes(1);
}
@Test
void should_able_to_mock_override_method() throws Exception {
BlackBox box = (BlackBox)demoService.putBox();
verify("put").withTimes(1);
assertEquals("put_data_mocked", box.get());
}
@Test
void should_able_to_get_source_method_name() throws Exception {
// synchronous
assertEquals("mock_one_mock_others", demoService.callerOne() + "_" + demoService.callerTwo());
// asynchronous
assertEquals("mock_one_mock_others",
Executors.newSingleThreadExecutor().submit(() -> demoService.callerOne() + "_" + demoService.callerTwo()).get());
verify("callFromDifferentMethod").withTimes(4);
}
@Test
void should_able_to_get_test_case_name() throws Exception {
// synchronous
assertEquals("mock_special", demoService.callerOne());
// asynchronous
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoService.callerOne()).get());
verify("callFromDifferentMethod").withTimes(2);
}
}

View File

@@ -1,37 +0,0 @@
package com.alibaba.testable.demo.service;
import com.alibaba.testable.core.accessor.PrivateAccessor;
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@EnablePrivateAccess
class DemoPrivateAccessServiceTest {
private DemoPrivateAccessService demoService = new DemoPrivateAccessService();
@Test
void should_able_to_access_private_method() throws Exception {
assertEquals("hello - 1", demoService.privateFunc("hello", 1));
assertEquals("hello - 1", PrivateAccessor.invoke(demoService, "privateFunc", "hello", 1));
}
@Test
void should_able_to_access_private_field() throws Exception {
demoService.count = 2;
assertEquals("4", demoService.privateFieldAccessFunc());
assertEquals(new Integer(4), demoService.count);
PrivateAccessor.set(demoService, "count", 3);
assertEquals("5", demoService.privateFieldAccessFunc());
assertEquals(new Integer(5), PrivateAccessor.get(demoService, "count"));
}
@Test
void should_able_to_update_final_field() throws Exception {
demoService.pi = 4.13;
assertEquals(4.13, demoService.pi);
}
}

View File

@@ -0,0 +1,33 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.4.10"
}
group = "com.alibaba.testable"
version = "1.0.0-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8
repositories {
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.11")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.11")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "1.8"
}
}
tasks.withType<Test> {
jvmArgs("-javaagent:${classpath.find { it.name.contains("testable-agent") }!!.absolutePath}")
useJUnitPlatform()
}

View File

@@ -1,135 +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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>kotlin-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>kotlin-demo</name>
<description>Demo project for Spring Boot</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>
<testable.version>0.3.0</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.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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-processor</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</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.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</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>
</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

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

View File

@@ -1,11 +0,0 @@
package com.alibaba.testable.demo
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class DemoApplication
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}

View File

@@ -0,0 +1,66 @@
package com.alibaba.testable.demo
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 {
/**
* call method overridden by sub class via parent class variable
*/
fun putIntoBox(): Box {
val box: Box = BlackBox("")
box.put("data")
return box
}
/**
* call method overridden by sub class via sub class variable
*/
fun putIntoBlackBox(): BlackBox {
val box = BlackBox("")
box.put("data")
return box
}
/**
* call method defined in parent class via parent class variable
*/
val fromBox: String?
get() {
val box: Box = BlackBox("data")
return box.get()
}
/**
* call method defined in parent class via sub class variable
*/
val fromBlackBox: String?
get() {
val box = BlackBox("data")
return box.get()
}
/**
* call method defined in interface via interface variable
*/
val colorViaColor: String
get() {
val color: Color = BlackBox("")
return color.color
}
/**
* call method defined in interface via sub class variable
*/
val colorViaBox: String
get() {
val box = BlackBox("")
return box.color
}
}

View File

@@ -0,0 +1,79 @@
package com.alibaba.testable.demo
import com.alibaba.testable.demo.model.BlackBox
import java.util.*
/**
* 演示Mock方法调用校验器
* Demonstrate mock method invocation verifier
*/
class DemoMatcher {
/**
* Method to be mocked
*/
private fun methodToBeMocked() {
// pretend to have some code here
}
/**
* Method to be mocked
*/
private fun methodToBeMocked(a1: Any?, a2: Any?) {
// pretend to have some code here
}
/**
* Method to be mocked
*/
private fun methodToBeMocked(a: Array<out Any>) {
// pretend to have some code here
}
fun callMethodWithoutArgument() {
methodToBeMocked()
}
fun callMethodWithNumberArguments() {
// named variable and lambda variable will be recorded as different type
// should have them both in test case
val floatList: MutableList<Float> = ArrayList()
floatList.add(1.0f)
floatList.add(2.0f)
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))
}
fun callMethodWithStringArgument() {
methodToBeMocked("hello", "world")
methodToBeMocked("testable", "mock")
methodToBeMocked(arrayOf("demo"))
}
fun callMethodWithObjectArgument() {
methodToBeMocked(BlackBox("hello"), BlackBox("world"))
methodToBeMocked(BlackBox("demo"), null)
methodToBeMocked(null, BlackBox("demo"))
}
}

View File

@@ -1,20 +1,20 @@
package com.alibaba.testable.demo.service
package com.alibaba.testable.demo
import com.alibaba.testable.demo.model.BlackBox
import com.alibaba.testable.demo.model.Box
import com.alibaba.testable.demo.model.ColorBox
import org.springframework.stereotype.Service
import sun.net.www.http.HttpClient
import java.net.URL
import java.nio.file.Files
import java.nio.file.Paths
@Service
class DemoMockService {
/**
* 演示基本的Mock功能
* Demonstrate basic mock functionality
*/
class DemoMock {
/**
* method with new operation
*/
fun newFunc(): String {
fun newFunc(): String? {
return BlackBox("something").get()
}
@@ -22,7 +22,7 @@ class DemoMockService {
* method with member method invoke
*/
fun outerFunc(s: String): String {
return "{ \"res\": \"" + innerFunc(s) + "\"}"
return "{ \"res\": \"" + innerFunc(s) + staticFunc() + "\"}"
}
/**
@@ -40,15 +40,6 @@ class DemoMockService {
return ColorBox.createBox("Red", BlackBox.secretBox())
}
/**
* method with override method invoke
*/
fun putBox(): Box {
val box: Box = BlackBox("")
box.put("data")
return box
}
/**
* two methods invoke same private method
*/
@@ -60,7 +51,19 @@ class DemoMockService {
return callFromDifferentMethod()
}
private fun innerFunc(s: String) = HttpClient.New(URL("http:/xxx/$s")).urlFile
private fun innerFunc(s: String): String {
return Files.readAllLines(Paths.get("/a-not-exist-file")).joinToString()
}
private fun callFromDifferentMethod() = "realOne"
companion object {
private fun staticFunc(): String {
return "_STATIC_TAIL"
}
// fun callStaticFunc(): String {
// return "CALL${staticFunc()}"
// }
}
}

View File

@@ -0,0 +1,48 @@
package com.alibaba.testable.demo
/**
* 演示私有成员访问功能
* Demonstrate private member access functionality
*/
class DemoPrivateAccess {
/**
* a private member field
*/
private var count = 0
/**
* a constant field
*/
val pi = 3.14
/**
* private member method
*/
private fun privateFunc(list: List<String>, str: String, i: Int): String {
return list.reduce { a: String, s: String -> a + s } + " + " + "$str + $i"
}
companion object {
/**
* a private static field
*/
private var staticCount = 0
/**
* private static method
*/
private fun privateStaticFunc(str: String, i: Int): String {
return "$str + $i"
}
/**
* private jvm static method
*/
@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

@@ -0,0 +1,39 @@
package com.alibaba.testable.demo
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()
l.add(value)
return l
}
private fun <K, V> getMap(key: K, value: V): Map<K, V> {
val m: MutableMap<K, V> = HashMap()
m[key] = value
return m
}
fun singleTemplateMethod(): String {
val list = getList("demo")
return list[0]
}
fun doubleTemplateMethod(): String? {
val map = getMap("hello", "testable")
return map["hello"]
}
fun newTemplateMethod(): Set<*> {
val set: MutableSet<String> = HashSet()
set.add("world")
return set
}
}

View File

@@ -1,26 +1,29 @@
package com.alibaba.testable.demo.model
class BlackBox(private var data: String) : Box {
class BlackBox(var input: String) : Box(), Color {
init {
this.content = input
}
override fun put(something: String) {
data = something
content = something
}
fun get(): String {
return data
override val color: String
get() = "black"
fun trim(): String? {
return content?.trim()
}
fun trim(): String {
return data.trim()
}
fun substring(from: Int, to: Int): String {
return data.substring(from, to)
fun substring(from: Int, to: Int): String? {
return content?.substring(from, to)
}
fun startsWith(prefix: String): Boolean {
return data.startsWith(prefix)
return content?.startsWith(prefix) == true
}
companion object {

View File

@@ -1,7 +1,13 @@
package com.alibaba.testable.demo.model
interface Box {
abstract class Box {
fun put(something: String)
var content: String? = null
abstract fun put(something: String)
open fun get(): String? {
return content
}
}

View File

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

View File

@@ -1,28 +0,0 @@
package com.alibaba.testable.demo.service
import org.springframework.stereotype.Service
@Service
class DemoPrivateAccessService {
private var count = 0
final val pi = 3.14
/**
* private method
*/
private fun privateFunc(s: String, i: Int): String {
return "$s - $i"
}
/**
* method with private field access
*/
fun privateFieldAccessFunc(): String {
count += 2
return count.toString()
}
}

View File

@@ -1,13 +0,0 @@
package com.alibaba.testable.demo
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class DemoApplicationTests {
@Test
fun contextLoads() {
}
}

View File

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

View File

@@ -0,0 +1,86 @@
package com.alibaba.testable.demo
import com.alibaba.testable.core.annotation.MockMethod
import com.alibaba.testable.core.error.VerifyFailedError
import com.alibaba.testable.core.matcher.InvokeMatcher
import com.alibaba.testable.core.matcher.InvokeVerifier
import com.alibaba.testable.demo.model.BlackBox
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
/**
* 演示Mock方法调用校验器
* Demonstrate mock method invocation verifier
*/
internal class DemoMatcherTest {
private val demoMatcher = DemoMatcher()
@MockMethod(targetMethod = "methodToBeMocked")
private fun methodWithoutArgument(self: DemoMatcher) {
}
@MockMethod(targetMethod = "methodToBeMocked")
private fun methodWithArguments(self: DemoMatcher, a1: Any, a2: Any) {
}
@MockMethod(targetMethod = "methodToBeMocked")
private fun methodWithArrayArgument(self: DemoMatcher, a: Array<Any>) {
}
@Test
fun should_match_no_argument() {
demoMatcher.callMethodWithoutArgument()
InvokeVerifier.verify("methodWithoutArgument").withTimes(1)
demoMatcher.callMethodWithoutArgument()
InvokeVerifier.verify("methodWithoutArgument").withTimes(2)
}
@Test
fun should_match_number_arguments() {
demoMatcher.callMethodWithNumberArguments()
InvokeVerifier.verify("methodWithArguments").without(InvokeMatcher.anyString(), 2)
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)).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())
}
@Test
fun should_match_string_arguments() {
demoMatcher.callMethodWithStringArgument()
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.startsWith("he"), InvokeMatcher.endsWith("ld"))
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.contains("stab"), InvokeMatcher.matches("m.[cd]k"))
InvokeVerifier.verify("methodWithArrayArgument").with(InvokeMatcher.anyArrayOf(String::class.java))
}
@Test
fun should_match_object_arguments() {
demoMatcher.callMethodWithObjectArgument()
InvokeVerifier.verify("methodWithArguments").withInOrder(InvokeMatcher.any(BlackBox::class.java), InvokeMatcher.any(BlackBox::class.java))
InvokeVerifier.verify("methodWithArguments").withInOrder(InvokeMatcher.nullable(BlackBox::class.java), InvokeMatcher.nullable(BlackBox::class.java))
InvokeVerifier.verify("methodWithArguments").withInOrder(InvokeMatcher.isNull(), InvokeMatcher.notNull())
}
@Test
fun should_match_with_times() {
demoMatcher.callMethodWithNumberArguments()
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(5)
} catch (e: VerifyFailedError) {
gotError = true
}
if (!gotError) {
Assertions.fail<Any>()
}
}
}

View File

@@ -0,0 +1,122 @@
package com.alibaba.testable.demo
import com.alibaba.testable.core.annotation.MockConstructor
import com.alibaba.testable.core.annotation.MockMethod
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
import com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD
import com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT
import com.alibaba.testable.demo.model.BlackBox
import com.alibaba.testable.demo.model.ColorBox
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.util.concurrent.Executors
/**
* 演示基本的Mock功能
* Demonstrate basic mock functionality
*/
internal class DemoMockTest {
private val demoMock = DemoMock()
@MockConstructor
private fun createBlackBox(text: String) = BlackBox("mock_$text")
@MockMethod(targetClass = DemoMock::class)
private fun innerFunc(text: String) = "mock_$text"
@MockMethod(targetClass = DemoMock::class)
private fun staticFunc(): String {
return "_MOCK_TAIL";
}
@MockMethod(targetClass = BlackBox::class)
private fun trim() = "trim_string"
@MockMethod(targetClass = BlackBox::class, targetMethod = "substring")
private fun sub(i: Int, j: Int) = "sub_string"
@MockMethod(targetClass = BlackBox::class)
private fun startsWith(s: String) = false
@MockMethod(targetClass = BlackBox::class)
private fun secretBox(): BlackBox {
return BlackBox("not_secret_box")
}
@MockMethod(targetClass = ColorBox::class)
private fun createBox(color: String, box: BlackBox): BlackBox {
return BlackBox("White_${box.get()}")
}
@MockMethod(targetClass = DemoMock::class)
private fun callFromDifferentMethod(): String {
return if (MOCK_CONTEXT["case"] == "special_case") {
"mock_special"
} else {
when (SOURCE_METHOD) {
"callerOne" -> "mock_one"
else -> "mock_others"
}
}
}
@Test
fun should_able_to_mock_new_object() {
assertEquals("mock_something", demoMock.newFunc())
verify("createBlackBox").with("something")
}
@Test
fun should_able_to_mock_member_method() {
assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoMock.outerFunc("hello"))
verify("innerFunc").with("hello")
verify("staticFunc").with()
}
// @Test
// fun should_able_to_mock_method_in_companion_object() {
// 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())
verify("trim").withTimes(1)
verify("sub").withTimes(1)
verify("startsWith").withTimes(1)
}
@Test
fun should_able_to_mock_static_method() {
assertEquals("White_not_secret_box", demoMock.getBox().get())
verify("secretBox").withTimes(1)
verify("createBox").withTimes(1)
}
@Test
fun should_able_to_get_source_method_name() {
// synchronous
assertEquals("mock_one_mock_others", demoMock.callerOne() + "_" + demoMock.callerTwo())
// asynchronous
assertEquals("mock_one_mock_others", Executors.newSingleThreadExecutor().submit<String> {
demoMock.callerOne() + "_" + demoMock.callerTwo()
}.get())
verify("callFromDifferentMethod").withTimes(4)
}
@Test
fun should_able_to_get_test_case_name() {
MOCK_CONTEXT["case"] = "special_case"
// synchronous
assertEquals("mock_special", demoMock.callerOne())
// asynchronous
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit<String> {
demoMock.callerOne()
}.get())
verify("callFromDifferentMethod").withTimes(2)
MOCK_CONTEXT.clear()
}
}

View File

@@ -0,0 +1,46 @@
package com.alibaba.testable.demo
import com.alibaba.testable.core.accessor.PrivateAccessor
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
/**
* 演示私有成员访问功能
* Demonstrate private member access functionality
*/
internal class DemoPrivateAccessTest {
private val demoPrivateAccess = DemoPrivateAccess()
@Test
fun should_able_to_access_private_method() {
val list = listOf("a", "b", "c");
assertEquals("abc + hello + 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", list, "hello", 1))
}
@Test
fun should_able_to_access_private_field() {
PrivateAccessor.set(demoPrivateAccess, "count", 3)
assertEquals(3, PrivateAccessor.get(demoPrivateAccess, "count"))
}
@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("abc * hello * 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateJvmStaticFunc", list, "hello", 1))
}
@Test
fun should_able_to_access_private_static_field() {
PrivateAccessor.setStatic(DemoPrivateAccess::class.java, "staticCount", 3)
assertEquals(3, PrivateAccessor.getStatic(DemoPrivateAccess::class.java, "staticCount"))
}
@Test
fun should_able_to_update_final_field() {
PrivateAccessor.set(demoPrivateAccess, "pi", 4.13)
assertEquals(4.13, demoPrivateAccess.pi)
}
}

View File

@@ -0,0 +1,62 @@
package com.alibaba.testable.demo
import com.alibaba.testable.core.annotation.MockConstructor
import com.alibaba.testable.core.annotation.MockMethod
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import java.util.*
/**
* 演示模板方法的Mock场景
* Demonstrate scenario of mocking template method
*/
internal class DemoTemplateTest {
private val demoTemplate = DemoTemplate()
@MockMethod
private fun <T> getList(self: DemoTemplate, value: T): List<T> {
return mutableListOf((value.toString() + "_mock_list") as T)
}
@MockMethod
private fun <K, V> getMap(self: DemoTemplate, key: K, value: V): Map<K, V> {
return mutableMapOf(key to (value.toString() + "_mock_map") as V)
}
@MockConstructor
private fun newHashSet(): HashSet<*> {
val set = HashSet<Any>()
set.add("insert_mock")
return set
}
@MockMethod
private fun <E> add(s: MutableSet<E>, e: E): Boolean {
s.add((e.toString() + "_mocked") as E)
return true
}
@Test
fun should_able_to_mock_single_template_method() {
val res = demoTemplate.singleTemplateMethod()
Assertions.assertEquals("demo_mock_list", res)
}
@Test
fun should_able_to_mock_double_template_method() {
val res = demoTemplate.doubleTemplateMethod()
Assertions.assertEquals("testable_mock_map", res)
}
@Test
fun should_able_to_mock_new_template_method() {
val res = demoTemplate.newTemplateMethod()
Assertions.assertEquals(2, res.size)
val iterator = res.stream().iterator()
Assertions.assertEquals("insert_mock", iterator.next())
Assertions.assertEquals("world_mocked", iterator.next())
}
}

View File

@@ -1,114 +0,0 @@
package com.alibaba.testable.demo.service
import com.alibaba.testable.core.annotation.TestableMock
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
import com.alibaba.testable.core.tool.TestableTool.*
import com.alibaba.testable.demo.model.BlackBox
import com.alibaba.testable.demo.model.Box
import com.alibaba.testable.demo.model.ColorBox
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.util.concurrent.Executors
internal class DemoMockServiceTest {
@TestableMock(targetMethod = CONSTRUCTOR)
private fun createBlackBox(text: String) = BlackBox("mock_$text")
@TestableMock
private fun innerFunc(self: DemoMockService, text: String) = "mock_$text"
@TestableMock
private fun trim(self: BlackBox) = "trim_string"
@TestableMock(targetMethod = "substring")
private fun sub(self: BlackBox, i: Int, j: Int) = "sub_string"
@TestableMock
private fun startsWith(self: BlackBox, s: String) = false
@TestableMock
private fun secretBox(ignore: BlackBox): BlackBox {
return BlackBox("not_secret_box")
}
@TestableMock
private fun createBox(ignore: ColorBox, color: String, box: BlackBox): BlackBox {
return BlackBox("White_${box.get()}")
}
@TestableMock
private fun put(self: Box, something: String) {
self.put("put_" + something + "_mocked")
}
@TestableMock
private fun callFromDifferentMethod(self: DemoMockService): String {
return if (TEST_CASE == "should_able_to_get_test_case_name") {
"mock_special"
} else {
when (SOURCE_METHOD) {
"callerOne" -> "mock_one"
else -> "mock_others"
}
}
}
private val demoService = DemoMockService()
@Test
fun should_able_to_mock_new_object() {
assertEquals("mock_something", demoService.newFunc())
verify("createBlackBox").with("something")
}
@Test
fun should_able_to_mock_member_method() {
assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello"))
verify("innerFunc").with("hello")
}
@Test
fun should_able_to_mock_common_method() {
assertEquals("trim_string__sub_string__false", demoService.commonFunc())
verify("trim").withTimes(1)
verify("sub").withTimes(1)
verify("startsWith").withTimes(1)
}
@Test
fun should_able_to_mock_static_method() {
assertEquals("White_not_secret_box", demoService.getBox().get())
verify("secretBox").withTimes(1)
verify("createBox").withTimes(1)
}
@Test
fun should_able_to_mock_override_method() {
val box = demoService.putBox() as BlackBox
verify("put").withTimes(1)
assertEquals("put_data_mocked", box.get())
}
@Test
fun should_able_to_get_source_method_name() {
// synchronous
assertEquals("mock_one_mock_others", demoService.callerOne() + "_" + demoService.callerTwo())
// asynchronous
assertEquals("mock_one_mock_others", Executors.newSingleThreadExecutor().submit<String> {
demoService.callerOne() + "_" + demoService.callerTwo()
}.get())
verify("callFromDifferentMethod").withTimes(4)
}
@Test
fun should_able_to_get_test_case_name() {
// synchronous
assertEquals("mock_special", demoService.callerOne())
// asynchronous
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit<String> {
demoService.callerOne()
}.get())
verify("callFromDifferentMethod").withTimes(2)
}
}

View File

@@ -1,30 +0,0 @@
package com.alibaba.testable.demo.service
import com.alibaba.testable.core.accessor.PrivateAccessor
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
internal class DemoPrivateAccessServiceTest {
private val demoService = DemoPrivateAccessService()
@Test
fun should_able_to_access_private_method() {
assertEquals("hello - 1", PrivateAccessor.invoke(demoService, "privateFunc", "hello", 1))
}
@Test
fun should_able_to_access_private_field() {
PrivateAccessor.set(demoService, "count", 3)
assertEquals("5", demoService.privateFieldAccessFunc())
assertEquals(5, PrivateAccessor.get(demoService, "count"))
}
@Test
fun should_able_to_update_final_field() {
PrivateAccessor.set(demoService, "pi", 4.13)
assertEquals(4.13, demoService.pi)
}
}

View File

@@ -1,13 +1,13 @@
package com.alibaba.testable.demo.util
import com.alibaba.testable.core.annotation.TestableMock
import com.alibaba.testable.core.annotation.MockMethod
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
import org.junit.jupiter.api.Test
import java.io.File
class PathUtilTest {
@TestableMock
@MockMethod
fun exists(f: File): Boolean {
return when (f.absolutePath) {
"/a/b" -> true
@@ -16,7 +16,7 @@ class PathUtilTest {
}
}
@TestableMock
@MockMethod
fun isDirectory(f: File): Boolean {
return when (f.absolutePath) {
"/a/b/c" -> true
@@ -24,12 +24,12 @@ class PathUtilTest {
}
}
@TestableMock
@MockMethod
fun delete(f: File): Boolean {
return true
}
@TestableMock
@MockMethod
fun listFiles(f: File): Array<File>? {
return when (f.absolutePath) {
"/a/b" -> arrayOf(File("/a/b/c"), File("/a/b/d"))

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

@@ -1,10 +0,0 @@
常见用户问题
---
**1. 如何Mock被测类中通过`@Autowired`初始化的字段?**
直接创建被测类对象,然后利用`TestableMock`访问私有成员的能力直接给这些字段赋值即可。
**2. 通过<u>接口对象或基类对象</u>指向派生类的实例,调用执行了派生类实现的方法。使用`@TestableMock`定义Mock方法时首个参数类型应该用 接口/基类 还是 派生类?**
应该使用 接口/基类 类型,参见`should_able_to_mock_override_method`测试用例。

41
docs/index.html Normal file
View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>TestableMock</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
</head>
<body>
<div id="app">Loading...</div>
<script>
window.$docsify = {
name: 'TestableMock',
repo: 'https://github.com/alibaba/testable-mock',
loadSidebar: "sidebar.md",
loadNavbar: "navbar.md",
mergeNavbar: true,
homepage: '/zh-cn/README.md',
alias: { '^/([^/]+)$': '/zh-cn/$1' },
plugins: [
function (hook, vm) {
hook.beforeEach(function (html) {
return html
+ '\n\n----\n\n'
+ '<a href="https://www.aliyun.com/product/yunxiao" target="_blank" style="color: inherit; font-weight: normal; text-decoration: none;">Powered by 云效</a>'
})
}
]
}
</script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-java.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify-tabs@1"></script>
<script async defer src="https://buttons.github.io/buttons.js"></script>
</body>
</html>

View File

@@ -1,46 +0,0 @@
已知问题
---
**1. 访问私有方法或私有成员代码在IDE提示语法错误**
使用`@EnablePrivateAccessor`注解后访问私有方法或成员变量虽然能正常通过编译但在IDE上依然会提示语法错误。
这个问题与使用`Lombok`工具库后使用生成的`getter``setter`会被IDE报语法错误一样需要通过IDE插件来解决。
当前`TestableMock`尚未提供相关插件。也可以改用`PrivateAccessor`工具类来访问私有成员来避免IDE的异常信息。
**2. 通过IDE运行单个测试用例时Mock功能失效**
这是由于IDE运行单个测试用例时只会运行`maven-surefire-plugin`插件,跳过了`testable-maven-plugin`插件执行导致Mock功能所需的JavaAgent没有随测试启动。
解决方法有两种:
**方法一**:在单元测试配置的"虚拟机参数VM Option"属性值末尾添加JavaAgent启动参数`-javaagent:${HOME}/.m2/repository/com/alibaba/testable/testable-agent/0.2.2/testable-agent-0.2.2.jar`
> PS请将路径中的版本号替换成实际使用的版本号
![idea-vm-option](https://testable-code.oss-cn-beijing.aliyuncs.com/idea-vm-option.png)
**方法二**:不使用`testable-maven-plugin`插件直接配置JavaAgent参数到`maven-surefire-plugin`插件上。(`JMockit`也是使用了这种方法)配置方法为:
> PS请将路径中的版本号替换成实际使用的版本号
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/0.2.2/testable-agent-0.2.2.jar</argLine>
</configuration>
</plugin>
```
用这种方法需要注意,如果项目同时还使用了`Jacoco``on-the-fly`模式(默认模式)统计单元测试覆盖率,则需要在参数中再添加一个`@{argLine}`参数,完整配置如下:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/0.2.2/testable-agent-0.2.2.jar</argLine>
</configuration>
</plugin>
```

View File

@@ -1,47 +0,0 @@
# Release Note
## upcoming version
- add `without()` checker to verify mock method never invoked with specified parameters
- support fuzz matcher when verifying mock invocation
- add `@MockWith` annotation to support share mock method between test classes
- add method to print detail log of mocking process for debugging
## 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 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

View File

@@ -1,151 +0,0 @@
使用说明
---
## 引入TestableMock
在项目的`pom.xml`文件中添加`testable-processor`依赖:
```xml
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-processor</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
```
以及`testable-maven-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>
```
> 其中`${testable.version}`需替换为具体版本号,当前最新版本为`0.2.2-SNAPSHOT`
若仅需使用单元测试随意访问被测类私有字段和方法的能力不使用Mock功能`testable-maven-plugin`插件可以省略。
## 使用TestableMock
`TestableMock`目前能为测试类提供两项增强能力__直接访问被测类的私有成员__ 和 __极速Mock被测方法中的调用__
### 访问私有成员字段和方法
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
- 调用被测类的私有方法
- 读取被测类的私有成员
- 修改被测类的私有成员
- 修改被测类的常量成员使用final修饰的成员
访问和修改私有、常量成员时IDE可能会提示语法有误但编译器将能够正常运行测试。
若不希望看到IDE的语法错误提醒或是在基于JVM的非Java语言项目里譬如Kotlin语言也可以借助`PrivateAccessor`工具类来实现私有成员的访问。
效果见`java-demo``kotlin-demo`示例项目中`DemoPrivateAccessService`类的测试用例。
### Mock被测类的任意方法调用
**1. <u>覆写任意类的方法调用</u>**
在测试类里定义一个有`@TestableMock`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
此时被测类中所有对该需覆写方法的调用将在单元测试运行时将自动被替换为对上述自定义Mock方法的调用。
**注意**:也可以将需覆写的方法名写到`@TestableMock`注解的`targetMethod`参数里这样Mock方法自身就可以随意命名了当遇到重名的待覆写方法时特别有用
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在测试类定义如下方法:
```java
// 原方法签名为`String substring(int, int)`
// 调用此方法的对象`"anything"`类型为`String`
// 则Mock方法签名在其参数列表首位增加一个类型为`String`的参数(名字随意)
// 此参数可用于获得当时的实际调用者的值和上下文
@TestableMock
private String substring(String self, int i, int j) {
return "sub_string";
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_common_method()`测试用例。(由于Kotlin对String类型进行了魔改故Kotlin示例中将被测方法在`BlackBox`类里加了一层封装)
**2. <u>覆写被测类自身的成员方法</u>**
有时候在对某些方法进行测试时希望将被测类自身的另外一些成员方法Mock掉。
操作方法与前一种情况相同Mock方法的第一个参数类型需与被测类相同即可实现对被测类自身不论是公有或私有成员方法的覆写。
例如,被测类中有一个签名为`String innerFunc(String)`的私有方法,我们希望在测试的时候将它替换掉,则只需在测试类定义如下方法:
```java
// 被测类型是`DemoMockService`
// 因此在定义Mock方法时在目标方法参数首位加一个类型为`DemoMockService`的参数(名字随意)
@TestableMock
private String innerFunc(DemoMockService self, String text) {
return "mock_" + text;
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_member_method()`测试用例。
**3. <u>覆写任意类的静态方法</u>**
对于静态方法的Mock与普通方法相同。但需要注意的是对于静态方法传入Mock方法的第一个参数实际值始终是`null`
例如,在被测类中调用了`BlackBox`类型中的静态方法`secretBox()`,改方法签名为`BlackBox secretBox()`则Mock方法如下
```java
// 目标静态方法定义在`BlackBox`类型中
// 在定义Mock方法时在目标方法参数首位加一个类型为`BlackBox`的参数(名字随意)
// 此参数仅用于标识目标类型,实际传入值将始终为`null`
@TestableMock
private BlackBox secretBox(BlackBox ignore) {
return new BlackBox("not_secret_box");
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_static_method()`测试用例。
**4. <u>覆写任意类的new操作</u>**
在测试类里定义一个有`@TestableMock`注解的普通方法,将注解的`targetMethod`参数写为"<init>",然后使该方法与要被创建类型的构造函数参数、返回值类型完全一致,方法名称随意。
此时被测类中所有用`new`创建指定类的操作并使用了与Mock方法参数一致的构造函数将被替换为对该自定义方法的调用。
例如,在被测类中有一处`new BlackBox("something")`调用希望在测试时将它换掉通常是换成Mock对象或换成使用测试参数创建的临时对象则只需定义如下Mock方法
```java
// 要覆写的构造函数签名为`BlackBox(String)`
// 无需在Mock方法参数列表增加额外参数由于使用了`targetMethod`参数Mock方法的名称随意起
// 此处的`CONSTRUCTOR`为`TestableTool`辅助类提供的常量,值为"<init>"
@TestableMock(targetMethod = CONSTRUCTOR)
private BlackBox createBlackBox(String text) {
return new BlackBox("mock_" + text);
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_new_object()`测试用例。
**5. <u>识别当前测试用例和调用来源</u>**
在Mock方法中可以通过`TestableTool.TEST_CASE``TestableTool.SOURCE_METHOD`来识别**当前运行的测试用例名称**和**进入该Mock方法前的被测类方法名称**,从而区分处理不同的调用场景。
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_get_source_method_name()``should_able_to_get_test_case_name()`测试用例。
**6. <u>验证Mock方法被调用的顺序和参数</u>**
在测试用例中可用通过`TestableTool.verify()`方法,配合`with()``withInOrder()``without()``withTimes()`等方法实现对Mock调用情况的验证。
详见`java-demo``kotlin-demo`示例项目中的相关测试用例。

12
docs/zh-cn/README.md Normal file
View File

@@ -0,0 +1,12 @@
TestableMock简介
---
单元测试中的Mock方法通常是为了绕开那些依赖外部资源或无关功能的方法调用使得测试重点能够集中在需要验证和保障的代码逻辑上。
在定义Mock方法时开发者真正关心的只有一件事"<u>这个调用在测试的时候要换成那个假的Mock方法</u>"。
然而当下主流的Mock框架在实现Mock功能时需要开发者操心的事情实在太多Mock框架如何初始化、与所用的单元测试框架是否兼容、要被Mock的方法是不是私有的、是不是静态的、被Mock对象是new出来的还是注入的、怎样把被测对象送回被测类里...这些非关键的额外工作极大分散了使用Mock工具应有的乐趣。
于是,我们开发了`TestableMock`**一款特立独行的轻量Mock工具**。
![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

@@ -0,0 +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

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

@@ -0,0 +1,75 @@
校验Mock调用
---
在测试中除了需要将某些含有外部依赖的方法替换为Mock经常还会需要验证该方法被调用时的参数是否符合预期。
`TestableMock`中提供了校验器verifier和匹配器matcher来实现这一功能。譬如
```java
@Test
public test_case() {
int res = insToTest.methodToTest();
verify("mockMethod").with(123, "abc");
}
```
这个用例会检查在执行被测方法`methodToTest()`时,名称是`mockMethod`的Mock方法是否有被调用过且调用时收到的参数值是否为`123``"abc"`假设被Mock的`mockMethod`方法有两个参数)。
除了这种简单校验以外,`TestableMock`当前已经支持了多种**校验器**,以及能够模糊匹配参数特征的**匹配器**。
在示例项目`java-demo``kotlin-demo`中的`DemoMatcherTest`测试类详细展示了这些校验器和匹配器的用法。
## 基本校验器
- `with(Object... args)` → 验证方法是否被指定参数调用过
- `withInOrder(Object... args)` → 如果指定方法被调用了多次,依据实际调用顺序依次匹配
- `withTimes(int expectedCount)` → 验证方法是否被调用过指定次数,忽略对调用参数的检查
- `without(Object... args)` → 验证方法从未被使用指定参数调用过
- `times(int count)` → 连在`with()``withInOrder()`方法之后使用,验证该方法被同样条件的参数调用过了指定次数
## 基本匹配器
- `any()` → 匹配任何值包括Null
- `any(Class<?> clazz)` → 匹配任何指定类型或子类型的值
- `anyTypeOf(Class<?>... classes)` → 匹配在列表中的任意一种类型的值
- `anyString()` → 匹配任何字符串
- `anyNumber()` → 匹配任何数值(整数或浮点数)
- `anyBoolean()` → 匹配任何布尔值
- `anyByte()` → 匹配任何单字节类型的值
- `anyChar()` → 匹配任何单字符类型的值
- `anyInt()` → 匹配任何整数类型的值
- `anyLong()` → 匹配任何长整数类型的值
- `anyFloat()` → 匹配任何浮点数类型的值
- `anyDouble()` → 匹配任何双精度浮点数类型的值
- `anyShort()` → 匹配任何短整数类型的值
- `anyArray()` → 匹配任何数组
- `anyArrayOf(Class<?> clazz)` → 匹配任何指定类型的数组
- `anyList()` → 匹配任何列表
- `anyListOf(Class<?> clazz)` → 匹配任何指定类型的列表
- `anySet()` → 匹配任何集合
- `anySetOf(Class<?> clazz)` → 匹配任何指定类型的集合
- `anyMap()` → 匹配任何映射
- `anyMapOf(Class<?> keyClass, Class<?> valueClass)` → 匹配任何指定类型的映射
- `anyCollection()` → 匹配任何容器
- `anyCollectionOf(Class<?> clazz)` → 匹配任何指定类型的容器
- `anyIterable()` → 匹配任何迭代器
- `anyIterableOf(Class<?> clazz)` → 匹配任何指定类型的迭代器
- `eq(Object obj)` → 匹配与指定值相等的对象
- `refEq(Object obj)` → 匹配指定对象(非值相等,而是就是同一个对象)
## 空值匹配器
- `isNull()` → 匹配Null
- `notNull()` → 匹配除Null以外的任何值
- `nullable(Class<?> clazz)` → 匹配空或指定类型的任何值
## 字符串匹配器
- `contains(String substring)` → 匹配包含特定子串的字符串
- `matches(String regex)` → 匹配符合指定正则表达式的字符串
- `endsWith(String suffix)` → 匹配以指定子串结尾的字符串
- `startsWith(String prefix)` → 匹配以指定子串开头的字符串
## 万能匹配器
- `any(MatchFunction matcher)` → 匹配符合指定表达式的值

View File

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

View File

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

View File

@@ -0,0 +1,106 @@
# 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
- `@TestableMock`注解已弃用,推荐使用`@MockMethod``@MockConstructor`注解
## 0.4.0
- 修复由于默认类加载器变化导致JVM 9+运行时Mock出错的BUG
- 修复多个测试类包含同名Mock方法时潜在的串号问题
- 重构项目结构,增加用于简化依赖配置的`testable-all`模块
- `TestableTool`中与用户无关的内容迁移到了`TestableConst`类型
## 0.3.2
- 支持在Gradle项目中使用私有成员访问和快速Mock功能
- 支持通过`PrivateAccessor`工具类访问私有静态成员
## 0.3.1
- 支持使用`@MockWith`注解打印详细Mock执行过程
## v0.3.0
- 增加`without()`校验器用于匹配指定Mock方法从未被调用的场景
- 支持对Mock调用参数验证时使用模糊匹配
## v0.2.2
- 支持对Mock调用参数进行校验
- 修复JVM 9+的兼容性问题
## v0.2.1
- 支持Mock静态方法
- 支持Mock Kotlin Companion对象的方法
- 支持Mock接口或父类引用指向子类实例时的调用
## v0.2.0
- 支持通过`TestableTool`工具类识别当前的调用上下文
- 增加`testable-maven-plugin`模块用于简化运行时JavaAgent配置
- 不再需要使用`@EnableTestable`注释显式标记测试类
- 重名了现有的各种注解,以便与更好的与实际功能对应
## 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/zh-cn/doc/setup.md Normal file
View File

@@ -0,0 +1,95 @@
使用TestableMock
---
`TestableMock`是基于源码和字节码增强的Java单元测试辅助工具包含以下功能
- [快速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项目中使用
在项目`pom.xml`文件中,增加`testable-all`依赖和`maven-surefire-plugin`配置,具体方法如下。
建议先添加一个标识TestableMock版本的`property`,便于统一管理:
```xml
<properties>
<testable.version>0.4.11</testable.version>
</properties>
```
`dependencies`列表添加TestableMock依赖
```xml
<dependencies>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
```
最后在`build`区域的`plugins`列表里添加`maven-surefire-plugin`插件(如果已包含此插件则只需添加`<argLine>`部分配置):
```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>
```
若项目同时还使用了`Jacoco``on-the-fly`模式(默认模式)统计单元测试覆盖率,则需在`<argLine>`配置中添加一个`@{argLine}`参数,添加后的配置如下:
```xml
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
```
参见项目`java-demo`的[pom.xml](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/pom.xml)和`kotlin-demo`的[pom.xml](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/pom.xml)文件。
## 在Gradle项目中使用
`build.gradle`文件中添加`TestableMock`依赖:
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.4.11')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.11')
}
```
然后在测试配置中添加javaagent
```groovy
test {
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
}
```
参见项目`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

@@ -0,0 +1,95 @@
测试无返回值的方法
---
如何对void类型的方法进行测试一直是许多单元测试框架在悄悄回避的话题由于以往的单元测试手段主要是对被测单元的返回结果进行校验当遇到方法没有返回值时就会变得无从下手。
从功能的角度来说虽然void方法不返回任何值但它的执行一定会对外界产生某些潜在影响我们将其称为方法的"副作用",比如:
1. 初始化某些外部变量(私有成员变量或者全局静态变量)
2. 在方法体内对外部对象实例进行赋值
3. 输出了日志
4. 调用了其他外部方法
5. ... ...
> 不返回任何值也不产生任何"副作用"的方法没有存在的意义。
这些"副作用"的本质归纳来说可分为两类:**修改外部变量**和**调用外部方法**。
通过`TestableMock`的私有字段访问和Mock校验器可以很方便的实现对"副作用"的结果检查。
### 1. 修改外部变量的void方法
例如,下面这个方法会根据输入修改私有成员变量`hashCache`
```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));
}
... // 其他方法省略
}
```
若要测试此方法,可以利用`TestableMock`直接读取私有成员变量的值,对结果进行校验:
```java
@EnablePrivateAccess // 启用TestableMock的私有成员访问功能
class DemoTest {
private Demo demo = new Demo();
@Test
public void testSaveToCache() {
Integer firstVal = demo.initHash("hello"); // 访问私有方法
Integer nextVal = demo.nextHash(firstVal, "hello"); // 访问私有方法
demo.saveToCache("demo", "hello");
assertEquals(firstVal, demo.hashCache.get("demo::hello")); // 读取私有变量
demo.saveToCache("demo", "hello");
assertEquals(nextVal, demo.hashCache.get("demo::hello")); // 读取私有变量
}
}
```
### 2. 调用外部方法的void方法
例如,下面这个方法会根据输入打印信息到控制台:
```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());
}
}
```
若要测试此方法,可以利用`TestableMock`快速Mock掉`System.out.println`方法。在Mock方法体里可以继续执行原调用相当于并不影响本来方法功能仅用于做调用记录也可以直接留空相当于去除了原方法的副作用
在执行完被测的void类型方法以后`InvokeVerifier.verify()`校验传入的打印内容是否符合预期:
```java
class DemoTest {
private Demo demo = new Demo();
// 拦截`System.out.println`调用
@MockMethod
public void println(PrintStream ps, String msg) {
// 执行原调用
ps.println(msg);
}
@Test
public void testRecordAction() {
Action action = new Action("click", ":download");
demo.recordAction();
// 验证Mock方法`println`被调用,且传入参数符合预期
verify("println").with(matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download"));
}
}
```

View File

@@ -0,0 +1,46 @@
自助问题排查
---
相比`Mockito`等由开发者手工放置Mock类的做法`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也有可能发生预料之外的Mock替换。
若要排查Mock相关的问题只需在测试类上添加`@MockWith`注解,并配置参数`diagnose`值为`MockDiagnose.ENABLE`在运行测试时就会打印出详细的Mock方法替换过程。
```java
@MockWith(diagnose = MockDiagnose.ENABLE)
class DemoTest {
...
}
```
输出日志示例如下:
```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
```
该日志展示了被测类中所有发生了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

@@ -0,0 +1,44 @@
使用Testable Maven插件
---
在使用Maven构建的项目里除了直接修改`maven-surefire-plugin`插件的运行参数,也可通过`testable-maven-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>
```
**注意:**当使用`testable-maven-plugin`插件时,应该移除`maven-surefire-plugin`插件上的TestableMock相关配置。
`testable-maven-plugin`插件能够与Jacoco插件直接同时使用无需额外适配因此能使`pom.xml`文件编写起来更简单且美观。
> 还有一种特殊情况,当`jacoco`插件是通过`maven`命令行参数引入的时候,若要使用`TestableMock`功能,则也必须通过命令行参数引入`testable-maven-plugin`插件。详见[issue-14](https://github.com/alibaba/testable-mock/issues/14)。
但需要注意的是,使用`testable-maven-plugin`插件后通过IntelliJ IDE运行单个测试用例时Mock功能会失效。
这是由于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`替换成实际使用的版本号
![add-testable-javaagent](https://testable-code.oss-cn-beijing.aliyuncs.com/add-testable-javaagent.png)
最后点击运行单元测试,如图中位置③
总体来说,使用`testable-maven-plugin`带来的额外复杂性依然高于其对配置的简化作用。目前而言,直接在`pom.xml`文件中修改`maven-surefire-plugin`插件配置还是相对推荐的实用方案。

169
docs/zh-cn/doc/use-mock.md Normal file
View File

@@ -0,0 +1,169 @@
快速Mock被测类的任意方法调用
---
相比以往Mock工具以类为粒度的Mock方式`TestableMock`允许用户直接定义需要Mock的单个方法并遵循约定优于配置的原则按照规则自动在测试运行时替换被测方法中的指定方法调用。
> 归纳起来就两条:
> - Mock非构造方法拷贝原方法定义到测试类加`@MockMethod`注解
> - Mock构造方法拷贝原方法定义到测试类返回值换成构造的类型方法名随意加`@MockContructor`注解
具体的Mock方法定义约定如下
#### 1. 覆写任意类的方法调用
在测试类里定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,并在注解的`targetClass`参数指定该方法原本所属对象类型。
此时被测类中所有对该需覆写方法的调用将在单元测试运行时将自动被替换为对上述自定义Mock方法的调用。
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在测试类定义如下方法:
```java
// 原方法签名为`String substring(int, int)`
// 调用此方法的对象`"anything"`类型为`String`
@MockMethod(targetClass = String.class)
private String substring(int i, int j) {
return "sub_string";
}
```
当遇到待覆写方法有重名时,可以将需覆写的方法名写到`@MockMethod`注解的`targetMethod`参数里这样Mock方法自身就可以随意命名了。
下面这个例子展示了`targetMethod`参数的用法,其效果与上述示例相同:
```java
// 使用`targetMethod`指定需Mock的方法名
// 此方法本身现在可以随意命名,但方法参数依然需要遵循相同的匹配规则
@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掉比如这个方法里有许多外部依赖或耗时操作
做法与前一种情况完全相同,只需将`targetClass`参数赋值为被测类,即可实现对被测类自身(不论是公有或私有)成员方法的覆写。
例如,被测类中有一个签名为`String innerFunc(String)`的私有方法,我们希望在测试的时候将它替换掉,则只需在测试类定义如下方法:
```java
// 被测类型是`DemoMock`
@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与普通方法相同。
例如,在被测类中调用了`BlackBox`类型中的静态方法`secretBox()`,该方法签名为`BlackBox secretBox()`则Mock方法如下
```java
@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操作
在测试类里定义一个返回值类型为要被创建的对象类型且方法参数与要Mock的构造函数参数完全一致的方法名称随意然后加上`@MockContructor`注解。
此时被测类中所有用`new`创建指定类的操作并使用了与Mock方法参数一致的构造函数将被替换为对该自定义方法的调用。
例如,在被测类中有一处`new BlackBox("something")`调用希望在测试时将它换掉通常是换成Mock对象或换成使用测试参数创建的临时对象则只需定义如下Mock方法
```java
// 要覆写的构造函数签名为`BlackBox(String)`
// Mock方法返回`BlackBox`类型对象,方法的名称随意起
@MockContructor
private BlackBox createBlackBox(String text) {
return new BlackBox("mock_" + text);
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_new_object()`测试用例。
#### 5. 识别当前测试用例和调用来源
在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()`测试用例。
#### 6. 验证Mock方法被调用的顺序和参数
在测试用例中可用通过`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`版本中彻底解决。

5
docs/zh-cn/navbar.md Normal file
View File

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

19
docs/zh-cn/sidebar.md Normal file
View File

@@ -0,0 +1,19 @@
- 快速上手
- [使用TestableMock](zh-cn/doc/setup.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/frequently-asked-questions.md)
- [在IDE运行单元测试](zh-cn/doc/use-in-ide.md)
- [自助问题排查](zh-cn/doc/troubleshooting.md)
- [使用Maven插件](zh-cn/doc/use-maven-plugin.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

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

View File

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

View File

@@ -1,7 +1,7 @@
package com.alibaba.testable.agent;
import com.alibaba.testable.agent.transformer.TestableClassTransformer;
import com.alibaba.testable.agent.util.LogUtil;
import com.alibaba.testable.agent.util.GlobalConfig;
import java.lang.instrument.Instrumentation;
@@ -12,7 +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 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);
@@ -24,8 +27,19 @@ public class PreMain {
return;
}
for (String a : args.split(AND)) {
if (a.equals(DEBUG)) {
LogUtil.enableDebugLog();
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

@@ -8,11 +8,24 @@ public class ConstPool {
public static final String DOT = ".";
public static final String SLASH = "/";
public static final String CONSTRUCTOR = "<init>";
public static final String TEST_POSTFIX = "Test";
public static final String TESTABLE_INJECT_REF = "_testableInternalRef";
public static final String FIELD_TARGET_METHOD = "targetMethod";
public static final String 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
*/
public static final String CONSTRUCTOR = "<init>";
}

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.agent.util.LogUtil;
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) {
@@ -52,45 +58,70 @@ public class SourceClassHandler extends BaseClassHandler {
private void transformMethod(ClassNode cn, MethodNode mn, Set<MethodInfo> memberInjectMethods,
Set<MethodInfo> newOperatorInjectMethods) {
LogUtil.debug(" Received %d member mock methods, %d constructor mock methods",
memberInjectMethods.size(), newOperatorInjectMethods.size());
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;
@@ -120,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;
}
@@ -149,25 +164,65 @@ 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.debug(" Using %s mock new operation in %s", newOperatorInjectMethodName, mn.name);
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) {
for (int i = start - 1; i >= 0; i--) {
if (instructions[i] instanceof LineNumberNode) {
return ((LineNumberNode)instructions[i]).line;
}
}
return 0;
}
private String getConstructorInjectDesc(String constructorDesc, String classType) {
@@ -175,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.debug(" Using %s mock method in %s", substitutionMethod, mn.name);
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 @TestableMock 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,12 +4,14 @@ import com.alibaba.testable.agent.constant.ConstPool;
import com.alibaba.testable.agent.tool.ImmutablePair;
import com.alibaba.testable.agent.util.AnnotationUtil;
import com.alibaba.testable.agent.util.ClassUtil;
import com.alibaba.testable.core.util.LogUtil;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.*;
import java.util.ArrayList;
import java.util.HashMap;
import javax.lang.model.type.NullType;
import java.util.List;
import java.util.Map;
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
/**
* @author flin
@@ -19,17 +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_RECORD_MOCK_INVOKE = "recordMockInvoke";
private static final String SIGNATURE_TESTABLE_UTIL_METHOD = "(Ljava/lang/Object;)Ljava/lang/String;";
private static final String SIGNATURE_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;Z)V";
private static final Map<String, String> FIELD_TO_METHOD_MAPPING = new HashMap<String, String>() {{
put(FIELD_TEST_CASE, METHOD_CURRENT_TEST_CASE_NAME);
put(FIELD_SOURCE_METHOD, METHOD_CURRENT_SOURCE_METHOD_NAME);
}};
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_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;ZZ)V";
private static final String SIGNATURE_PARAMETERS = "Ljava/util/Map;";
/**
* Handle bytecode of test class
@@ -37,45 +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.TESTABLE_MOCK))) {
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++;
@@ -84,17 +143,29 @@ 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();
il.add(new VarInsnNode(ALOAD, 0));
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, FIELD_TO_METHOD_MAPPING.get(fieldName),
SIGNATURE_TESTABLE_UTIL_METHOD, false));
mn.instructions.insert(instructions[pos], il);
mn.instructions.remove(instructions[pos]);
if (FIELD_TEST_CASE.equals(fieldName)) {
il.add(new 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);
mn.instructions.remove(instructions[pos]);
}
return mn.instructions.toArray();
}
@@ -102,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++) {
@@ -124,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);
@@ -131,15 +207,32 @@ public class TestClassHandler extends BaseClassHandler {
private boolean isMockForConstructor(MethodNode mn) {
for (AnnotationNode an : mn.visibleAnnotations) {
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:
@@ -178,20 +271,4 @@ public class TestClassHandler extends BaseClassHandler {
}
}
private void injectTestableRef(ClassNode cn, MethodNode mn) {
InsnList il = new InsnList();
il.add(new VarInsnNode(ALOAD, 0));
il.add(new FieldInsnNode(PUTSTATIC, cn.name, ConstPool.TESTABLE_INJECT_REF,
ClassUtil.toByteCodeClassName(cn.name)));
mn.instructions.insertBefore(mn.instructions.get(0), il);
}
/**
* Different unit test framework may have different @Test annotation
* but they should always NOT private, protected or static
*/
private boolean couldBeTestMethod(MethodNode mn) {
return (mn.access & (ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC)) == 0 ;
}
}

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,25 +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.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.agent.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.net.URLClassLoader;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
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,46 +35,99 @@ import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassNa
*/
public class TestableClassTransformer implements ClassFileTransformer {
private final Set<ComparableWeakRef<String>> loadedClassNames = ComparableWeakRef.getWeekHashSet();
private static final String FIELD_DIAGNOSE = "diagnose";
private static final String COMMA = ",";
/**
* Just avoid spend time to scan those surely non-user classes
* Should keep these lists as tiny as possible
*/
private final String[] WHITELIST_PREFIXES = new String[] { "com/alibaba/testable/demo/" };
private final String[] BLACKLIST_PREFIXES = new String[] { "jdk/", "java/", "javax/", "com/sun/",
"org/apache/maven/", "com/alibaba/testable/", "junit/", "org/junit/", "org/testng/" };
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classFileBuffer) {
if (isSystemClass(loader, className) || loadedClassNames.contains(new ComparableWeakRef<String>(className))) {
if (isSystemClass(className)) {
// Ignore system class and reloaded class
return null;
}
LogUtil.verbose("Handle class: " + className);
byte[] bytes = null;
try {
if (shouldTransformAsSourceClass(className)) {
// it's a source class with testable enabled
LogUtil.debug("Handling source class %s", className);
loadedClassNames.add(new ComparableWeakRef<String>(className));
List<MethodInfo> injectMethods = getTestableMockMethods(ClassUtil.getTestClassName(className));
return new SourceClassHandler(injectMethods).getBytes(classFileBuffer);
LogUtil.diagnose("Handling source class %s", className);
bytes = new SourceClassHandler(injectMethods).getBytes(classFileBuffer);
dumpByte(className, bytes);
} else if (shouldTransformAsTestClass(className)) {
// it's a test class with testable enabled
LogUtil.debug("Handling test class %s", className);
loadedClassNames.add(new ComparableWeakRef<String>(className));
return new TestClassHandler().getBytes(classFileBuffer);
LogUtil.diagnose("Handling test class %s", className);
bytes = new TestClassHandler().getBytes(classFileBuffer);
dumpByte(className, bytes);
}
} catch (IOException e) {
return null;
} 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();
}
return null;
}
private boolean shouldTransformAsSourceClass(String className) {
return ClassUtil.anyMethodHasAnnotation(ClassUtil.getTestClassName(className), ConstPool.TESTABLE_MOCK);
return hasMockAnnotation(ClassUtil.getTestClassName(className));
}
private boolean shouldTransformAsTestClass(String className) {
return className.endsWith(ConstPool.TEST_POSTFIX) &&
ClassUtil.anyMethodHasAnnotation(className, ConstPool.TESTABLE_MOCK);
return className.endsWith(ConstPool.TEST_POSTFIX) && hasMockAnnotation(className);
}
private boolean isSystemClass(ClassLoader loader, String className) {
private boolean isSystemClass(String className) {
// className can be null for Java 8 lambdas
return !(loader instanceof URLClassLoader) || null == className || className.startsWith("jdk/");
if (null == className) {
return true;
}
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;
}
}
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;
}
private List<MethodInfo> getTestableMockMethods(String className) {
@@ -79,7 +138,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
for (MethodNode mn : cn.methods) {
checkMethodAnnotation(cn, methodInfos, mn);
}
LogUtil.debug(" Found %d mock methods", methodInfos.size());
LogUtil.diagnose(" Found %d mock methods", methodInfos.size());
return methodInfos;
} catch (Exception e) {
return new ArrayList<MethodInfo>();
@@ -87,26 +146,98 @@ 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) {
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_MOCK)) {
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
String fullClassName = toDotSeparateFullClassName(an.desc);
if (fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
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)) {
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
methodInfos.add(new MethodInfo(sourceClassName, targetMethod, mn.name, mn.desc));
if (ConstPool.CONSTRUCTOR.equals(targetMethod)) {
addMockConstructor(methodInfos, cn, mn);
} else {
methodInfos.add(new MethodInfo(targetClass, targetMethod, mn.name, methodDescPair.right));
MethodInfo mi = getMethodInfo(mn, an, targetMethod);
if (mi != null) {
methodInfos.add(mi);
}
}
break;
}
}
}
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(List<MethodInfo> methodInfos, ClassNode cn, MethodNode mn) {
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
methodInfos.add(new MethodInfo(sourceClassName, ConstPool.CONSTRUCTOR, mn.desc, mn.name, mn.desc));
}
/**
* Check whether any method in specified class has specified annotation
* @param className class that need to explore
* @return found annotation or not
*/
private boolean hasMockAnnotation(String className) {
try {
ClassNode cn = new ClassNode();
new ClassReader(className).accept(cn, 0);
if (cn.visibleAnnotations != null) {
for (AnnotationNode an : cn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.MOCK_WITH)) {
setupMockContext(an);
return true;
}
}
}
for (MethodNode mn : cn.methods) {
if (mn.visibleAnnotations != null) {
for (AnnotationNode an : mn.visibleAnnotations) {
String fullClassName = toDotSeparateFullClassName(an.desc);
if (fullClassName.equals(ConstPool.MOCK_METHOD) ||
fullClassName.equals(ConstPool.TESTABLE_MOCK) ||
fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
return true;
}
}
}
}
} catch (Exception e) {
// Usually class not found, return without record
return false;
}
return false;
}
private void setupMockContext(AnnotationNode an) {
MockDiagnose diagnose = AnnotationUtil.getAnnotationParameter(an, FIELD_DIAGNOSE, null, MockDiagnose.class);
if (diagnose != null) {
LogUtil.setLevel(diagnose == MockDiagnose.ENABLE ? LogUtil.LogLevel.LEVEL_DIAGNOSE :
(diagnose == MockDiagnose.VERBOSE ? LogUtil.LogLevel.LEVEL_VERBOSE : LogUtil.LogLevel.LEVEL_MUTE));
}
}
/**
* Split desc to "first parameter" and "desc of rest parameters"
* @param desc method desc
@@ -114,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

@@ -17,10 +17,23 @@ public class AnnotationUtil {
* @return value of parameter
*/
public static <T> T getAnnotationParameter(AnnotationNode an, String key, T defaultValue, Class<T> clazz) {
if (an.values != null) {
if (an != null && an.values != null) {
for (int i = 0; i < an.values.size(); i += 2) {
if (an.values.get(i).equals(key)) {
return clazz.cast(an.values.get(i + 1));
if (clazz.isEnum()) {
// Enum type are stored as String[] in annotation parameter
String[] values = (String[])an.values.get(i + 1);
if (values == null || values.length != 2) {
return defaultValue;
}
Class<? extends Enum> enumClazz = (Class<? extends Enum>)clazz;
return (T)Enum.valueOf(enumClazz, values[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;
@@ -43,11 +41,7 @@ public class ClassUtil {
private static final String EMPTY = "";
private static final String METHOD_VALUE_OF = "valueOf";
private final static String JOINER = "::";
private static final Map<Byte, String> TYPE_MAPPING = new HashMap<Byte, String>();
private static final Map<ComparableWeakRef<String>, Boolean> loadedClass =
new WeakHashMap<ComparableWeakRef<String>, Boolean>();
static {
TYPE_MAPPING.put(TYPE_BYTE, CLASS_BYTE);
@@ -61,45 +55,13 @@ public class ClassUtil {
TYPE_MAPPING.put(TYPE_VOID, EMPTY);
}
/**
* Check whether any method in specified class has specified annotation
* @param className class that need to explore
* @param annotationName annotation to look for
* @return found annotation or not
*/
public static boolean anyMethodHasAnnotation(String className, String annotationName) {
String cacheKey = className + JOINER + annotationName;
Boolean found = loadedClass.get(new ComparableWeakRef<String>(cacheKey));
if (found != null) {
return found;
}
try {
ClassNode cn = new ClassNode();
new ClassReader(className).accept(cn, 0);
for (MethodNode mn : cn.methods) {
if (mn.visibleAnnotations != null) {
for (AnnotationNode an : mn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(annotationName)) {
loadedClass.put(new ComparableWeakRef<String>(cacheKey), true);
return true;
}
}
}
}
} catch (Exception e) {
// ignore
}
loadedClass.put(new ComparableWeakRef<String>(cacheKey), false);
return false;
}
/**
* fit kotlin companion class name to original name
* @param name a class name (which could be a companion class)
* @return is companion class or not
*/
public static boolean isCompanionClassName(String name) {
return name.endsWith("$Companion");
return name.endsWith(ConstPool.KOTLIN_POSTFIX_COMPANION);
}
/**
@@ -108,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;
}
/**
@@ -117,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;
}
@@ -137,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;
}
@@ -156,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
@@ -231,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 + ";";
}

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