mirror of
https://github.com/alibaba/testable-mock.git
synced 2026-08-19 17:53:29 +08:00
Compare commits
213 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f96d272f97 | ||
|
|
b1e6ad58cc | ||
|
|
48d6ef328b | ||
|
|
84df2a0fd8 | ||
|
|
f9621a6f2e | ||
|
|
55ccabbb39 | ||
|
|
cfc28467bb | ||
|
|
23a0a0cd90 | ||
|
|
a1086ff402 | ||
|
|
6237e19a32 | ||
|
|
df73dac54f | ||
|
|
f660ccb88f | ||
|
|
82d87f3447 | ||
|
|
65cc631d39 | ||
|
|
bdd99577c4 | ||
|
|
8be5550331 | ||
|
|
6a5c817a3a | ||
|
|
c76ed2d84a | ||
|
|
14549040d9 | ||
|
|
03ec857cd0 | ||
|
|
b4a29e7e8e | ||
|
|
02c74299f4 | ||
|
|
4c6fc5b228 | ||
|
|
6d6e2ecb7f | ||
|
|
c364e96f77 | ||
|
|
dab1d36a81 | ||
|
|
01bd676df7 | ||
|
|
02d7fb6e91 | ||
|
|
d8ffcacdaf | ||
|
|
be53ea2d9c | ||
|
|
9eb8682ec0 | ||
|
|
2a54fe70dd | ||
|
|
790409a336 | ||
|
|
189ef43565 | ||
|
|
34f1dc8f73 | ||
|
|
b60ebf2f2c | ||
|
|
d95cba6d37 | ||
|
|
5ef06c4bde | ||
|
|
8814276d63 | ||
|
|
374a4d5442 | ||
|
|
9a22f361f3 | ||
|
|
94c5aa8621 | ||
|
|
74e3cf16a4 | ||
|
|
19f28c32db | ||
|
|
affa56d057 | ||
|
|
f1edf92626 | ||
|
|
6f9accf319 | ||
|
|
cefc5fb1df | ||
|
|
948878687c | ||
|
|
d626906d28 | ||
|
|
705ba796f1 | ||
|
|
7e44a66205 | ||
|
|
d57dedb6fc | ||
|
|
d860684e91 | ||
|
|
7bbdf82a41 | ||
|
|
126c7257d8 | ||
|
|
adfba4bac0 | ||
|
|
2c18ea12ed | ||
|
|
d4bce13918 | ||
|
|
b23b8b98b3 | ||
|
|
f228ed5002 | ||
|
|
2303ac188e | ||
|
|
290d0db400 | ||
|
|
77b955a5ce | ||
|
|
ab8a0b32e5 | ||
|
|
39c788cd02 | ||
|
|
7029579534 | ||
|
|
ba951ce744 | ||
|
|
2bbe2eba93 | ||
|
|
4c96f4cc0e | ||
|
|
3a2e3032df | ||
|
|
530f3295bd | ||
|
|
650b385785 | ||
|
|
99e7c3bf18 | ||
|
|
ef5d21baf6 | ||
|
|
f9be92e9d0 | ||
|
|
019e407a4f | ||
|
|
6951b98c7e | ||
|
|
369c2bd192 | ||
|
|
13de5e42ad | ||
|
|
453908cd46 | ||
|
|
2f110ae4d6 | ||
|
|
7ba44ae618 | ||
|
|
66cb8f7732 | ||
|
|
1f29aff00f | ||
|
|
8d12bb42ba | ||
|
|
c1744d2254 | ||
|
|
444967f676 | ||
|
|
860fdcf072 | ||
|
|
851ae91406 | ||
|
|
580fed7dac | ||
|
|
f07ac8f21f | ||
|
|
31a0b49b7a | ||
|
|
0a1b46b352 | ||
|
|
55ec1ca5b5 | ||
|
|
2806d38c94 | ||
|
|
070aeb6559 | ||
|
|
0255ffa911 | ||
|
|
f64e0cd959 | ||
|
|
74b68d495a | ||
|
|
6c0bd7de46 | ||
|
|
593d134672 | ||
|
|
c496837861 | ||
|
|
b42ee1c9b9 | ||
|
|
079410ee0f | ||
|
|
acc3f0bdf2 | ||
|
|
6f9d535cee | ||
|
|
631f45f4c5 | ||
|
|
94127064e8 | ||
|
|
36f170bdd3 | ||
|
|
5ab1cf7a40 | ||
|
|
68075a2ead | ||
|
|
783b127bbb | ||
|
|
cd7097ded9 | ||
|
|
0f2c889c46 | ||
|
|
df4ac8e950 | ||
|
|
9e393b5ef5 | ||
|
|
bb47f5d199 | ||
|
|
2efee1feb2 | ||
|
|
3df95db607 | ||
|
|
a6b289e136 | ||
|
|
3c375713aa | ||
|
|
9a7372d9ab | ||
|
|
e11f2e1645 | ||
|
|
d14c570c96 | ||
|
|
8a8065e179 | ||
|
|
28bb6454c8 | ||
|
|
13fa8a76ce | ||
|
|
cbdab5a246 | ||
|
|
d7945f52f3 | ||
|
|
96ea5680df | ||
|
|
ee21c8dcd8 | ||
|
|
281be6bd51 | ||
|
|
a8b58ffe17 | ||
|
|
8d33cb718f | ||
|
|
2d8433056c | ||
|
|
32eefc25fe | ||
|
|
7f73fc0cc1 | ||
|
|
44fed6ce8b | ||
|
|
d6ac715f57 | ||
|
|
6b126223e8 | ||
|
|
508610b0c8 | ||
|
|
459fea37c3 | ||
|
|
703aa319ba | ||
|
|
df7deedcce | ||
|
|
4701b3b42b | ||
|
|
085c6784d4 | ||
|
|
a1437e7291 | ||
|
|
d17bdb258b | ||
|
|
03253e529d | ||
|
|
6b4467a03b | ||
|
|
8cfaa6d36e | ||
|
|
78a19b6876 | ||
|
|
5484b216f1 | ||
|
|
d1007f6166 | ||
|
|
9b82fcdc24 | ||
|
|
bf4c263d93 | ||
|
|
28bde0b85a | ||
|
|
179e71c7c2 | ||
|
|
f624c9131e | ||
|
|
83974cfb2a | ||
|
|
abb6d04c16 | ||
|
|
16ec3b9444 | ||
|
|
180dfe9da8 | ||
|
|
92a2c69e05 | ||
|
|
f40834eb94 | ||
|
|
2dea6f9480 | ||
|
|
2bdad3c1f6 | ||
|
|
3f672e45da | ||
|
|
328c8540a8 | ||
|
|
857b5e2295 | ||
|
|
1ec2c48c4b | ||
|
|
0ae39d9deb | ||
|
|
dad2ea3a76 | ||
|
|
260fe94a49 | ||
|
|
dfb1e2c094 | ||
|
|
c8115f9623 | ||
|
|
4def468d85 | ||
|
|
efe57feeda | ||
|
|
5150868e8c | ||
|
|
757becc08b | ||
|
|
857cbac81c | ||
|
|
495c08f716 | ||
|
|
da650eb14d | ||
|
|
bd0ec5f4b7 | ||
|
|
65807eece7 | ||
|
|
b74b21cae6 | ||
|
|
ad63de628d | ||
|
|
08aa73b4bd | ||
|
|
dd7499f3cf | ||
|
|
1d98e3c618 | ||
|
|
001001bfd3 | ||
|
|
472f90e7e4 | ||
|
|
1af6b8c8b6 | ||
|
|
1bf6ab3b21 | ||
|
|
9ab737ce57 | ||
|
|
35dbc1f497 | ||
|
|
92f9c27561 | ||
|
|
7b273affe3 | ||
|
|
b4d6a8357e | ||
|
|
ff29172a93 | ||
|
|
d94daf6910 | ||
|
|
fb62ab63cc | ||
|
|
d75fdcd4e9 | ||
|
|
df9da81f75 | ||
|
|
a8ed46e60d | ||
|
|
47115b22af | ||
|
|
b8b4844c38 | ||
|
|
2ae34e0b41 | ||
|
|
401b5f83d7 | ||
|
|
5770a702cc | ||
|
|
15c7e9c48d | ||
|
|
0657c19da3 |
15
.gitignore
vendored
15
.gitignore
vendored
@@ -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
|
||||
|
||||
43
README.md
43
README.md
@@ -1,17 +1,37 @@
|
||||
# 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)了解更多故事。
|
||||
|
||||
<font size="5">**0.5版本已发布**</font>,从`0.4.x`升级到`0.5.x`版本请参考[0.5版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-to-v05)
|
||||
|
||||
如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将在24小时内回复并处理
|
||||
|
||||
-----
|
||||
|
||||
## 版本计划
|
||||
|
||||
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
|
||||
|
||||
- `0.5` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
|
||||
- `0.6` 实现第四项单元测试增强能力"[快速入参构造器](https://alibaba.github.io/testable-mock/#/zh-cn/doc/parameter-constructor)"
|
||||
- `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 +45,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
48
README_EN.md
Normal 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.
|
||||
27
demo/java-demo/build.gradle
Normal file
27
demo/java-demo/build.gradle
Normal file
@@ -0,0 +1,27 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
group = 'com.alibaba.testable'
|
||||
version = '1.0.0-SNAPSHOT'
|
||||
sourceCompatibility = '8'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.5.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.0')
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
test {
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -1,105 +1,83 @@
|
||||
<?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>5.6.2</junit.version>
|
||||
<testable.version>0.5.0</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>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-processor</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<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>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<configuration>
|
||||
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- <plugin>-->
|
||||
<!-- <groupId>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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox;
|
||||
import com.alibaba.testable.demo.basic.model.Box;
|
||||
import com.alibaba.testable.demo.basic.model.Color;
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的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();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.alibaba.testable.demo.service;
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox;
|
||||
|
||||
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);
|
||||
@@ -1,14 +1,16 @@
|
||||
package com.alibaba.testable.demo.service;
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
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 com.alibaba.testable.demo.basic.model.BlackBox;
|
||||
|
||||
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() {
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
package com.alibaba.testable.demo.basic.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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.alibaba.testable.demo.basic.model;
|
||||
|
||||
abstract public class Box {
|
||||
|
||||
protected String data;
|
||||
|
||||
abstract public void put(String something);
|
||||
|
||||
public String get() {
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.alibaba.testable.demo.basic.model;
|
||||
|
||||
public interface Color {
|
||||
|
||||
String getColor();
|
||||
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
|
||||
public interface Box {
|
||||
|
||||
void put(String something);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
public class ASvc {
|
||||
|
||||
public String demo(String name) {
|
||||
return String.format("a_%s", name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
public class BSvc {
|
||||
|
||||
public String demo(String name) {
|
||||
return String.format("b_%s", name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
public class CSvc {
|
||||
|
||||
public String demo(String name) {
|
||||
return String.format("c_%s", name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.demo.basic.DemoInherit;
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox;
|
||||
import com.alibaba.testable.demo.basic.model.Box;
|
||||
import com.alibaba.testable.demo.basic.model.Color;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
|
||||
*/
|
||||
class DemoInheritTest {
|
||||
|
||||
private DemoInherit demoInherit = new DemoInherit();
|
||||
|
||||
public static class Mock {
|
||||
@MockMethod(targetMethod = "put")
|
||||
private void put_into_box(Box self, String something) {
|
||||
self.put("put_" + something + "_into_box");
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private void put_into_blackbox(BlackBox self, String something) {
|
||||
self.put("put_" + something + "_into_blackbox");
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "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
|
||||
void should_able_to_mock_call_sub_object_method_by_parent_object() {
|
||||
BlackBox box = (BlackBox)demoInherit.putIntoBox();
|
||||
verify("put_into_box").withTimes(1);
|
||||
assertEquals("put_data_into_box", box.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_sub_object_method_by_sub_object() {
|
||||
BlackBox box = demoInherit.putIntoBlackBox();
|
||||
verify("put_into_blackbox").withTimes(1);
|
||||
assertEquals("put_data_into_blackbox", box.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_parent_object_method_by_parent_object() {
|
||||
String content = demoInherit.getFromBox();
|
||||
verify("get_from_box").withTimes(1);
|
||||
assertEquals("get_from_box", content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_parent_object_method_by_sub_object() {
|
||||
String content = demoInherit.getFromBlackBox();
|
||||
verify("get_from_blackbox").withTimes(1);
|
||||
assertEquals("get_from_blackbox", content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_interface_method_by_interface_object() {
|
||||
String color = demoInherit.getColorViaColor();
|
||||
verify("get_color_from_color").withTimes(1);
|
||||
assertEquals("color_from_color", color);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_interface_method_by_sub_class_object() {
|
||||
String color = demoInherit.getColorViaBox();
|
||||
verify("get_color_from_blackbox").withTimes(1);
|
||||
assertEquals("color_from_blackbox", color);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.core.error.VerifyFailedError;
|
||||
import com.alibaba.testable.demo.basic.DemoMatcher;
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeMatcher.*;
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* 演示Mock方法调用校验器
|
||||
* Demonstrate mock method invocation verifier
|
||||
*/
|
||||
class DemoMatcherTest {
|
||||
|
||||
private DemoMatcher demoMatcher = new DemoMatcher();
|
||||
|
||||
public static class Mock {
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithoutArgument(DemoMatcher self) {}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArguments(DemoMatcher self, Object a1, Object a2) {}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArrayArgument(DemoMatcher self, Object[] a) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_match_no_argument() {
|
||||
demoMatcher.callMethodWithoutArgument();
|
||||
verify("methodWithoutArgument").withTimes(1);
|
||||
demoMatcher.callMethodWithoutArgument();
|
||||
verify("methodWithoutArgument").withTimes(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
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
|
||||
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
|
||||
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
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.demo.basic.DemoMock;
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox;
|
||||
import org.junit.jupiter.api.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.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示基本的Mock功能
|
||||
* Demonstrate basic mock functionality
|
||||
*/
|
||||
class DemoMockTest {
|
||||
|
||||
private DemoMock demoMock = new DemoMock();
|
||||
|
||||
public static class Mock {
|
||||
@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
|
||||
void should_able_to_mock_new_object() {
|
||||
assertEquals("mock_something", demoMock.newFunc());
|
||||
verify("createBlackBox").with("something");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoMock.outerFunc("hello"));
|
||||
verify("innerFunc").with("hello");
|
||||
verify("staticFunc").with();
|
||||
}
|
||||
|
||||
@Test
|
||||
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
|
||||
void should_able_to_mock_static_method() {
|
||||
assertEquals("not_secret_box", demoMock.getBox().get());
|
||||
verify("secretBox").withTimes(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_source_method_name() throws Exception {
|
||||
// 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
|
||||
void should_able_to_set_mock_context() throws Exception {
|
||||
MOCK_CONTEXT.put("case", "special_case");
|
||||
// synchronous
|
||||
assertEquals("mock_special", demoMock.callerOne());
|
||||
// asynchronous
|
||||
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoMock.callerOne()).get());
|
||||
verify("callFromDifferentMethod").withTimes(2);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import com.alibaba.testable.demo.basic.DemoPrivateAccess;
|
||||
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
*/
|
||||
@EnablePrivateAccess
|
||||
class DemoPrivateAccessTest {
|
||||
|
||||
private DemoPrivateAccess demoPrivateAccess = new DemoPrivateAccess();
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_method() {
|
||||
List<String> list = new ArrayList<String>() {{ add("a"); add("b"); add("c"); }};
|
||||
assertEquals("member", demoPrivateAccess.privateFunc());
|
||||
assertEquals("member", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc"));
|
||||
assertEquals("abc + hello + 1", demoPrivateAccess.privateFuncWithArgs(list, "hello", 1));
|
||||
assertEquals("abc + hello + 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFuncWithArgs", list, "hello", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_field() {
|
||||
demoPrivateAccess.count = 2;
|
||||
assertEquals(Integer.valueOf(2), demoPrivateAccess.count);
|
||||
|
||||
PrivateAccessor.set(demoPrivateAccess, "count", 3);
|
||||
assertEquals(Integer.valueOf(3), PrivateAccessor.get(demoPrivateAccess, "count"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_static_method() {
|
||||
assertEquals("static", DemoPrivateAccess.privateStaticFunc());
|
||||
assertEquals("static", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFunc"));
|
||||
assertEquals("hello + 1", DemoPrivateAccess.privateStaticFuncWithArgs("hello", 1));
|
||||
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", "hello", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_static_field() {
|
||||
DemoPrivateAccess.staticCount = 2;
|
||||
assertEquals(Integer.valueOf(2), DemoPrivateAccess.staticCount);
|
||||
|
||||
PrivateAccessor.setStatic(DemoPrivateAccess.class, "staticCount", 3);
|
||||
assertEquals(Integer.valueOf(3), PrivateAccessor.getStatic(DemoPrivateAccess.class, "staticCount"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_update_final_field() {
|
||||
demoPrivateAccess.pi = 4.13;
|
||||
assertEquals(Double.valueOf(4.13), demoPrivateAccess.pi);
|
||||
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", 3.14);
|
||||
assertEquals(Double.valueOf(3.14), PrivateAccessor.get(demoPrivateAccess, "pi"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_use_null_parameter() {
|
||||
demoPrivateAccess.pi = null;
|
||||
assertNull(demoPrivateAccess.pi);
|
||||
assertEquals("null + 1", DemoPrivateAccess.privateStaticFuncWithArgs(null, 1));
|
||||
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", null);
|
||||
assertNull(PrivateAccessor.get(demoPrivateAccess, "pi"));
|
||||
assertEquals("null + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", null, 1));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.alibaba.testable.demo.basic;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.demo.basic.DemoTemplate;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示模板方法的Mock场景
|
||||
* Demonstrate scenario of mocking template method
|
||||
*/
|
||||
class DemoTemplateTest {
|
||||
|
||||
private DemoTemplate demoTemplate = new DemoTemplate();
|
||||
|
||||
public static class Mock {
|
||||
/* 第一种写法:使用泛型定义 */
|
||||
/* First solution: use generics type */
|
||||
|
||||
@MockMethod
|
||||
private <T> List<T> getList(DemoTemplate self, T value) {
|
||||
return new ArrayList<T>() {{ add((T)(value.toString() + "_mock_list")); }};
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private <K, V> Map<K, V> getMap(DemoTemplate self, K key, V value) {
|
||||
return new HashMap<K, V>() {{ put(key, (V)(value.toString() + "_mock_map")); }};
|
||||
}
|
||||
|
||||
@MockConstructor
|
||||
private <T> HashSet<T> newHashSet() {
|
||||
HashSet<T> set = new HashSet<>();
|
||||
set.add((T)"insert_mock");
|
||||
return set;
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private <E> boolean add(Set s, E e) {
|
||||
s.add(e.toString() + "_mocked");
|
||||
return true;
|
||||
}
|
||||
|
||||
/* 第二种写法:使用Object类型 */
|
||||
/* Second solution: use object type */
|
||||
|
||||
//@MockMethod
|
||||
//private List<Object> getList(DemoTemplate self, Object value) {
|
||||
// return new ArrayList<Object>() {{ add(value.toString() + "_mock_list"); }};
|
||||
//}
|
||||
//
|
||||
//@MockMethod
|
||||
//private Map<Object, Object> getMap(DemoTemplate self, Object key, Object value) {
|
||||
// return new HashMap<Object, Object>() {{ put(key, value.toString() + "_mock_map"); }};
|
||||
//}
|
||||
//
|
||||
//@MockConstructor
|
||||
//private HashSet newHashSet() {
|
||||
// HashSet<Object> set = new HashSet<>();
|
||||
// set.add("insert_mock");
|
||||
// return set;
|
||||
//}
|
||||
//
|
||||
//@MockMethod
|
||||
//private boolean add(Set s, Object e) {
|
||||
// s.add(e.toString() + "_mocked");
|
||||
// return true;
|
||||
//}
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_single_template_method() {
|
||||
String res = demoTemplate.singleTemplateMethod();
|
||||
assertEquals("demo_mock_list", res);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_double_template_method() {
|
||||
String res = demoTemplate.doubleTemplateMethod();
|
||||
assertEquals("testable_mock_map", res);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_new_template_method() {
|
||||
Set<?> res = demoTemplate.newTemplateMethod();
|
||||
assertEquals(2, res.size());
|
||||
Iterator<?> iterator = res.stream().iterator();
|
||||
assertEquals("insert_mock", iterator.next());
|
||||
assertEquals("world_mocked", iterator.next());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
public class ASvcMock {
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "format")
|
||||
public String a_format(String format, Object... args) {
|
||||
return "a_mock";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
public class BSvcMock {
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "format")
|
||||
public String b_format(String format, Object... args) {
|
||||
return "b_mock";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
|
||||
public class CSvcMock {
|
||||
|
||||
@MockMethod(targetClass = String.class, targetMethod = "format")
|
||||
public String c_format(String format, Object... args) {
|
||||
return "c_mock";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.alibaba.testable.demo.one2multi;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockWith;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@MockWith
|
||||
public class OneToMultiSvcTest {
|
||||
|
||||
private ASvc aSvc = new ASvc();
|
||||
private BSvc bSvc = new BSvc();
|
||||
private CSvc cSvc = new CSvc();
|
||||
|
||||
@Test
|
||||
public void should_able_to_test_multi_class_together() {
|
||||
assertEquals("a_mock", aSvc.demo("test"));
|
||||
assertEquals("b_mock", bSvc.demo("test"));
|
||||
assertEquals("c_mock", cSvc.demo("test"));
|
||||
verify("a_format").withTimes(1);
|
||||
verify("b_format").withTimes(1);
|
||||
verify("c_format").withTimes(1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
33
demo/kotlin-demo/build.gradle.kts
Normal file
33
demo/kotlin-demo/build.gradle.kts
Normal 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.5.0")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.5.0")
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -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.5.0</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>
|
||||
|
||||
1
demo/kotlin-demo/settings.gradle.kts
Normal file
1
demo/kotlin-demo/settings.gradle.kts
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = "demo"
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox
|
||||
import com.alibaba.testable.demo.basic.model.Box
|
||||
import com.alibaba.testable.demo.basic.model.Color
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
import com.alibaba.testable.demo.basic.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"))
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
package com.alibaba.testable.demo.service
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
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 com.alibaba.testable.demo.basic.model.BlackBox
|
||||
import com.alibaba.testable.demo.basic.model.ColorBox
|
||||
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()}"
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
* 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"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.alibaba.testable.demo.basic.model
|
||||
|
||||
|
||||
class BlackBox(var input: String) : Box(), Color {
|
||||
|
||||
init {
|
||||
this.content = input
|
||||
}
|
||||
|
||||
override fun put(something: String) {
|
||||
content = something
|
||||
}
|
||||
|
||||
override val color: String
|
||||
get() = "black"
|
||||
|
||||
fun trim(): String? {
|
||||
return content?.trim()
|
||||
}
|
||||
|
||||
fun substring(from: Int, to: Int): String? {
|
||||
return content?.substring(from, to)
|
||||
}
|
||||
|
||||
fun startsWith(prefix: String): Boolean {
|
||||
return content?.startsWith(prefix) == true
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun secretBox(): BlackBox {
|
||||
return BlackBox("secret")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ColorBox {
|
||||
fun createBox(color: String, box: BlackBox): BlackBox {
|
||||
return BlackBox("${color}_${box.get()}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.alibaba.testable.demo.basic.model
|
||||
|
||||
abstract class Box {
|
||||
|
||||
var content: String? = null
|
||||
|
||||
abstract fun put(something: String)
|
||||
|
||||
open fun get(): String? {
|
||||
return content
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.alibaba.testable.demo.basic.model
|
||||
|
||||
interface Color {
|
||||
|
||||
val color: String
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.demo.util
|
||||
package com.alibaba.testable.demo.java2kotlin
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.alibaba.testable.demo.model
|
||||
|
||||
|
||||
class BlackBox(private var data: String) : Box {
|
||||
|
||||
override fun put(something: String) {
|
||||
data = something
|
||||
}
|
||||
|
||||
fun get(): String {
|
||||
return data
|
||||
}
|
||||
|
||||
fun trim(): String {
|
||||
return data.trim()
|
||||
}
|
||||
|
||||
fun substring(from: Int, to: Int): String {
|
||||
return data.substring(from, to)
|
||||
}
|
||||
|
||||
fun startsWith(prefix: String): Boolean {
|
||||
return data.startsWith(prefix)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun secretBox(): BlackBox {
|
||||
return BlackBox("secret")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ColorBox {
|
||||
fun createBox(color: String, box: BlackBox): BlackBox {
|
||||
return BlackBox("${color}_${box.get()}")
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.alibaba.testable.demo.model
|
||||
|
||||
interface Box {
|
||||
|
||||
fun put(something: String)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
class ASvc {
|
||||
|
||||
fun demo(name: String): String {
|
||||
return String.format("a_%s", name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
class BSvc {
|
||||
|
||||
fun demo(name: String): String {
|
||||
return String.format("b_%s", name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
class CSvc {
|
||||
|
||||
fun demo(name: String): String {
|
||||
return String.format("c_%s", name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox
|
||||
import com.alibaba.testable.demo.basic.model.Box
|
||||
import com.alibaba.testable.demo.basic.model.Color
|
||||
import 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()
|
||||
|
||||
class Mock {
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_box(self: Box, something: String) {
|
||||
self.put("put_" + something + "_into_box")
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_blackbox(self: BlackBox, something: String) {
|
||||
self.put("put_" + something + "_into_blackbox")
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_box(self: Box): String {
|
||||
return "get_from_box"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_blackbox(self: BlackBox): String {
|
||||
return "get_from_blackbox"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_color(self: Color): String {
|
||||
return "color_from_color"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_blackbox(self: BlackBox): String {
|
||||
return "color_from_blackbox"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.error.VerifyFailedError
|
||||
import com.alibaba.testable.core.matcher.InvokeMatcher
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* 演示Mock方法调用校验器
|
||||
* Demonstrate mock method invocation verifier
|
||||
*/
|
||||
internal class DemoMatcherTest {
|
||||
|
||||
private val demoMatcher = DemoMatcher()
|
||||
|
||||
class Mock {
|
||||
@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>()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD
|
||||
import com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT
|
||||
import com.alibaba.testable.demo.basic.model.BlackBox
|
||||
import com.alibaba.testable.demo.basic.model.ColorBox
|
||||
import 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()
|
||||
|
||||
class Mock {
|
||||
@MockConstructor
|
||||
private fun createBlackBox(text: String) = BlackBox("mock_$text")
|
||||
|
||||
@MockMethod(targetClass = DemoMock::class)
|
||||
private fun innerFunc(text: String) = "mock_$text"
|
||||
|
||||
@MockMethod(targetClass = DemoMock::class)
|
||||
private fun 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.alibaba.testable.demo.basic
|
||||
|
||||
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()
|
||||
|
||||
class Mock {
|
||||
@MockMethod
|
||||
private fun <T> getList(self: DemoTemplate, value: T): List<T> {
|
||||
return mutableListOf((value.toString() + "_mock_list") as T)
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private fun <K, V> getMap(self: DemoTemplate, key: K, value: V): Map<K, V> {
|
||||
return mutableMapOf(key to (value.toString() + "_mock_map") as V)
|
||||
}
|
||||
|
||||
@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())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.alibaba.testable.demo.java2kotlin
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
|
||||
class PathUtilTest {
|
||||
|
||||
class Mock {
|
||||
@MockMethod
|
||||
fun exists(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> true
|
||||
"/a/b/c" -> true
|
||||
else -> f.exists()
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun isDirectory(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b/c" -> true
|
||||
else -> f.isDirectory
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun delete(f: File): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun listFiles(f: File): Array<File>? {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> arrayOf(File("/a/b/c"), File("/a/b/d"))
|
||||
"/a/b/c" -> arrayOf(File("/a/b/c/e"))
|
||||
else -> f.listFiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_java_method_invoke_in_kotlin() {
|
||||
PathUtil.deleteRecursively(File("/a/b/"))
|
||||
verify("listFiles").withTimes(2)
|
||||
verify("delete").withTimes(4)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
|
||||
class ASvcMock {
|
||||
|
||||
@MockMethod(targetClass = String::class, targetMethod = "format")
|
||||
fun a_format(format: String, vararg args: Any?): String {
|
||||
return "a_mock"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
|
||||
class BSvcMock {
|
||||
|
||||
@MockMethod(targetClass = String::class, targetMethod = "format")
|
||||
fun b_format(format: String, vararg args: Any?): String {
|
||||
return "b_mock"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
|
||||
class CSvcMock {
|
||||
|
||||
@MockMethod(targetClass = String::class, targetMethod = "format")
|
||||
fun c_format(format: String, vararg args: Any?): String {
|
||||
return "c_mock"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.alibaba.testable.demo.one2multi
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockWith
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@MockWith
|
||||
class OneToMultiSvcTest {
|
||||
|
||||
private val aSvc = ASvc()
|
||||
private val bSvc = BSvc()
|
||||
private val cSvc = CSvc()
|
||||
|
||||
@Test
|
||||
fun should_able_to_test_multi_class_together() {
|
||||
Assertions.assertEquals("a_mock", aSvc.demo("test"))
|
||||
Assertions.assertEquals("b_mock", bSvc.demo("test"))
|
||||
Assertions.assertEquals("c_mock", cSvc.demo("test"))
|
||||
verify("a_format").withTimes(1)
|
||||
verify("b_format").withTimes(1)
|
||||
verify("c_format").withTimes(1)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.alibaba.testable.demo.util
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
|
||||
class PathUtilTest {
|
||||
|
||||
@TestableMock
|
||||
fun exists(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> true
|
||||
"/a/b/c" -> true
|
||||
else -> f.exists()
|
||||
}
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
fun isDirectory(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b/c" -> true
|
||||
else -> f.isDirectory
|
||||
}
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
fun delete(f: File): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
fun listFiles(f: File): Array<File>? {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> arrayOf(File("/a/b/c"), File("/a/b/d"))
|
||||
"/a/b/c" -> arrayOf(File("/a/b/c/e"))
|
||||
else -> f.listFiles()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_java_method_invoke_in_kotlin() {
|
||||
PathUtil.deleteRecursively(File("/a/b/"))
|
||||
verify("listFiles").withTimes(2)
|
||||
verify("delete").withTimes(4)
|
||||
}
|
||||
|
||||
}
|
||||
13
docs/en-us/README.md
Normal file
13
docs/en-us/README.md
Normal 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**.
|
||||
|
||||
|
||||

|
||||
9
docs/en-us/doc/about-us.md
Normal file
9
docs/en-us/doc/about-us.md
Normal 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! 🤠
|
||||
26
docs/en-us/doc/comparation.md
Normal file
26
docs/en-us/doc/comparation.md
Normal 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.
|
||||
8
docs/en-us/doc/feedback.md
Normal file
8
docs/en-us/doc/feedback.md
Normal 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 : )
|
||||
48
docs/en-us/doc/frequently-asked-questions.md
Normal file
48
docs/en-us/doc/frequently-asked-questions.md
Normal file
@@ -0,0 +1,48 @@
|
||||
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. Can `TestableMock` be used for testing Android projects?
|
||||
|
||||
It can be used in combination with [Roboelectric](https://github.com/robolectric/robolectric) testing framework.
|
||||
|
||||
The `Dalvik` and `ART` virtual machines of the Android system use a bytecode system different from the standard JVM, which will affect the normal functionality of `TestableMock`. The `Roboelectric` framework can run Android unit tests on a standard JVM virtual machine, which is much faster than running unit tests through the Android virtual machine. Recently, most Android App unit tests are written with the `Roboelectric` framework.
|
||||
|
||||
#### 7. Meet "Command Line is too Long. Shorten command line for ..." error when triggering test in IntelliJ IDE?
|
||||
|
||||
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`.
|
||||
|
||||

|
||||
75
docs/en-us/doc/invoke-matcher.md
Normal file
75
docs/en-us/doc/invoke-matcher.md
Normal 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
|
||||
6
docs/en-us/doc/parameter-constructor.md
Normal file
6
docs/en-us/doc/parameter-constructor.md
Normal 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.6` version.
|
||||
50
docs/en-us/doc/private-accessor.md
Normal file
50
docs/en-us/doc/private-accessor.md
Normal file
@@ -0,0 +1,50 @@
|
||||
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)
|
||||
|
||||
> This function assumes that the test class is in the same package as the class under test, and the name is `<ClassUnderTest>+Test`. When this convention is not met, you can use the `srcClass` parameter on the `@EnablePrivateAccess` annotation to specify the actual class under test. E.g:
|
||||
>
|
||||
> ```java
|
||||
> @EnablePrivateAccess(srcClass = DemoServiceImpl.class)
|
||||
> class DemoServiceTest() { ... }
|
||||
> ```
|
||||
|
||||
### Solution 2: Use the `PrivateAccessor` tool class
|
||||
|
||||
If you don't want to see the IDE's syntax error reminder, or in a non-Java language JVM project (such as Kotlin language), you can also use the `PrivateAccessor` tool class to directly access private members.
|
||||
|
||||
This class provides 7 static methods:
|
||||
|
||||
- `PrivateAccessor.get(<AnyObject>, "<private-field-name>")` ➜ read the private field of any object
|
||||
- `PrivateAccessor.set(<AnyObject>, "<private-field-name>", <new-value>)` ➜ modify the private field (or constant field) of any object
|
||||
- `PrivateAccessor.invoke(<AnyObject>, "<private-method-name>", <call-parameters>...)` ➜ call the private method of any object
|
||||
- `PrivateAccessor.getStatic(<AnyClass>, "<private-static-field-name>")` ➜ read the **static** private field of any class
|
||||
- `PrivateAccessor.setStatic(<AnyClass>, "<private-static-field-name>", <new-value>)` ➜ modify the **static** private field (or **static** constant field) of any class
|
||||
- `PrivateAccessor.invokeStatic(<AnyClass>, "<private-static-method-name>", <call-parameters>...)` ➜ call the **static** private method of any class
|
||||
- `PrivateAccessor.construct(<AnyClass>, <constructor-parameters>...)` ➜ create a new object by the private constructor of any class
|
||||
|
||||
> Using the `PrivateAccessor` class does not require the test class to have `@EnablePrivateAccess` annotation, but adding this annotation will enable the compile-time verification for the private members of the class under test.
|
||||
|
||||
For details, see the use cases in the test classes of the `java-demo` and `kotlin-demo` sample projects `DemoPrivateAccessTest`.
|
||||
|
||||
### Compile-time verification of private members
|
||||
|
||||
Both of the above two methods essentially use JVM reflection mechanism to achieve private member access, but the JVM compiler will not check the existence of the reflection target. When the code is refactored, if the private method names and parameters in the source class are modified, it would cause exceptions to be discovered only when the unit test is triggered. For this reason, another function of the `@EnablePrivateAccess` annotation is to perform additional compile-time checks for existence of private member of the **class under test**.
|
||||
|
||||
**Note**: When the private member verification function is enabled, the `PrivateAccessor` class can only be used to access the private members of the **class under test**, which will help limit the using of the `PrivateAccessor` tool class for "unauthorized" operations unrelated to the current test. If you really need to access private members of other classes, you can remove the `@EnablePrivateAccess` annotation, or set the `verifyTargetOnCompile` parameter of the annotation to `false` to manually turn off the verification function.
|
||||
112
docs/en-us/doc/release-note.md
Normal file
112
docs/en-us/doc/release-note.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Release Note
|
||||
|
||||
## 0.4.12
|
||||
- support verbose diagnose log for better self-troubleshooting
|
||||
- support disable private access target existence check
|
||||
- support specify mock scanning packages
|
||||
- fix an ArrayIndexOutOfBoundsException issue when transforming native method
|
||||
|
||||
## 0.4.11
|
||||
- support accessing private members of class under test in different package path
|
||||
- validate the number of private method parameters accessed by `PrivateAccessor`
|
||||
- fix a bug which may cause errors when the mock method contains array parameters
|
||||
- fix an issue which cause some private members not be found in the IntelliJ build
|
||||
|
||||
## 0.4.10
|
||||
- fix an issue of using mock in lambda expression
|
||||
- fix the NullPointerException when invoke private method with parameter value `null`
|
||||
|
||||
## 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
95
docs/en-us/doc/setup.md
Normal 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.5.0</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.5.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.0')
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
96
docs/en-us/doc/test-void-method.md
Normal file
96
docs/en-us/doc/test-void-method.md
Normal file
@@ -0,0 +1,96 @@
|
||||
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();
|
||||
|
||||
public static class Mock {
|
||||
// Intercept `System.out.println` invocation
|
||||
@MockMethod
|
||||
public void println(PrintStream ps, String msg) {
|
||||
// Execute the original call
|
||||
ps.println(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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"));
|
||||
}
|
||||
}
|
||||
```
|
||||
92
docs/en-us/doc/troubleshooting.md
Normal file
92
docs/en-us/doc/troubleshooting.md
Normal file
@@ -0,0 +1,92 @@
|
||||
Self-Help Troubleshooting
|
||||
---
|
||||
|
||||
Compared with `Mockito` and other mock tools where developers have to manually inject mock classes, `TestableMock` uses method name and parameter type matching to automatically find invocations that require mock. While this mechanism brings convenience, it may also cause unexpected mock replacement.
|
||||
|
||||
To troubleshoot mock-related issues, just add the `@MockWith` annotation to the test class, and configure the parameter `diagnose` to `MockDiagnose.ENABLE`, so the detailed mock method replacement process will be printed when the test is run.
|
||||
|
||||
```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] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
[DIAGNOSE] Line 27, mock method "staticFunc" used
|
||||
[DIAGNOSE] Handling method commonFunc
|
||||
[DIAGNOSE] Line 34, mock method "trim" used
|
||||
[DIAGNOSE] Line 34, mock method "sub" used
|
||||
[DIAGNOSE] Line 34, mock method "startsWith" used
|
||||
... ...
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
For situations where expected mocking is not take effect, you could set the diagnosis level to `MockDiagnose.VERBOSE` for further investigation information.
|
||||
|
||||
```java
|
||||
@MockWith(diagnose = MockDiagnose.VERBOSE)
|
||||
class DemoTest {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Executing the unit test again will print out the runtime-signatures of all mock methods, and the runtime-signatures of all invocations scanned in the class under test:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
|
||||
[VERBOSE] Mock constructor "createBlackBox" as "(Ljava/lang/String;)V" for "com/alibaba/testable/demo/model/BlackBox"
|
||||
[VERBOSE] Mock method "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
|
||||
[VERBOSE] Mock method "staticFunc" as "()Ljava/lang/String;"
|
||||
[VERBOSE] Mock method "trim" as "()Ljava/lang/String;"
|
||||
[VERBOSE] Mock method "sub" as "(II)Ljava/lang/String;"
|
||||
[VERBOSE] Mock method "startsWith" as "(Ljava/lang/String;)Z"
|
||||
[VERBOSE] Mock method "secretBox" as "()Lcom/alibaba/testable/demo/model/BlackBox;"
|
||||
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[VERBOSE] Line 13, constructing "java/lang/Object" as "()V"
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[VERBOSE] Line 19, constructing "com/alibaba/testable/demo/model/BlackBox" as "(Ljava/lang/String;)V"
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[VERBOSE] Line 19, invoking "createBlackBox" as "(Ljava/lang/String;)Lcom/alibaba/testable/demo/model/BlackBox;"
|
||||
[VERBOSE] Line 20, invoking "get" as "()Ljava/lang/String;"
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[VERBOSE] Line 27, constructing "java/lang/StringBuilder" as "()V"
|
||||
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
|
||||
[VERBOSE] Line 27, invoking "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
[VERBOSE] Line 27, invoking "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
|
||||
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
|
||||
[VERBOSE] Line 27, invoking "staticFunc" as "()Ljava/lang/String;"
|
||||
[DIAGNOSE] Line 27, mock method "staticFunc" used
|
||||
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
|
||||
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
|
||||
[VERBOSE] Line 27, invoking "toString" as "()Ljava/lang/String;"
|
||||
... ...
|
||||
```
|
||||
|
||||
The logs are formatted in follow pattern:
|
||||
|
||||
- `Mock constructor "<MockMethodName>" as "<Signature>" for "<TypeName>"` Mock constructor found in test class
|
||||
- `Mock method "<MockMethodName>" as "<Signature>"` Mock method found in test class (the first parameter that identify the mock target class is currently kept)
|
||||
- `Line XX, constructing "<TypeName>" as "<Signature>"` Constructor invocation found in test under class
|
||||
- `Line XX, invoking "<MethodName>" as "<Signature>"` Member method invocation found in test under class
|
||||
49
docs/en-us/doc/upgrade-to-v05.md
Normal file
49
docs/en-us/doc/upgrade-to-v05.md
Normal file
@@ -0,0 +1,49 @@
|
||||
Upgrade to version 0.5
|
||||
---
|
||||
|
||||
After nearly a month of design and development, the `0.5` version of TestableMock has finally come out. Compared with the `0.4` version, the new version solves the three historical problems left over before:
|
||||
|
||||
1. <s>**Mock method cannot call other non-static methods**</s>. The mock method in the new version no longer has any difference from the ordinary method, and can access any external method and member variable.
|
||||
2. <s>**Mock method always acts on the entire test life cycle**</s>. From now on, the mock method supports restricting the effective scope to the test cases in the test class of which it belongs, so there is no need to worry about accidentally mocking cross-class test invocations.
|
||||
3. <s>**The MOCK_CONTEXT needs manually cleaned up and only supports class-level parallel testing**</s>. Now each test case has an independent `MOCK_CONTEXT` variable, no need to clean up after used, and you can use unit test with any parallel level.
|
||||
|
||||
In order to better realize the reuse of Mock methods, we have made a clear boundary between the mock class and the test class in the new version. When upgrading from `0.4` to `0.5`, the only change required is to wrap all mock methods in the test class with a `public static class Mock {}`.
|
||||
|
||||
For example, the original test class definition was as follows:
|
||||
|
||||
```java
|
||||
public class DemoMockTest {
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "hello_" + text;
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After upgrading to the `0.5` version, move all mock methods (in this example, only the `innerFunc` method) to a static inner class named `Mock`, which is equivalent to adding two lines of code:
|
||||
|
||||
```java
|
||||
public class DemoMockTest {
|
||||
|
||||
public static class Mock { // Add this line
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "hello_" + text;
|
||||
}
|
||||
} // Add this line
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
assertEquals("hello_world", demoMock.outerFunc());
|
||||
verify("innerFunc").with("world");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then upgrade the `TestableMock` dependency in the `pom.xml` or `build.gradle` file to `0.5.0` or above.
|
||||
18
docs/en-us/doc/use-in-ide.md
Normal file
18
docs/en-us/doc/use-in-ide.md
Normal 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.
|
||||
|
||||

|
||||
42
docs/en-us/doc/use-maven-plugin.md
Normal file
42
docs/en-us/doc/use-maven-plugin.md
Normal 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 ①
|
||||
|
||||

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

|
||||
|
||||
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
167
docs/en-us/doc/use-mock.md
Normal 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 mock class, add a `@MockMethod` annotation
|
||||
> - Mock construction method, copy the original method definition to the mock class, replace the return value with the constructed type, the method name is arbitrary, and add a `@MockContructor` annotation
|
||||
|
||||
The detail mock method definition convention is as follows.
|
||||
|
||||
#### 0. Pre-step, prepare the mock class
|
||||
|
||||
First, create a mock class as the container for mock methods associated with the test class. The simplest way is to add a static inner class named `Mock` to the test class. E.g:
|
||||
|
||||
```java
|
||||
public class DemoTest {
|
||||
|
||||
public static class Mock {
|
||||
// mock methods goes here
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
#### 1. Mock method calls of any class
|
||||
|
||||
Define an ordinary method annotated with `@MockMethod` in the mock class with exactly the same signature (name, parameter, and return value type) as the method to be mocked, and then add the type of target object (which the method originally belongs to) as `targetMethod` parameter of `@MockMethod` annotation.
|
||||
|
||||
At this time, all invocations to that original method in the class under test will be automatically replaced with invocations to the above-mentioned mock method when the unit test is running.
|
||||
|
||||
For example, there is a call to `"anything".substring(1, 2)` in the class under test, and we want to change it to a fixed string when running the test, we only need to define the following method in the mock 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 mock 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 mock class, make the return value type of the method the type of the object to be created, and the method parameters are exactly the same as the constructor parameters to be mocked, the method name is arbitrary.
|
||||
|
||||
At this time, all operations in the class under test that use `new` to create the specified class (and use the constructor that is consistent with the mock method parameters) will be replaced with calls to the custom method.
|
||||
|
||||
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 different invocation source in mock method
|
||||
|
||||
In the mock method, you can use the `TestableTool.SOURCE_METHOD` variable to identify **the method name of the class under test before entering the mock method**; in addition, the `TestableTool.MOCK_CONTEXT` variable can **inject additional context parameters into the mock method**, to distinguish and process different calling scenarios.
|
||||
|
||||
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());
|
||||
}
|
||||
```
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
5
docs/en-us/navbar.md
Normal file
5
docs/en-us/navbar.md
Normal file
@@ -0,0 +1,5 @@
|
||||
* [Home](/en-us/)
|
||||
* [Feedback And Issue](https://github.com/alibaba/testable-mock/issues)
|
||||
* Languages
|
||||
* English
|
||||
* [中文](/zh-cn/)
|
||||
20
docs/en-us/sidebar.md
Normal file
20
docs/en-us/sidebar.md
Normal file
@@ -0,0 +1,20 @@
|
||||
- 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
|
||||
- [Upgrade To 0.5 Version](en-us/doc/upgrade-to-v05.md)
|
||||
- [Mock Tools Comparison](en-us/doc/comparation.md)
|
||||
- [Release Note](en-us/doc/release-note.md)
|
||||
- [About Us](en-us/doc/about-us.md)
|
||||
- [Feedback Channel](en-us/doc/feedback.md)
|
||||
@@ -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
41
docs/index.html
Normal 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>
|
||||
@@ -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:请将路径中的版本号替换成实际使用的版本号
|
||||
|
||||

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

|
||||
9
docs/zh-cn/doc/about-us.md
Normal file
9
docs/zh-cn/doc/about-us.md
Normal 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语言遗留的测试难题。与其束手束脚的采用“测试技巧”间接测试功能,不如索性解除限制,就让单元测试来得更猛烈一些吧!🤠
|
||||
61
docs/zh-cn/doc/annotations.md
Normal file
61
docs/zh-cn/doc/annotations.md
Normal file
@@ -0,0 +1,61 @@
|
||||
注解参数清单
|
||||
---
|
||||
|
||||
基于轻量的原则,`TestableMock`为开发者提供了尽可能精炼、易用的注解组合,以下参数信息可供开发参考。
|
||||
|
||||
#### @EnablePrivateAccess
|
||||
|
||||
启用对被测类的<u>私有成员访问编译期增强</u>和<u>私有目标存在性的编译期校验</u>功能。
|
||||
|
||||
- 作用于:测试类
|
||||
|
||||
| 参数 | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| srcClass | Class | 否 | N/A | 当测试类命名不符合约定时,指定实际被测类 |
|
||||
| verifyTargetOnCompile | boolean | 否 | true | 是否启用私有目标的编译期存在性校验 |
|
||||
|
||||
#### @MockMethod
|
||||
|
||||
将当前方法标识为待匹配的Mock成员方法。
|
||||
|
||||
- 作用于:Mock容器类中的方法
|
||||
|
||||
| 参数 | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| targetClass | Class | 否 | N/A | 指定Mock目标的调用者类型 |
|
||||
| targetMethod | String | 否 | N/A | 指定Mock目标的方法名 |
|
||||
| scope | MockScope | 否 | MockScope.GLOBAL | 指定Mock的生效范围 |
|
||||
|
||||
#### @MockConstructor
|
||||
|
||||
将当前方法标识为待匹配的Mock构造方法。
|
||||
|
||||
- 作用于:Mock容器类中的方法
|
||||
|
||||
| 参数 | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| scope | MockScope | 否 | MockScope.GLOBAL | 指定Mock的生效范围 |
|
||||
|
||||
|
||||
#### @MockWith
|
||||
|
||||
显式指定当前类型关联的Mock容器类。
|
||||
|
||||
- 作用于:测试类、被测类
|
||||
|
||||
| 参数(`N/A`为默认参数) | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| N/A | Class | 否 | NullType.class | 指定使用的Mock容器类 |
|
||||
| treatAs | ClassType | 否 | ClassType.GuessByName | 指定当前类是测试类或被测类 |
|
||||
| diagnose | LogLevel | 否 | N/A | (**deprecated**)指定Mock诊断日志级别 |
|
||||
|
||||
#### @MockDiagnose
|
||||
|
||||
启用或禁止Mock相关的诊断信息输出。
|
||||
|
||||
- 作用于:Mock容器类
|
||||
|
||||
| 参数(`N/A`为默认参数) | 类型 | 是否必须 | 默认值 | 作用 |
|
||||
| --- | --- | --- | ---- | --- |
|
||||
| N/A | LogLevel | 是 | N/A | 指定当前Mock容器关联测试用例的诊断日志级别 |
|
||||
|
||||
26
docs/zh-cn/doc/comparation.md
Normal file
26
docs/zh-cn/doc/comparation.md
Normal 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编写习惯会带来一定改变。
|
||||
8
docs/zh-cn/doc/feedback.md
Normal file
8
docs/zh-cn/doc/feedback.md
Normal file
@@ -0,0 +1,8 @@
|
||||
问题反馈
|
||||
---
|
||||
|
||||
在使用`TestableMock`过程中如有任何问题或建议,请直接在[项目Issue](https://github.com/alibaba/testable-mock/issues)中提出,我们将在24小时内回复。
|
||||
|
||||
请详细描述遇到的情况,若您觉得问题可能与`TestableMock`的潜在BUG有关,请尽量提供可复现的相关源码或示例,以便进行针对性定位和排查。
|
||||
|
||||
`TestableMock`,让Java没有难测的代码 : )
|
||||
48
docs/zh-cn/doc/frequently-asked-questions.md
Normal file
48
docs/zh-cn/doc/frequently-asked-questions.md
Normal file
@@ -0,0 +1,48 @@
|
||||
常见使用问题
|
||||
---
|
||||
|
||||
#### 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. `TestableMock`能否用于Android项目的测试?
|
||||
|
||||
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。
|
||||
|
||||
Android系统的`Dalvik`和`ART`虚拟机采用了与标准JVM不同的字节码体系,会影响`TestableMock`的正常工作。`Roboelectric`框架能在普通JVM虚拟机上运行Android单元测试,其速度比通过Android虚拟机运行单元测试快非常多,绝大多数Android App的单元测试都在使用`Roboelectric`框架。
|
||||
|
||||
#### 7. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
|
||||
|
||||
这个问题是由于系统ClassPath包含太多路径所致,与是否使用`TestableMock`无关。但需要注意的是,IntelliJ提供了两种辅助解决机制:`JAR manifest`和`classpath file`,若测试中使用了`TestableMock`,请选择`JAR manifest`。
|
||||
|
||||

|
||||
75
docs/zh-cn/doc/invoke-matcher.md
Normal file
75
docs/zh-cn/doc/invoke-matcher.md
Normal 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)` → 匹配符合指定表达式的值
|
||||
45
docs/zh-cn/doc/javaagent-args.md
Normal file
45
docs/zh-cn/doc/javaagent-args.md
Normal file
@@ -0,0 +1,45 @@
|
||||
全局运行参数
|
||||
---
|
||||
|
||||
`TestableMock`的许多功能采用了基于JavaAgent的运行时字节码修改实现。在JavaAgent启动时,可以通过额外的全局参数来调整`TestableMock`的执行过程。
|
||||
|
||||
### 可用参数清单
|
||||
|
||||
| 参数 | 描述 | 可用值和示例(`N/A`表示无需赋值) |
|
||||
| ---- | ---- | ---- |
|
||||
| logLevel | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
|
||||
| dumpPath | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 例如 `/tmp/bytecode`(需要此目录事先存在) |
|
||||
| pkgPrefix | 限定`TestableMock`仅对部分包生效 | 使用`,`分隔的包路径前缀列表,例如 `com.demo.svc,com.demo.dao` |
|
||||
| mockScope | 修改默认的Mock生效范围(详见[Mock生效范围](zh-cn/doc/scope-of-mock)) | 可用值为:`global`(全局生效) / `associated`(只对关联的测试用例生效) |
|
||||
| useThreadPool | 启用基于`TransmittableThreadLocal`的Mock上下文存储(用于包含线程池的测试用例) | `N/A` |
|
||||
|
||||
### 参数的连接
|
||||
|
||||
若参数有值,参数名和值之间用`=`符合连接。例如:
|
||||
|
||||
`useThreadPool`、`logLevel=debug`、`dumpPath=/tmp/debug`
|
||||
|
||||
多个参数之间使用`&`符号连接,例如:
|
||||
|
||||
`useThreadPool&logLevel=debug`、`logLevel=debug&dumpPath=/tmp/debug`
|
||||
|
||||
### 添加运行参数
|
||||
|
||||
与其他基于JavaAgent的工具相似,`TestableMock`通过在单元测试引入`testable-agent`包的末尾加上一个`=`符号,然后连接额外参数来传递用户的自定义参数。
|
||||
|
||||
对于Maven项目,可将参数追加到`maven-surefire-plugin`参数`testable-agent`包尾部,紧接着`.jar`的位置。例如:
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar=mockScope=associated&pkgPrefix=com.demo.</argLine>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
> 注意:在`xml`文件中,连接参数的`&`符号需要写为`&`
|
||||
|
||||
对于Gradle项目,同样是直接将参数追加到引入`testable-agent`的配置末尾。例如:
|
||||
|
||||
|
||||
```groovy
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}=mockScope=associated&pkgPrefix=com.demo."
|
||||
```
|
||||
63
docs/zh-cn/doc/mock-method-reusing.md
Normal file
63
docs/zh-cn/doc/mock-method-reusing.md
Normal file
@@ -0,0 +1,63 @@
|
||||
复用Mock类与方法
|
||||
---
|
||||
|
||||
Don't Repeat Yourself是软件开发过程当中的一项重要原则(即“DRY原则”),在编写测试代码时,有些通用的基础功能调用语句常常出现在许多相似的业务类里,若测试中需要Mock这些调用,就要在各个测试类中重复提供同样的Mock方法。而通过Mock方法的复用机制,能够很好的避免编写臃肿重复Mock代码的麻烦。
|
||||
|
||||
TestableMock支持两种粒度的Mock复用方式:<u>复用Mock类</u>和<u>复用Mock方法</u>。
|
||||
|
||||
## 复用Mock类
|
||||
|
||||
如果有两个或以上测试类需要Mock的方法近乎相同,那么采用类级别的Mock复用就是最省心的一种方式。
|
||||
|
||||
进行类级别的Mock复用,只需将Mock容器定义为独立的类,然后在要使用它的测试类上通过`@MockWith`进行引用。例如:
|
||||
|
||||
|
||||
```java
|
||||
@MockWith(ServiceMock.class)
|
||||
public class AaaServiceTest {
|
||||
...
|
||||
}
|
||||
|
||||
@MockWith(ServiceMock.class)
|
||||
public class BbbServiceTest {
|
||||
...
|
||||
}
|
||||
|
||||
public class ServiceMock {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
这样在`AaaServiceTest`和`BbbServiceTest`类中的测试用例在执行时,都会用`ServiceMock`容器类中定义的Mock方法进行调用匹配和Mock替换。
|
||||
|
||||
## 复用Mock方法
|
||||
|
||||
实际场景中,相比一次性复用整个Mock类的情况,更常见的是对部分高频Mock方法进行复用。
|
||||
|
||||
Mock方法的复用可以通过Mock容器类的继承来实现,父类中定义的所有Mock方法都会在子类中自然存在,例如:
|
||||
|
||||
```java
|
||||
public class AaaServiceTest {
|
||||
public static class Mock extends BasicMock {
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
public class BbbServiceTest {
|
||||
public static class Mock extends BasicMock {
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
public class BasicMock {
|
||||
@MockMethod(targetClass = UserDao.class)
|
||||
protected String getById(int id) {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
则名为`getById`的Mock方法在`AaaServiceTest`和`BbbServiceTest`的测试用例执行时都会生效。
|
||||
|
||||
6
docs/zh-cn/doc/parameter-constructor.md
Normal file
6
docs/zh-cn/doc/parameter-constructor.md
Normal file
@@ -0,0 +1,6 @@
|
||||
快速构造复杂入参
|
||||
---
|
||||
|
||||
不论被测方法所需的参数结构多么错综复杂、甚至没有合适的构造方法、甚至需要私有内部类对象... 呼唤TestableMock,马上递给您~
|
||||
|
||||
计划在`0.6`版本中推出。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user