mirror of
https://github.com/alibaba/testable-mock.git
synced 2026-08-23 11:43:29 +08:00
Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
b275f32e44 | ||
|
|
85276e1c55 | ||
|
|
c68289920f | ||
|
|
7e77f77fa2 | ||
|
|
40a1a999a7 | ||
|
|
7d2dd83410 | ||
|
|
43cd4aeeec | ||
|
|
525e252f96 | ||
|
|
f63b694b0c | ||
|
|
809b1aa57f | ||
|
|
f7dba2d33d | ||
|
|
36e260688e | ||
|
|
648e4aa9cc | ||
|
|
80499f0da2 | ||
|
|
d7ffbcbc32 | ||
|
|
e586d5db79 | ||
|
|
bf9ceeea0f | ||
|
|
73ef418f39 | ||
|
|
c6ea4c168d | ||
|
|
3f59e9e098 | ||
|
|
754d9ce08d | ||
|
|
72269fbcd8 | ||
|
|
6e47057d7f | ||
|
|
854c062e93 | ||
|
|
baf3c8d657 | ||
|
|
7e88813518 | ||
|
|
88fddf2bcc | ||
|
|
ba2a823c19 | ||
|
|
4b282179f4 | ||
|
|
6454ae547e | ||
|
|
5b7382e9b0 | ||
|
|
dc904496fc | ||
|
|
6f74ba771c | ||
|
|
df63bc5c74 | ||
|
|
da3eca3d49 | ||
|
|
d816726f4f | ||
|
|
4b1b1c3126 | ||
|
|
649fdebda7 | ||
|
|
24e4b4f317 | ||
|
|
0b9a9c7ac6 | ||
|
|
e33c208b8d | ||
|
|
afecf4ddf5 | ||
|
|
ae134ac5ce | ||
|
|
36136e878e | ||
|
|
0ef13a8e95 | ||
|
|
989a52a048 | ||
|
|
419a10669d | ||
|
|
2ad671e1f9 | ||
|
|
f9fc1e6224 |
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
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Alibaba
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
29
README.md
29
README.md
@@ -1,16 +1,20 @@
|
||||
# Testable
|
||||
# TestableMock
|
||||
|
||||
基于代码和字节码增强的Java单元测试辅助工具,包含以下功能:
|
||||
换种思路写Mock,让单元测试更简单。
|
||||
|
||||
- 使单元测试能直接调用和访问被测类的私有成员,解决私有方法无法测试的问题
|
||||
- 使被测类的任意方法调用快速替换为Mock,实现"指哪换哪",解决传统Mock工具使用繁琐的问题
|
||||
无需初始化,不挑测试框架,甭管要换的方法是被测类的私有方法、静态方法还是其他任何类的成员方法,也甭管要换的对象是怎么创建的。写好Mock方法,加个`@MockMethod`注解,一切统统搞定。
|
||||
|
||||
文档:https://alibaba.github.io/testable-mock/
|
||||
|
||||
## 目录结构
|
||||
|
||||
```bash
|
||||
|-- testable-core ➜ 核心组件,提供测试辅助功能、注解和工具类
|
||||
|-- 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语言的示例代码
|
||||
@@ -22,5 +26,14 @@
|
||||
主项目使用JDK 1.6+和Maven 3+版本构建,其中`demo`子项目需要JDK 1.8+版本。
|
||||
|
||||
```bash
|
||||
mvn clean package
|
||||
mvn clean install
|
||||
```
|
||||
|
||||
## 本地生成文档
|
||||
|
||||
```bash
|
||||
docsify serve docs
|
||||
```
|
||||
|
||||
> TestableMock文档使用`docsify`工具生成,构建前请安装[nodejs](https://nodejs.org/en/download/)运行时,并使用`npm install -g docsify`命令安装文档生成工具。
|
||||
|
||||
|
||||
22
demo/java-demo/build.gradle
Normal file
22
demo/java-demo/build.gradle
Normal file
@@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
group = 'com.alibaba.testable'
|
||||
version = '1.0.0-SNAPSHOT'
|
||||
sourceCompatibility = '8'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.2')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.2')
|
||||
}
|
||||
|
||||
test {
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -1,105 +1,87 @@
|
||||
<?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.2.0-SNAPSHOT</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.4.2</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>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-core</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>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<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>org.apache.maven.plugins</groupId>-->
|
||||
<!-- <artifactId>maven-surefire-plugin</artifactId>-->
|
||||
<!-- <configuration>-->
|
||||
<!-- <argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>-->
|
||||
<!-- </configuration>-->
|
||||
<!-- </plugin>-->
|
||||
<plugin>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare</id>
|
||||
<goals>
|
||||
<goal>prepare</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<dataFile>target/jacoco.exec</dataFile>
|
||||
<outputDirectory>target/jacoco-ut</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<!-- <plugin>-->
|
||||
<!-- <groupId>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,15 +0,0 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
public class BlackBox {
|
||||
|
||||
private String data;
|
||||
|
||||
public BlackBox(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public String callMe() {
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,61 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.model.Box;
|
||||
import com.alibaba.testable.demo.model.Color;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class DemoMatcher {
|
||||
|
||||
/**
|
||||
* Method to be mocked
|
||||
*/
|
||||
private void methodToBeMocked() {
|
||||
// pretend to have some code here
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be mocked
|
||||
*/
|
||||
private void methodToBeMocked(Object a1, Object a2) {
|
||||
// pretend to have some code here
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be mocked
|
||||
*/
|
||||
private void methodToBeMocked(Object[] a) {
|
||||
// pretend to have some code here
|
||||
}
|
||||
|
||||
public void callMethodWithoutArgument() {
|
||||
methodToBeMocked();
|
||||
}
|
||||
|
||||
public void callMethodWithNumberArguments() {
|
||||
// named variable and lambda variable will be recorded as different type
|
||||
// should have them both in test case
|
||||
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);
|
||||
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);
|
||||
methodToBeMocked(longArray);
|
||||
methodToBeMocked(new Double[]{1.0, 2.0});
|
||||
}
|
||||
|
||||
public void callMethodWithStringArgument() {
|
||||
methodToBeMocked("hello", "world");
|
||||
methodToBeMocked("testable", "mock");
|
||||
methodToBeMocked(new String[]{"demo"});
|
||||
}
|
||||
|
||||
public void callMethodWithObjectArgument() {
|
||||
methodToBeMocked(new BlackBox("hello"), new BlackBox("world"));
|
||||
methodToBeMocked(new BlackBox("demo"), null);
|
||||
methodToBeMocked(null, new BlackBox("demo"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,52 +1,45 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import sun.net.www.http.HttpClient;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class DemoService {
|
||||
|
||||
private int count;
|
||||
public class DemoMock {
|
||||
|
||||
/**
|
||||
* Target 1 - private method
|
||||
*/
|
||||
private String privateFunc(String s, int i) {
|
||||
return s + " - " + i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Target 2 - method with private field access
|
||||
*/
|
||||
public String privateFieldAccessFunc() {
|
||||
count += 2;
|
||||
return String.valueOf(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Target 3 - method with new operation
|
||||
* method with new operation
|
||||
*/
|
||||
public String newFunc() {
|
||||
BlackBox component = new BlackBox("something");
|
||||
return component.callMe();
|
||||
return component.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Target 4 - method with member method invoke
|
||||
* method with member method invoke
|
||||
*/
|
||||
public String outerFunc(String s) throws Exception {
|
||||
return "{ \"res\": \"" + innerFunc(s) + "\"}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Target 5 - method with common method invoke
|
||||
* method with common method invoke
|
||||
*/
|
||||
public String commonFunc() {
|
||||
return "anything".trim() + "__" + "anything".substring(1, 2) + "__" + "abc".startsWith("ab");
|
||||
}
|
||||
|
||||
/**
|
||||
* method with static method invoke
|
||||
*/
|
||||
public BlackBox getBox() {
|
||||
return BlackBox.secretBox();
|
||||
}
|
||||
|
||||
/**
|
||||
* two methods invoke same private method
|
||||
*/
|
||||
public String callerOne() {
|
||||
return callFromDifferentMethod();
|
||||
}
|
||||
@@ -56,7 +49,7 @@ public class DemoService {
|
||||
}
|
||||
|
||||
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,35 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
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
|
||||
*/
|
||||
private static String privateStaticFunc(String s, int i) {
|
||||
return s + " + " + i;
|
||||
}
|
||||
|
||||
/**
|
||||
* private member method
|
||||
*/
|
||||
private String privateFunc(String s, int i) {
|
||||
return s + " - " + i;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
|
||||
public class BlackBox extends Box implements Color {
|
||||
|
||||
public BlackBox(String data) {
|
||||
this.data = 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.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.model;
|
||||
|
||||
public interface Color {
|
||||
|
||||
String getColor();
|
||||
|
||||
}
|
||||
@@ -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,93 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.model.Box;
|
||||
import com.alibaba.testable.demo.model.Color;
|
||||
import org.junit.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();
|
||||
|
||||
@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,84 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.core.error.VerifyFailedError;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import org.junit.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();
|
||||
|
||||
@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,110 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.TEST_CASE;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示基本的Mock功能
|
||||
* Demonstrate basic mock functionality
|
||||
*/
|
||||
class DemoMockTest {
|
||||
|
||||
private DemoMock demoMock = new DemoMock();
|
||||
|
||||
@MockConstructor
|
||||
private BlackBox createBlackBox(String text) {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private String innerFunc(DemoMock self, String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private String trim(String self) {
|
||||
return "trim_string";
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "substring")
|
||||
private String sub(String self, int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private boolean startsWith(String self, String s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private BlackBox secretBox(BlackBox ignore) {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private String callFromDifferentMethod(DemoMock 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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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\"}", demoMock.outerFunc("hello"));
|
||||
verify("innerFunc").with("hello");
|
||||
}
|
||||
|
||||
@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_get_test_case_name() throws Exception {
|
||||
// synchronous
|
||||
assertEquals("mock_special", demoMock.callerOne());
|
||||
// asynchronous
|
||||
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoMock.callerOne()).get());
|
||||
verify("callFromDifferentMethod").withTimes(2);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
*/
|
||||
@EnablePrivateAccess
|
||||
class DemoPrivateAccessTest {
|
||||
|
||||
private DemoPrivateAccess demoPrivateAccess = new DemoPrivateAccess();
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_method() {
|
||||
assertEquals("hello - 1", demoPrivateAccess.privateFunc("hello", 1));
|
||||
assertEquals("hello - 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", "hello", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_field() {
|
||||
demoPrivateAccess.count = 2;
|
||||
assertEquals(new Integer(2), demoPrivateAccess.count);
|
||||
|
||||
PrivateAccessor.set(demoPrivateAccess, "count", 3);
|
||||
assertEquals(new Integer(3), PrivateAccessor.get(demoPrivateAccess, "count"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_static_method() {
|
||||
//assertEquals("hello + 1", DemoPrivateAccess.privateStaticFunc("hello", 1));
|
||||
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFunc", "hello", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_static_field() {
|
||||
//DemoPrivateAccess.staticCount = 2;
|
||||
//assertEquals(new Integer(2), DemoPrivateAccess.staticCount);
|
||||
|
||||
PrivateAccessor.setStatic(DemoPrivateAccess.class, "staticCount", 3);
|
||||
assertEquals(new Integer(3), PrivateAccessor.getStatic(DemoPrivateAccess.class, "staticCount"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_update_final_field() {
|
||||
demoPrivateAccess.pi = 4.13;
|
||||
assertEquals(4.13, demoPrivateAccess.pi);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import com.alibaba.testable.core.annotation.EnablePrivateAccess;
|
||||
import com.alibaba.testable.core.annotation.TestableMock;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static com.alibaba.testable.core.tool.TestableTool.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@EnablePrivateAccess
|
||||
class DemoServiceTest {
|
||||
|
||||
@TestableMock(targetMethod = CONSTRUCTOR)
|
||||
private BlackBox createBlackBox(String text) {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
private String innerFunc(DemoService 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 String callFromDifferentMethod(DemoService 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 DemoService demoService = new DemoService();
|
||||
|
||||
@Test
|
||||
void should_able_to_test_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_test_private_field() throws Exception {
|
||||
demoService.count = 2;
|
||||
assertEquals("4", demoService.privateFieldAccessFunc());
|
||||
PrivateAccessor.set(demoService, "count", 3);
|
||||
assertEquals("5", demoService.privateFieldAccessFunc());
|
||||
assertEquals(new Integer(5), PrivateAccessor.get(demoService, "count"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_test_new_object() throws Exception {
|
||||
assertEquals("mock_something", demoService.newFunc());
|
||||
verify("createBlackBox").times(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_test_member_method() throws Exception {
|
||||
assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello"));
|
||||
verify("innerFunc").times(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_test_common_method() throws Exception {
|
||||
assertEquals("trim_string__sub_string__false", demoService.commonFunc());
|
||||
verify("trim").times(1);
|
||||
verify("sub").times(1);
|
||||
verify("startsWith").times(1);
|
||||
}
|
||||
|
||||
@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").times(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").times(2);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示模板方法可以被Mock
|
||||
* Demonstrate template method can be mocked
|
||||
*/
|
||||
class DemoTemplateTest {
|
||||
|
||||
private DemoTemplate demoTemplate = new DemoTemplate();
|
||||
|
||||
/* 第一种写法:使用泛型定义 */
|
||||
/* 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());
|
||||
}
|
||||
|
||||
}
|
||||
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.4.2")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.2")
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -2,69 +2,55 @@
|
||||
<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>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>kotlin-demo</name>
|
||||
<description>Demo project for Spring Boot</description>
|
||||
<description>Demo project for TestableMock</description>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<kotlin.version>1.3.72</kotlin.version>
|
||||
<testable.version>0.2.0-SNAPSHOT</testable.version>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.4.2</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>
|
||||
<version>${kotlin.version}</version>
|
||||
</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>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-core</artifactId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<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>
|
||||
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
</plugin>
|
||||
<!-- <plugin>-->
|
||||
<!-- <groupId>org.apache.maven.plugins</groupId>-->
|
||||
@@ -86,25 +72,6 @@
|
||||
</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>
|
||||
@@ -129,6 +96,27 @@
|
||||
</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>
|
||||
|
||||
|
||||
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,22 +0,0 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
|
||||
class BlackBox(private val data: String) {
|
||||
|
||||
fun callMe(): 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,62 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.Box
|
||||
import com.alibaba.testable.demo.model.Color
|
||||
|
||||
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,78 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
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"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.ColorBox
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
class DemoMock {
|
||||
|
||||
/**
|
||||
* method with new operation
|
||||
*/
|
||||
fun newFunc(): String? {
|
||||
return BlackBox("something").get()
|
||||
}
|
||||
|
||||
/**
|
||||
* method with member method invoke
|
||||
*/
|
||||
fun outerFunc(s: String): String {
|
||||
return "{ \"res\": \"" + innerFunc(s) + "\"}"
|
||||
}
|
||||
|
||||
/**
|
||||
* method with common method invoke
|
||||
*/
|
||||
fun commonFunc(): String {
|
||||
val box = BlackBox("anything")
|
||||
return box.trim() + "__" + box.substring(1, 2) + "__" + box.startsWith("any")
|
||||
}
|
||||
|
||||
/**
|
||||
* method with static method invoke
|
||||
*/
|
||||
fun getBox(): BlackBox {
|
||||
return ColorBox.createBox("Red", BlackBox.secretBox())
|
||||
}
|
||||
|
||||
/**
|
||||
* two methods invoke same private method
|
||||
*/
|
||||
fun callerOne(): String {
|
||||
return callFromDifferentMethod()
|
||||
}
|
||||
|
||||
fun callerTwo(): String {
|
||||
return callFromDifferentMethod()
|
||||
}
|
||||
|
||||
private fun innerFunc(s: String): String {
|
||||
return Files.readAllLines(Paths.get("/a-not-exist-file")).joinToString()
|
||||
}
|
||||
|
||||
private fun callFromDifferentMethod() = "realOne"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
class DemoPrivateAccess {
|
||||
|
||||
/**
|
||||
* a private member field
|
||||
*/
|
||||
private var count = 0
|
||||
|
||||
/**
|
||||
* a constant field
|
||||
*/
|
||||
val pi = 3.14
|
||||
|
||||
/**
|
||||
* private member method
|
||||
*/
|
||||
private fun privateFunc(s: String, i: Int): String {
|
||||
return "$s - $i"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* a private static field
|
||||
*/
|
||||
private var staticCount = 0
|
||||
|
||||
/**
|
||||
* private static method
|
||||
*/
|
||||
private fun privateStaticFunc(s: String, i: Int): String {
|
||||
return "$s + $i"
|
||||
}
|
||||
|
||||
/**
|
||||
* private jvm static method
|
||||
*/
|
||||
@JvmStatic private fun privateJvmStaticFunc(s: String, i: Int): String {
|
||||
return "$s * $i"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import sun.net.www.http.HttpClient
|
||||
import java.net.URL
|
||||
|
||||
|
||||
@Service
|
||||
class DemoService {
|
||||
|
||||
private var count = 0
|
||||
|
||||
/**
|
||||
* Target 1 - private method
|
||||
*/
|
||||
private fun privateFunc(s: String, i: Int): String {
|
||||
return "$s - $i"
|
||||
}
|
||||
|
||||
/**
|
||||
* Target 2 - method with private field access
|
||||
*/
|
||||
fun privateFieldAccessFunc(): String {
|
||||
count += 2
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Target 3 - method with new operation
|
||||
*/
|
||||
fun newFunc(): String {
|
||||
return BlackBox("something").callMe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Target 4 - method with member method invoke
|
||||
*/
|
||||
fun outerFunc(s: String): String {
|
||||
return "{ \"res\": \"" + innerFunc(s) + "\"}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Target 5 - method with common method invoke
|
||||
*/
|
||||
fun commonFunc(): String {
|
||||
val box = BlackBox("anything")
|
||||
return box.trim() + "__" + box.substring(1, 2) + "__" + box.startsWith("any")
|
||||
}
|
||||
|
||||
fun callerOne(): String {
|
||||
return callFromDifferentMethod()
|
||||
}
|
||||
|
||||
fun callerTwo(): String {
|
||||
return callFromDifferentMethod()
|
||||
}
|
||||
|
||||
private fun innerFunc(s: String) = HttpClient.New(URL("http:/xxx/$s")).urlFile
|
||||
|
||||
private fun callFromDifferentMethod() = "realOne"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import java.util.ArrayList
|
||||
import java.util.HashMap
|
||||
import java.util.HashSet
|
||||
|
||||
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.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.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.model
|
||||
|
||||
interface Color {
|
||||
|
||||
val color: String
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.alibaba.testable.demo.util
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
object PathUtil {
|
||||
|
||||
fun deleteRecursively(file: File) {
|
||||
if (!file.exists()) {
|
||||
return
|
||||
}
|
||||
val fileList = file.listFiles()
|
||||
if (fileList != null) {
|
||||
for (childFile in fileList) {
|
||||
if (childFile.isDirectory) {
|
||||
deleteRecursively(childFile)
|
||||
} else if (!childFile.delete()) {
|
||||
throw IOException()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw IOException("Unable to delete file " + file.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,91 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.Box
|
||||
import com.alibaba.testable.demo.model.Color
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
|
||||
*/
|
||||
internal class DemoInheritTest {
|
||||
|
||||
private val demoInherit = DemoInherit()
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_box(self: Box, something: String) {
|
||||
self.put("put_" + something + "_into_box")
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_blackbox(self: BlackBox, something: String) {
|
||||
self.put("put_" + something + "_into_blackbox")
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_box(self: Box): String {
|
||||
return "get_from_box"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_blackbox(self: BlackBox): String {
|
||||
return "get_from_blackbox"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_color(self: Color): String {
|
||||
return "color_from_color"
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_blackbox(self: BlackBox): String {
|
||||
return "color_from_blackbox"
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_sub_object_method_by_parent_object() {
|
||||
val box = demoInherit.putIntoBox() as BlackBox
|
||||
InvokeVerifier.verify("put_into_box").withTimes(1)
|
||||
Assertions.assertEquals("put_data_into_box", box.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_sub_object_method_by_sub_object() {
|
||||
val box = demoInherit.putIntoBlackBox()
|
||||
InvokeVerifier.verify("put_into_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("put_data_into_blackbox", box.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_parent_object_method_by_parent_object() {
|
||||
val content = demoInherit.fromBox
|
||||
InvokeVerifier.verify("get_from_box").withTimes(1)
|
||||
Assertions.assertEquals("get_from_box", content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_parent_object_method_by_sub_object() {
|
||||
val content = demoInherit.fromBlackBox
|
||||
InvokeVerifier.verify("get_from_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("get_from_blackbox", content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_interface_method_by_interface_object() {
|
||||
val color = demoInherit.colorViaColor
|
||||
InvokeVerifier.verify("get_color_from_color").withTimes(1)
|
||||
Assertions.assertEquals("color_from_color", color)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_call_interface_method_by_sub_class_object() {
|
||||
val color = demoInherit.colorViaBox
|
||||
InvokeVerifier.verify("get_color_from_blackbox").withTimes(1)
|
||||
Assertions.assertEquals("color_from_blackbox", color)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.error.VerifyFailedError
|
||||
import com.alibaba.testable.core.matcher.InvokeMatcher
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* 演示Mock方法调用校验器
|
||||
* Demonstrate mock method invocation verifier
|
||||
*/
|
||||
internal class DemoMatcherTest {
|
||||
|
||||
private val demoMatcher = DemoMatcher()
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithoutArgument(self: DemoMatcher) {
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArguments(self: DemoMatcher, a1: Any, a2: Any) {
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArrayArgument(self: DemoMatcher, a: Array<Any>) {
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun should_match_no_argument() {
|
||||
demoMatcher.callMethodWithoutArgument()
|
||||
InvokeVerifier.verify("methodWithoutArgument").withTimes(1)
|
||||
demoMatcher.callMethodWithoutArgument()
|
||||
InvokeVerifier.verify("methodWithoutArgument").withTimes(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_match_number_arguments() {
|
||||
demoMatcher.callMethodWithNumberArguments()
|
||||
InvokeVerifier.verify("methodWithArguments").without(InvokeMatcher.anyString(), 2)
|
||||
InvokeVerifier.verify("methodWithArguments").withInOrder(InvokeMatcher.anyInt(), 2)
|
||||
InvokeVerifier.verify("methodWithArguments").withInOrder(InvokeMatcher.anyLong(), InvokeMatcher.anyNumber())
|
||||
// Note: Must use `::class.javaObjectType` for primary types check in Kotlin
|
||||
InvokeVerifier.verify("methodWithArguments").with(1.0, InvokeMatcher.anyMapOf(Int::class.javaObjectType, Float::class.javaObjectType)).times(2)
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyList(), InvokeMatcher.anySetOf(Float::class.javaObjectType)).times(2)
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyList(), InvokeMatcher.anyListOf(Float::class.javaObjectType))
|
||||
InvokeVerifier.verify("methodWithArrayArgument").with(InvokeMatcher.anyArrayOf(Long::class.javaObjectType))
|
||||
InvokeVerifier.verify("methodWithArrayArgument").with(InvokeMatcher.anyArray())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_match_string_arguments() {
|
||||
demoMatcher.callMethodWithStringArgument()
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.startsWith("he"), InvokeMatcher.endsWith("ld"))
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.contains("stab"), InvokeMatcher.matches("m.[cd]k"))
|
||||
InvokeVerifier.verify("methodWithArrayArgument").with(InvokeMatcher.anyArrayOf(String::class.java))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_match_object_arguments() {
|
||||
demoMatcher.callMethodWithObjectArgument()
|
||||
InvokeVerifier.verify("methodWithArguments").withInOrder(InvokeMatcher.any(BlackBox::class.java), InvokeMatcher.any(BlackBox::class.java))
|
||||
InvokeVerifier.verify("methodWithArguments").withInOrder(InvokeMatcher.nullable(BlackBox::class.java), InvokeMatcher.nullable(BlackBox::class.java))
|
||||
InvokeVerifier.verify("methodWithArguments").withInOrder(InvokeMatcher.isNull(), InvokeMatcher.notNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_match_with_times() {
|
||||
demoMatcher.callMethodWithNumberArguments()
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(4)
|
||||
|
||||
demoMatcher.callMethodWithNumberArguments()
|
||||
var gotError = false
|
||||
try {
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(5)
|
||||
} catch (e: VerifyFailedError) {
|
||||
gotError = true
|
||||
}
|
||||
if (!gotError) {
|
||||
Assertions.fail<Any>()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD
|
||||
import com.alibaba.testable.core.tool.TestableTool.TEST_CASE
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.ColorBox
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* 演示基本的Mock功能
|
||||
* Demonstrate basic mock functionality
|
||||
*/
|
||||
internal class DemoMockTest {
|
||||
|
||||
private val demoMock = DemoMock()
|
||||
|
||||
@MockConstructor
|
||||
private fun createBlackBox(text: String) = BlackBox("mock_$text")
|
||||
|
||||
@MockMethod
|
||||
private fun innerFunc(self: DemoMock, text: String) = "mock_$text"
|
||||
|
||||
@MockMethod
|
||||
private fun trim(self: BlackBox) = "trim_string"
|
||||
|
||||
@MockMethod(targetMethod = "substring")
|
||||
private fun sub(self: BlackBox, i: Int, j: Int) = "sub_string"
|
||||
|
||||
@MockMethod
|
||||
private fun startsWith(self: BlackBox, s: String) = false
|
||||
|
||||
@MockMethod
|
||||
private fun secretBox(ignore: BlackBox): BlackBox {
|
||||
return BlackBox("not_secret_box")
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private fun createBox(ignore: ColorBox, color: String, box: BlackBox): BlackBox {
|
||||
return BlackBox("White_${box.get()}")
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private fun callFromDifferentMethod(self: DemoMock): String {
|
||||
return if (TEST_CASE == "should_able_to_get_test_case_name") {
|
||||
"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\"}", demoMock.outerFunc("hello"))
|
||||
verify("innerFunc").with("hello")
|
||||
}
|
||||
|
||||
@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() {
|
||||
// synchronous
|
||||
assertEquals("mock_special", demoMock.callerOne())
|
||||
// asynchronous
|
||||
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit<String> {
|
||||
demoMock.callerOne()
|
||||
}.get())
|
||||
verify("callFromDifferentMethod").withTimes(2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
*/
|
||||
internal class DemoPrivateAccessTest {
|
||||
|
||||
private val demoPrivateAccess = DemoPrivateAccess()
|
||||
|
||||
@Test
|
||||
fun should_able_to_access_private_method() {
|
||||
assertEquals("hello - 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", "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() {
|
||||
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateStaticFunc", "hello", 1))
|
||||
assertEquals("hello * 1", PrivateAccessor.invokeStatic(DemoPrivateAccess::class.java, "privateJvmStaticFunc", "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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor
|
||||
import com.alibaba.testable.core.annotation.EnablePrivateAccess
|
||||
import com.alibaba.testable.core.annotation.TestableMock
|
||||
import com.alibaba.testable.core.tool.TestableTool.*
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
@EnablePrivateAccess
|
||||
internal class DemoServiceTest {
|
||||
|
||||
@TestableMock(targetMethod = CONSTRUCTOR)
|
||||
private fun createBlackBox(text: String) = BlackBox("mock_$text")
|
||||
|
||||
@TestableMock
|
||||
private fun innerFunc(self: DemoService, 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 callFromDifferentMethod(self: DemoService): 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 = DemoService()
|
||||
|
||||
@Test
|
||||
fun should_able_to_test_private_method() {
|
||||
assertEquals("hello - 1", PrivateAccessor.invoke(demoService, "privateFunc", "hello", 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_test_private_field() {
|
||||
PrivateAccessor.set(demoService, "count", 3)
|
||||
assertEquals("5", demoService.privateFieldAccessFunc())
|
||||
assertEquals(5, PrivateAccessor.get(demoService, "count"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_test_new_object() {
|
||||
assertEquals("mock_something", demoService.newFunc())
|
||||
verify("createBlackBox").times(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_test_member_method() {
|
||||
assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello"))
|
||||
verify("innerFunc").times(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_test_common_method() {
|
||||
assertEquals("trim_string__sub_string__false", demoService.commonFunc())
|
||||
verify("trim").times(1)
|
||||
verify("sub").times(1)
|
||||
verify("startsWith").times(1)
|
||||
}
|
||||
|
||||
@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").times(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").times(2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 演示模板方法可以被Mock
|
||||
* Demonstrate template method can be mocked
|
||||
*/
|
||||
internal class DemoTemplateTest {
|
||||
|
||||
private val demoTemplate = DemoTemplate()
|
||||
|
||||
@MockMethod
|
||||
private fun <T> getList(self: DemoTemplate, value: T): List<T> {
|
||||
return mutableListOf((value.toString() + "_mock_list") as T)
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private fun <K, V> getMap(self: DemoTemplate, key: K, value: V): Map<K, V> {
|
||||
return mutableMapOf(key to (value.toString() + "_mock_map") as V)
|
||||
}
|
||||
|
||||
@MockConstructor
|
||||
private fun newHashSet(): HashSet<*> {
|
||||
val set = HashSet<Any>()
|
||||
set.add("insert_mock")
|
||||
return set
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private fun <E> add(s: MutableSet<E>, e: E): Boolean {
|
||||
s.add((e.toString() + "_mocked") as E)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_single_template_method() {
|
||||
val res = demoTemplate.singleTemplateMethod()
|
||||
Assertions.assertEquals("demo_mock_list", res)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_double_template_method() {
|
||||
val res = demoTemplate.doubleTemplateMethod()
|
||||
Assertions.assertEquals("testable_mock_map", res)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_new_template_method() {
|
||||
val res = demoTemplate.newTemplateMethod()
|
||||
Assertions.assertEquals(2, res.size)
|
||||
val iterator = res.stream().iterator()
|
||||
Assertions.assertEquals("insert_mock", iterator.next())
|
||||
Assertions.assertEquals("world_mocked", iterator.next())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.alibaba.testable.demo.util
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
|
||||
class PathUtilTest {
|
||||
|
||||
@MockMethod
|
||||
fun exists(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> true
|
||||
"/a/b/c" -> true
|
||||
else -> f.exists()
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun isDirectory(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b/c" -> true
|
||||
else -> f.isDirectory
|
||||
}
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun delete(f: File): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
fun listFiles(f: File): Array<File>? {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> arrayOf(File("/a/b/c"), File("/a/b/d"))
|
||||
"/a/b/c" -> arrayOf(File("/a/b/c/e"))
|
||||
else -> f.listFiles()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_java_method_invoke_in_kotlin() {
|
||||
PathUtil.deleteRecursively(File("/a/b/"))
|
||||
verify("listFiles").withTimes(2)
|
||||
verify("delete").withTimes(4)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
使用说明
|
||||
---
|
||||
|
||||
## 引入Testable
|
||||
|
||||
首先在项目`pom.xml`文件中添加`testable-core`依赖:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-core</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
此时项目就获得了在单元测试中随意访问被测类私有字段和方法的能力(需配合注解使用,见下文详述)。
|
||||
|
||||
若要开启极速Mock功能,还需在`pom.xml`里加上`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
|
||||
|
||||
`Testable`目前能为测试类提供两项增强能力:__直接访问被测类的私有成员__ 和 __极速Mock被测方法中的调用__
|
||||
|
||||
### 访问私有成员字段和方法
|
||||
|
||||
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
|
||||
|
||||
- 调用被测类的私有方法
|
||||
- 读取被测类的私有成员
|
||||
- 修改被测类的私有成员
|
||||
- 修改被测类的常量成员(使用final或static final修饰的成员)
|
||||
|
||||
访问和修改私有、常量成员时,IDE可能会提示语法有误,但编译器将能够正常运行测试。
|
||||
|
||||
若不希望看到IDE的语法错误提醒,或是在基于JVM的非Java语言项目里(譬如Kotlin语言),也可以借助`PrivateAccessor`工具类来实现私有成员的访问。
|
||||
|
||||
效果见示例项目文件`DemoServiceTest.java`中的`should_able_to_test_private_method()`和`should_able_to_test_private_field()`测试用例。
|
||||
|
||||
### Mock被测类的任意方法调用
|
||||
|
||||
**【1】覆写任意类的方法调用**
|
||||
|
||||
在测试类里定义一个有`@TestableMock`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
|
||||
|
||||
此时被测类中所有对该需覆写方法的调用,将在单元测试运行时,将自动被替换为对上述自定义Mock方法的调用。
|
||||
|
||||
**注意**:当遇到有两个需覆写的方法重名时,可将需覆写的方法名写到`@TestableMock`注解的`targetMethod`参数里,此时Mock方法自身就可以随意命名了。
|
||||
|
||||
示例项目文件`DemoServiceTest.java`中的`should_able_to_test_common_method()`用例详细展示了这种用法。
|
||||
|
||||
**【2】覆写被测类自身的成员方法**
|
||||
|
||||
有时候,在对某些方法进行测试时,希望将被测类自身的另外一些成员方法Mock掉。
|
||||
|
||||
操作方法与前一种情况相同,Mock方法的第一个参数类型需与被测类相同,即可实现对被测类自身(不论是公有或私有)成员方法的覆写。
|
||||
|
||||
详见示例项目文件`DemoServiceTest.java`中的`should_able_to_test_member_method()`用例。
|
||||
|
||||
**【3】覆写任意类的new操作**
|
||||
|
||||
在测试类里定义一个有`@TestableMock`注解的普通方法,将注解的`targetMethod`参数写为"<init>",然后使该方法与要被创建类型的构造函数参数、返回值类型完全一致,方法名称随意。
|
||||
|
||||
此时被测类中所有用`new`创建指定类的操作(并使用了与Mock方法参数一致的构造函数)将被替换为对该自定义方法的调用。
|
||||
|
||||
详见示例项目文件`DemoServiceTest.java`中的`should_able_to_test_new_object()`用例。
|
||||
|
||||
**【4】识别当前测试用例和调用来源**
|
||||
|
||||
在Mock方法中可以通过`TestableTool.TEST_CASE`和`TestableTool.SOURCE_METHOD`来识别**当前运行的测试用例名称**和**进入该Mock方法前的被测类方法名称**,从而区分处理不同的调用场景。
|
||||
|
||||
详见示例项目文件`DemoServiceTest.java`中的`should_able_to_get_source_method_name()`和`should_able_to_get_test_case_name()`用例。
|
||||
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://devops.aliyun.com" 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>
|
||||
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工具**。
|
||||
|
||||

|
||||
15
docs/zh-cn/doc/comparation.md
Normal file
15
docs/zh-cn/doc/comparation.md
Normal file
@@ -0,0 +1,15 @@
|
||||
主流Mock工具对比
|
||||
---
|
||||
|
||||
除`TestableMock`外,目前主要的Mock工具主要有`Mockito`、`PowerMock`和`JMockit`,基本差异如下:
|
||||
|
||||
| 工具 | 原理 | 最小Mock单元 | 对被Mock方法的限制 | 上手难度 | IDE支持 |
|
||||
| ---- | ---- | ---- | ---- | ---- | ---- |
|
||||
| Mockito | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | **较容易** | **很好** |
|
||||
| PowerMock | 自定义类加载器 | 类 | **任何方法皆可** | 较繁琐 | **较好** |
|
||||
| JMockit | 运行时字节码修改 | 类 | 不能Mock构造方法(new操作符) | 较繁琐 | 一般 |
|
||||
| TestableMock | 运行时字节码修改 | 方法 | **任何方法皆可** | **很容易** | 一般 |
|
||||
|
||||
相比之下,`TestabledMock`的功能与`PowerMock`基本平齐,且极易上手,只需掌握`@MockMethod`注解就可以完成绝大多数任务。
|
||||
|
||||
当前`TestableMock`的主要不足在于,编写Mock方法时IDE尚无法即时提示方法参数是否正确匹配。若发现匹配效果不符合预期,需要通过[自助问题排查](zh-cn/doc/troubleshooting.md)文档提供的方法在运行期进行校验。这个功能未来需要通过扩展主流IDE插件来提供。
|
||||
42
docs/zh-cn/doc/frequency-asked-questions.md
Normal file
42
docs/zh-cn/doc/frequency-asked-questions.md
Normal file
@@ -0,0 +1,42 @@
|
||||
常见使用问题
|
||||
---
|
||||
|
||||
#### 1. 如何Mock被测类中通过`@Autowired`初始化的字段?
|
||||
|
||||
直接创建被测类对象,然后利用`TestableMock`访问私有成员的能力直接给这些字段赋值即可。
|
||||
|
||||
#### 2. `TestableMock`是否能够与其他Mock工具一起使用?
|
||||
|
||||
`TestableMock`可与其他基于动态代理机制的Mock工具安全的共同使用,譬如`Mockito`、`EasyMock`、`MockRunner`等皆属此范畴。
|
||||
|
||||
对于其他会修改类加载器或被测类字节码的Mock工具,譬如`PowerMock`和`JMockit`,尚无案例证明会与`TestableMock`发生冲突,但从原理来说二者可能存在不兼容风险,请谨慎使用。
|
||||
|
||||
#### 3. 父类变量指向子类对象时,如何实现Mock方法?
|
||||
|
||||
在代码中,经常会有使用<u>接口变量或父类变量</u>指向子类实例,调用父类或子类方法的情况。
|
||||
|
||||
这时候遵循一个原则,Mock方法的首个参数类型**始终与发起调用的变量类型一致**。
|
||||
|
||||
因此,不论实际被调用方法来自父类还是子类,也不论子类是否覆写该方法。若变量为父类型(或接口类型),则Mock方法的首个参数类型都应该使用相同的父类(或接口)类型。
|
||||
|
||||
参见Java和Kotlin示例中`DemoInheritTest`测试类的用例。
|
||||
|
||||
#### 4. 如何Mock对于泛型方法(模板方法)?
|
||||
|
||||
与普通方法的Mock方法相同,直接在Mock方法上使用相同的泛型参数即可。
|
||||
|
||||
参见Java和Kotlin示例中`DemoTemplateTest`测试类的用例。
|
||||
|
||||
不过,由于JVM存在泛型擦除机制,对于Java项目也可以直接使用`Object`类型替代泛型参数,见Java版`DemoTemplateTest`测试类中被注释掉的"第二种写法"示例。
|
||||
|
||||
#### 5. 在Kotlin项目对`String`类中的方法进行Mock不生效?
|
||||
|
||||
Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.String`。但在构建生成自字节码的时候又会被替换为Java的`java.lang.String`类,因此无论将Mock目标写为`kotlin.String`或`java.lang.String`均无法正常匹配到原始的被调用方法。
|
||||
|
||||
实际场景中需要对`String`类中的方法进行Mock的场景很少,`TestableMock`暂未对这种情况做特别处理。
|
||||
|
||||
#### 6. `TestableMock`能否用于Android项目的测试?
|
||||
|
||||
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。
|
||||
|
||||
Android系统的`Dalvik`和`ART`虚拟机采用了与标准JVM不同的字节码体系,会影响`TestableMock`的正常工作。`Roboelectric`框架能在普通JVM虚拟机上运行Android单元测试,其速度比通过Android虚拟机运行单元测试快非常多,绝大多数Android App的单元测试都在使用`Roboelectric`框架。
|
||||
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)` → 匹配符合指定表达式的值
|
||||
34
docs/zh-cn/doc/private-accessor.md
Normal file
34
docs/zh-cn/doc/private-accessor.md
Normal file
@@ -0,0 +1,34 @@
|
||||
访问私有成员字段和方法
|
||||
---
|
||||
|
||||
如今关于私有方法是否应该做单元测试的争论正逐渐消停,开发者的普遍实践已经给出事实答案。通过公有方法间接测私有方法在很多情况下难以进行,开发者们更愿意通过修改方法可见性的办法来让原本私有的方法在测试用例中变得可测。
|
||||
|
||||
此外,在单元测试中时常会需要对被测对象进行特定的成员字段初始化,但有时由于被测类的构造方法限制,使得无法便捷的对这些字段进行赋值。那么,能否在不破坏被测类型封装的情况下,允许单元测试用例内的代码直接访问被测类的私有方法和成员变量呢?TestableMock提供了两种简单的解决方案。
|
||||
|
||||
### 方法一:使用`@EnablePrivateAccess`注解
|
||||
|
||||
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
|
||||
|
||||
- 调用被测类的私有方法
|
||||
- 读取被测类的私有成员
|
||||
- 修改被测类的私有成员
|
||||
- 修改被测类的常量成员(使用final修饰的成员)
|
||||
|
||||
访问和修改私有、常量成员时,IDE可能会提示语法有误,但编译器将能够正常运行测试。(使用编译期代码增强,目前仅实现了Java语言的适配)
|
||||
|
||||
效果见`java-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
|
||||
|
||||
### 方法二:使用`PrivateAccessor`工具类
|
||||
|
||||
若不希望看到IDE的语法错误提醒,或是在非Java语言的JVM工程(譬如Kotlin语言)里,也可以借助`PrivateAccessor`工具类来直接访问私有成员。
|
||||
|
||||
这个类提供了6个静态方法:
|
||||
|
||||
- `PrivateAccessor.get(被测对象, "私有字段名")` ➜ 读取被测类的私有成员
|
||||
- `PrivateAccessor.set(被测对象, "私有字段名", 新的值)` ➜ 修改被测类的私有成员
|
||||
- `PrivateAccessor.invoke(被测对象, "私有方法名", 调用参数..)` ➜ 调用被测类的私有方法
|
||||
- `PrivateAccessor.getStatic(被测类型, "私有字段名")` ➜ 读取被测类的**静态**私有成员
|
||||
- `PrivateAccessor.setStatic(被测类型, "私有字段名", 新的值)` ➜ 修改被测类的**静态**私有成员
|
||||
- `PrivateAccessor.invokeStatic(被测类型, "私有方法名", 调用参数..)` ➜ 调用被测类的**静态**私有方法
|
||||
|
||||
详见`java-demo`和`kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
|
||||
@@ -1,5 +1,39 @@
|
||||
# Release Note
|
||||
|
||||
## 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 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 grable 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 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
|
||||
78
docs/zh-cn/doc/setup.md
Normal file
78
docs/zh-cn/doc/setup.md
Normal file
@@ -0,0 +1,78 @@
|
||||
使用TestableMock
|
||||
---
|
||||
|
||||
`TestableMock`是基于源码和字节码增强的Java单元测试辅助工具,包含以下功能:
|
||||
|
||||
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
|
||||
- [快速Mock任意调用](zh-cn/doc/use-mock.md):使被测类的任意方法调用快速替换为Mock方法,实现"指哪换哪",解决传统Mock工具使用繁琐的问题
|
||||
- [辅助测试void方法](zh-cn/doc/test-void-method.md):利用Mock校验器对方法的内部逻辑进行检查,解决无返回值方法难以实施单元测试的问题
|
||||
|
||||
## 在Maven项目中使用
|
||||
|
||||
在项目`pom.xml`文件中,增加`testable-all`依赖和`maven-surefire-plugin`配置,具体方法如下。
|
||||
|
||||
建议先添加一个标识TestableMock版本的`property`,便于统一管理:
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.4.2</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
在`dependencies`列表添加TestableMock依赖:
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
最后在`build`区域的`plugins`列表里添加`maven-surefire-plugin`插件(如果已包含此插件则只需添加`<argLine>`部分配置):
|
||||
|
||||
```xml
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
```
|
||||
|
||||
若项目同时还使用了`Jacoco`的`on-the-fly`模式(默认模式)统计单元测试覆盖率,则需在`<argLine>`配置中添加一个`@{argLine}`参数,添加后的配置如下:
|
||||
|
||||
```xml
|
||||
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
```
|
||||
|
||||
参见项目`java-demo`的[pom.xml](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/pom.xml)和`kotlin-demo`的[pom.xml](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/pom.xml)文件。
|
||||
|
||||
## 在Gradle项目中使用
|
||||
|
||||
在`build.gradle`文件中添加TestableMock依赖:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.2')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.2')
|
||||
}
|
||||
```
|
||||
|
||||
然后在测试配置中添加javaagent:
|
||||
|
||||
```groovy
|
||||
test {
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
}
|
||||
```
|
||||
|
||||
参见项目`java-demo`的[build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/build.gradle)和`kotlin-demo`的[build.gradle.kts](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/build.gradle.kts)文件。
|
||||
95
docs/zh-cn/doc/test-void-method.md
Normal file
95
docs/zh-cn/doc/test-void-method.md
Normal file
@@ -0,0 +1,95 @@
|
||||
测试无返回值的方法
|
||||
---
|
||||
|
||||
如何对void类型的方法进行测试一直是许多单元测试框架在悄悄回避的话题,由于以往的单元测试手段主要是对被测单元的返回结果进行校验,当遇到方法没有返回值时就会变得无从下手。
|
||||
|
||||
从功能的角度来说,虽然void方法不返回任何值,但它的执行一定会对外界产生某些潜在影响,我们将其称为方法的"副作用",比如:
|
||||
|
||||
1. 初始化某些外部变量(私有成员变量或者全局静态变量)
|
||||
2. 在方法体内对外部对象实例进行赋值
|
||||
3. 输出了日志
|
||||
4. 调用了其他外部方法
|
||||
5. ... ...
|
||||
|
||||
|
||||
> 不返回任何值也不产生任何"副作用"的方法没有存在的意义。
|
||||
|
||||
这些"副作用"的本质归纳来说可分为两类:**修改外部变量**和**调用外部方法**。
|
||||
|
||||
通过TestableMock的私有字段访问和Mock校验器可以很方便的实现对"副作用"的结果检查。
|
||||
|
||||
### 1. 修改外部变量的void方法
|
||||
|
||||
例如,下面这个方法会根据输入修改私有成员变量`hashCache`:
|
||||
|
||||
```java
|
||||
class Demo {
|
||||
private Map<String, Integer> hashCache = mapOf();
|
||||
|
||||
public void updateCache(String domain, String key) {
|
||||
String cacheKey = domain + "::" + key;
|
||||
Integer num = hashCache.get(cacheKey);
|
||||
hashCache.put(cacheKey, count == null ? initHash(key) : nextHash(num, key));
|
||||
}
|
||||
|
||||
... // 其他方法省略
|
||||
}
|
||||
```
|
||||
|
||||
若要测试此方法,可以利用TestableMock直接读取私有成员变量的值,对结果进行校验:
|
||||
|
||||
```java
|
||||
@EnablePrivateAccess // 启用TestableMock的私有成员访问功能
|
||||
class DemoTest {
|
||||
private Demo demo = new Demo();
|
||||
|
||||
@Test
|
||||
public void testSaveToCache() {
|
||||
Integer firstVal = demo.initHash("hello"); // 访问私有方法
|
||||
Integer nextVal = demo.nextHash(firstVal, "hello"); // 访问私有方法
|
||||
demo.saveToCache("demo", "hello");
|
||||
assertEquals(firstVal, demo.hashCache.get("demo::hello")); // 读取私有变量
|
||||
demo.saveToCache("demo", "hello");
|
||||
assertEquals(nextVal, demo.hashCache.get("demo::hello")); // 读取私有变量
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 调用外部方法的void方法
|
||||
|
||||
例如,下面这个方法会根据输入打印信息到控制台:
|
||||
|
||||
```java
|
||||
class Demo {
|
||||
public void recordAction(Action action) {
|
||||
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss ");
|
||||
String timeStamp = df.format(new Date());
|
||||
System.out.println(timeStamp + "[" + action.getType() + "] " + action.getTarget());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
若要测试此方法,可以利用TestableMock快速Mock掉`System.out.println`方法。在Mock方法体里可以继续执行原调用(相当于并不影响本来方法功能,仅用于做调用记录),也可以直接留空(相当于去除了原方法的副作用)。
|
||||
|
||||
在执行完被测的void类型方法以后,用`InvokeVerifier.verify()`校验传入的打印内容是否符合预期:
|
||||
|
||||
```java
|
||||
class DemoTest {
|
||||
private Demo demo = new Demo();
|
||||
|
||||
// 拦截`System.out.println`调用
|
||||
@MockMethod
|
||||
public void println(PrintStream ps, String msg) {
|
||||
// 执行原调用
|
||||
ps.println(msg);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecordAction() {
|
||||
Action action = new Action("click", ":download");
|
||||
demo.recordAction();
|
||||
// 验证Mock方法`println`被调用,且传入参数符合预期
|
||||
verify("println").matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download");
|
||||
}
|
||||
}
|
||||
```
|
||||
40
docs/zh-cn/doc/troubleshooting.md
Normal file
40
docs/zh-cn/doc/troubleshooting.md
Normal file
@@ -0,0 +1,40 @@
|
||||
自助问题排查
|
||||
---
|
||||
|
||||
相比`Mockito`等由开发者手工放置Mock类的做法,`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也有可能发生预料之外的Mock替换。
|
||||
|
||||
若要排查Mock相关的问题,只需在测试类上添加`@MockWith`注解,并配置参数`diagnose`值为`MockDiagnose.ENABLE`,在运行测试时就会打印出详细的Mock方法替换过程。
|
||||
|
||||
```java
|
||||
@MockWith(diagnose = MockDiagnose.ENABLE)
|
||||
class DemoTest {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
输出日志示例如下:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
|
||||
[DIAGNOSE] Found 7 mock methods
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[DIAGNOSE] Line 14, mock method createBlackBox used
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[DIAGNOSE] Line 22, mock method innerFunc used
|
||||
[DIAGNOSE] Handling method commonFunc
|
||||
[DIAGNOSE] Line 29, mock method trim used
|
||||
[DIAGNOSE] Line 29, mock method sub used
|
||||
[DIAGNOSE] Line 29, mock method startsWith used
|
||||
[DIAGNOSE] Handling method getBox
|
||||
[DIAGNOSE] Line 36, mock method secretBox used
|
||||
[DIAGNOSE] Handling method callerOne
|
||||
[DIAGNOSE] Line 43, mock method callFromDifferentMethod used
|
||||
[DIAGNOSE] Handling method callerTwo
|
||||
[DIAGNOSE] Line 47, mock method callFromDifferentMethod used
|
||||
[DIAGNOSE] Handling method innerFunc
|
||||
[DIAGNOSE] Handling method callFromDifferentMethod
|
||||
```
|
||||
|
||||
该日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
|
||||
36
docs/zh-cn/doc/use-maven-plugin.md
Normal file
36
docs/zh-cn/doc/use-maven-plugin.md
Normal file
@@ -0,0 +1,36 @@
|
||||
使用Testable Maven插件
|
||||
---
|
||||
|
||||
在使用Maven构建的项目里,除了直接修改`maven-surefire-plugin`插件的运行参数,也可通过`testable-maven-plugin`插件获得相同效果:
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare</id>
|
||||
<goals>
|
||||
<goal>prepare</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
> 当使用`testable-maven-plugin`插件时,应该移除`maven-surefire-plugin`插件上的TestableMock相关配置,同时可以从项目的`pom.xml`文件中移除对`com.alibaba.testable:testable-agent`包的显示依赖
|
||||
|
||||
`testable-maven-plugin`插件能够与Jacoco插件直接同时使用,无需额外适配,因此能使`pom.xml`文件编写起来更简单且美观。
|
||||
|
||||
但是,当通过IDE运行单个测试用例时,Mock功能会失效。
|
||||
|
||||
这是由于IDE运行单个测试用例时通常都只会运行`maven-surefire-plugin`插件,跳过了`testable-maven-plugin`插件执行,导致Mock功能所需的JavaAgent没有随测试注入。
|
||||
|
||||
该问题可以通过额外配置IDE的测试参数绕过。以IntelliJ为例,在单元测试配置的"虚拟机参数(VM Option)"属性值末尾添加JavaAgent启动参数:`-javaagent:${HOME}/.m2/repository/com/alibaba/testable/testable-agent/x.y.z/testable-agent-x.y.z.jar`
|
||||
|
||||
> PS:请将路径中的`x.y.z`替换成实际使用的版本号
|
||||
|
||||

|
||||
|
||||
这样实际上还是该了`maven-surefire-plugin`插件的配置,因此目前除了需要考虑美观因素的场景以外,直接在`pom.xml`文件中修改`maven-surefire-plugin`插件配置依然是更加实用的方案。
|
||||
110
docs/zh-cn/doc/use-mock.md
Normal file
110
docs/zh-cn/doc/use-mock.md
Normal file
@@ -0,0 +1,110 @@
|
||||
快速Mock被测类的任意方法调用
|
||||
---
|
||||
|
||||
相比以往Mock工具以类为粒度的Mock方式,`TestableMock`允许用户直接定义需要Mock的单个方法,并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测方法中的指定方法调用。
|
||||
|
||||
具体的Mock方法定义约定如下:
|
||||
|
||||
#### 1. 覆写任意类的方法调用
|
||||
|
||||
在测试类里定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
|
||||
|
||||
此时被测类中所有对该需覆写方法的调用,将在单元测试运行时,将自动被替换为对上述自定义Mock方法的调用。
|
||||
|
||||
**注意**:当遇到待覆写方法有重名时,可以将需覆写的方法名写到`@MockMethod`注解的`targetMethod`参数里,这样Mock方法自身就可以随意命名了。
|
||||
|
||||
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在测试类定义如下方法:
|
||||
|
||||
```java
|
||||
// 原方法签名为`String substring(int, int)`
|
||||
// 调用此方法的对象`"anything"`类型为`String`
|
||||
// 则Mock方法签名在其参数列表首位增加一个类型为`String`的参数(名字随意)
|
||||
// 此参数可用于获得当时的实际调用者的值和上下文
|
||||
@MockMethod
|
||||
private String substring(String self, int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
```
|
||||
|
||||
下面这个例子展示了`targetMethod`参数的用法,其效果与上述示例相同:
|
||||
|
||||
```java
|
||||
// 使用`targetMethod`指定需Mock的方法名
|
||||
// 此方法本身现在可以随意命名,但方法参数依然需要遵循相同的匹配规则
|
||||
@MockMethod(targetMethod = "substring")
|
||||
private String use_any_mock_method_name(String self, int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
```
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_common_method()`测试用例。(由于Kotlin对String类型进行了魔改,故Kotlin示例中将被测方法在`BlackBox`类里加了一层封装)
|
||||
|
||||
#### 2. 覆写被测类自身的成员方法
|
||||
|
||||
有时候,在对某些方法进行测试时,希望将被测类自身的另外一些成员方法Mock掉。
|
||||
|
||||
操作方法与前一种情况相同,Mock方法的第一个参数类型需与被测类相同,即可实现对被测类自身(不论是公有或私有)成员方法的覆写。
|
||||
|
||||
例如,被测类中有一个签名为`String innerFunc(String)`的私有方法,我们希望在测试的时候将它替换掉,则只需在测试类定义如下方法:
|
||||
|
||||
```java
|
||||
// 被测类型是`DemoMock`
|
||||
// 因此在定义Mock方法时,在目标方法参数首位加一个类型为`DemoMock`的参数(名字随意)
|
||||
@MockMethod
|
||||
private String innerFunc(DemoMock self, String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
```
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_member_method()`测试用例。
|
||||
|
||||
#### 3. 覆写任意类的静态方法
|
||||
|
||||
对于静态方法的Mock与普通方法相同。但需要注意的是,静态方法的Mock方法被调用时,传入的第一个参数实际值始终是`null`。
|
||||
|
||||
例如,在被测类中调用了`BlackBox`类型中的静态方法`secretBox()`,改方法签名为`BlackBox secretBox()`,则Mock方法如下:
|
||||
|
||||
```java
|
||||
// 目标静态方法定义在`BlackBox`类型中
|
||||
// 在定义Mock方法时,在目标方法参数首位加一个类型为`BlackBox`的参数(名字随意)
|
||||
// 此参数仅用于标识目标类型,实际传入值将始终为`null`
|
||||
@MockMethod
|
||||
private BlackBox secretBox(BlackBox ignore) {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
```
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_static_method()`测试用例。
|
||||
|
||||
#### 4. 覆写任意类的new操作
|
||||
|
||||
在测试类里定义一个有`@MockContructor`注解的普通方法,使该方法返回值类型为要被创建的对象类型,且方法参数与要Mock的构造函数参数完全一致,方法名称随意。
|
||||
|
||||
此时被测类中所有用`new`创建指定类的操作(并使用了与Mock方法参数一致的构造函数)将被替换为对该自定义方法的调用。
|
||||
|
||||
例如,在被测类中有一处`new BlackBox("something")`调用,希望在测试时将它换掉(通常是换成Mock对象,或换成使用测试参数创建的临时对象),则只需定义如下Mock方法:
|
||||
|
||||
```java
|
||||
// 要覆写的构造函数签名为`BlackBox(String)`
|
||||
// 无需在Mock方法参数列表增加额外参数,Mock方法的名称随意起
|
||||
@MockContructor
|
||||
private BlackBox createBlackBox(String text) {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
```
|
||||
|
||||
> 也可以依然使用`@MockMethod`注解,并配置`targetMethod`参数值为`"<init>"`,其余同上。效果与使用`@MockContructor`注解相同
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_new_object()`测试用例。
|
||||
|
||||
#### 5. 识别当前测试用例和调用来源
|
||||
|
||||
在Mock方法中可以通过`TestableTool.TEST_CASE`和`TestableTool.SOURCE_METHOD`来识别**当前运行的测试用例名称**和**进入该Mock方法前的被测类方法名称**,从而区分处理不同的调用场景。
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_get_source_method_name()`和`should_able_to_get_test_case_name()`测试用例。
|
||||
|
||||
#### 6. 验证Mock方法被调用的顺序和参数
|
||||
|
||||
在测试用例中可用通过`TestableTool.verify()`方法,配合`with()`、`withInOrder()`、`without()`、`withTimes()`等方法实现对Mock调用情况的验证。
|
||||
|
||||
详见[校验Mock调用](zh-cn/doc/matcher.md)文档。
|
||||
2
docs/zh-cn/navbar.md
Normal file
2
docs/zh-cn/navbar.md
Normal file
@@ -0,0 +1,2 @@
|
||||
* [首页](/zh-cn/)
|
||||
* [问题和反馈](https://github.com/alibaba/testable-mock/issues)
|
||||
15
docs/zh-cn/sidebar.md
Normal file
15
docs/zh-cn/sidebar.md
Normal file
@@ -0,0 +1,15 @@
|
||||
- 快速上手
|
||||
- [使用TestableMock](zh-cn/doc/setup.md)
|
||||
- [直接访问私有成员](zh-cn/doc/private-accessor.md)
|
||||
- [快速Mock任意方法](zh-cn/doc/use-mock.md)
|
||||
- [测试无返回值的方法](zh-cn/doc/test-void-method.md)
|
||||
|
||||
- 使用参考
|
||||
- [校验Mock调用](zh-cn/doc/invoke-matcher.md)
|
||||
- [常见使用问题](zh-cn/doc/frequency-asked-questions.md)
|
||||
- [自助问题排查](zh-cn/doc/troubleshooting.md)
|
||||
- [Testable Maven插件](zh-cn/doc/use-maven-plugin.md)
|
||||
|
||||
- 技术参考
|
||||
- [主流Mock工具对比](zh-cn/doc/comparation.md)
|
||||
- [Release Note](zh-cn/doc/release-note.md)
|
||||
5
pom.xml
5
pom.xml
@@ -4,13 +4,16 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>parent</artifactId>
|
||||
<artifactId>testable-aggregate</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>testable-parent</module>
|
||||
<module>testable-processor</module>
|
||||
<module>testable-core</module>
|
||||
<module>testable-agent</module>
|
||||
<module>testable-all</module>
|
||||
<module>testable-maven-plugin</module>
|
||||
<module>demo</module>
|
||||
</modules>
|
||||
|
||||
@@ -1,95 +1,168 @@
|
||||
<?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"
|
||||
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.2</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>testable-agent</name>
|
||||
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
<version>0.2.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>testable-agent</name>
|
||||
<properties>
|
||||
<asm.lib.version>8.0.1</asm.lib.version>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
</properties>
|
||||
|
||||
<properties>
|
||||
<java.version>1.6</java.version>
|
||||
<project.compiler.level>1.6</project.compiler.level>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<asm.lib.version>8.0.1</asm.lib.version>
|
||||
<testable.version>0.2.0-SNAPSHOT</testable.version>
|
||||
<plugin.compiler.version>3.8.1</plugin.compiler.version>
|
||||
<plugin.jar.version>3.2.0</plugin.jar.version>
|
||||
<plugin.shade.version>3.2.4</plugin.shade.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm-tree</artifactId>
|
||||
<version>${asm.lib.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-core</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm-tree</artifactId>
|
||||
<version>${asm.lib.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.6.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-core</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<finalName>testable-agent</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${plugin.compiler.version}</version>
|
||||
<configuration>
|
||||
<source>${project.compiler.level}</source>
|
||||
<target>${project.compiler.level}</target>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${plugin.jar.version}</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Premain-Class>com.alibaba.testable.agent.PreMain</Premain-Class>
|
||||
<Can-Retransform-Classes>true</Can-Retransform-Classes>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<build>
|
||||
<finalName>testable-agent</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${plugin.compiler.version}</version>
|
||||
<configuration>
|
||||
<source>${project.compiler.level}</source>
|
||||
<target>${project.compiler.level}</target>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${plugin.jar.version}</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Premain-Class>com.alibaba.testable.agent.PreMain</Premain-Class>
|
||||
<Can-Retransform-Classes>true</Can-Retransform-Classes>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>${plugin.shade.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<relocations>
|
||||
<relocation>
|
||||
<pattern>org.objectweb.asm</pattern>
|
||||
<shadedPattern>agent.org.objectweb.asm</shadedPattern>
|
||||
</relocation>
|
||||
</relocations>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>${plugin.shade.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<relocations>
|
||||
<relocation>
|
||||
<pattern>org.objectweb.asm</pattern>
|
||||
<shadedPattern>agent.org.objectweb.asm</shadedPattern>
|
||||
</relocation>
|
||||
</relocations>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${plugin.surefire.version}</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>release</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>${plugin.source.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>${plugin.javadoc.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>${plugin.gpg.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>${plugin.staging.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<serverId>oss</serverId>
|
||||
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
|
||||
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<distributionManagement>
|
||||
<snapshotRepository>
|
||||
<id>oss</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
|
||||
</snapshotRepository>
|
||||
<repository>
|
||||
<id>oss</id>
|
||||
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.alibaba.testable.agent;
|
||||
|
||||
import com.alibaba.testable.agent.transformer.TestableClassTransformer;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
|
||||
@@ -10,8 +11,44 @@ import java.lang.instrument.Instrumentation;
|
||||
*/
|
||||
public class PreMain {
|
||||
|
||||
private static final String AND = "&";
|
||||
private static final String DEBUG = "debug";
|
||||
private static final String VERBOSE = "verbose";
|
||||
private static final String LOG_LEVEL = "logLevel";
|
||||
private static final String EQUAL = "=";
|
||||
|
||||
public static void premain(String agentArgs, Instrumentation inst) {
|
||||
parseArgs(agentArgs);
|
||||
inst.addTransformer(new TestableClassTransformer());
|
||||
}
|
||||
|
||||
private static void parseArgs(String args) {
|
||||
if (args == null) {
|
||||
return;
|
||||
}
|
||||
for (String a : args.split(AND)) {
|
||||
int i = a.indexOf(EQUAL);
|
||||
if (i > 0) {
|
||||
String k = a.substring(0, i);
|
||||
String v = a.substring(i + 1);
|
||||
if (k.equals(LOG_LEVEL)) {
|
||||
setLogLevel(v);
|
||||
}
|
||||
} else {
|
||||
setLogLevel(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean setLogLevel(String level) {
|
||||
if (level.equals(DEBUG)) {
|
||||
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_DIAGNOSE);
|
||||
return true;
|
||||
} else if (level.equals(VERBOSE)) {
|
||||
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_VERBOSE);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,9 +8,19 @@ public class ConstPool {
|
||||
public static final String DOT = ".";
|
||||
public static final String SLASH = "/";
|
||||
|
||||
public static final String CONSTRUCTOR = "<init>";
|
||||
public static final String TEST_POSTFIX = "Test";
|
||||
public static final String TESTABLE_INJECT_REF = "_testableInternalRef";
|
||||
|
||||
public static final String FIELD_TARGET_METHOD = "targetMethod";
|
||||
|
||||
public static final String MOCK_WITH = "com.alibaba.testable.core.annotation.MockWith";
|
||||
public static final String MOCK_METHOD = "com.alibaba.testable.core.annotation.MockMethod";
|
||||
public static final String MOCK_CONSTRUCTOR = "com.alibaba.testable.core.annotation.MockConstructor";
|
||||
public static final String TESTABLE_MOCK = "com.alibaba.testable.core.annotation.TestableMock";
|
||||
|
||||
/**
|
||||
* Name of the constructor method
|
||||
*/
|
||||
public static final String CONSTRUCTOR = "<init>";
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.model.MethodInfo;
|
||||
import com.alibaba.testable.agent.util.BytecodeUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
@@ -19,8 +20,10 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
|
||||
private final List<MethodInfo> injectMethods;
|
||||
private final Set<Integer> invokeOps = new HashSet<Integer>() {{
|
||||
add(Opcodes.INVOKESPECIAL);
|
||||
add(Opcodes.INVOKEVIRTUAL);
|
||||
add(Opcodes.INVOKESPECIAL);
|
||||
add(Opcodes.INVOKESTATIC);
|
||||
add(Opcodes.INVOKEINTERFACE);
|
||||
}};
|
||||
|
||||
public SourceClassHandler(List<MethodInfo> injectMethods) {
|
||||
@@ -49,6 +52,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
|
||||
private void transformMethod(ClassNode cn, MethodNode mn, Set<MethodInfo> memberInjectMethods,
|
||||
Set<MethodInfo> newOperatorInjectMethods) {
|
||||
LogUtil.diagnose(" Handling method %s", mn.name);
|
||||
AbstractInsnNode[] instructions = mn.instructions.toArray();
|
||||
List<MethodInfo> memberInjectMethodList = new ArrayList<MethodInfo>(memberInjectMethods);
|
||||
int i = 0;
|
||||
@@ -57,11 +61,14 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
MethodInsnNode node = (MethodInsnNode)instructions[i];
|
||||
String memberInjectMethodName = getMemberInjectMethodName(memberInjectMethodList, node);
|
||||
if (memberInjectMethodName != null) {
|
||||
// it's a member method and an inject method for it exist
|
||||
// it's a member or static method and an inject method for it exist
|
||||
int rangeStart = getMemberMethodStart(instructions, i);
|
||||
if (rangeStart >= 0) {
|
||||
instructions = replaceMemberCallOps(cn, mn, instructions, node.owner, memberInjectMethodName, rangeStart, i);
|
||||
instructions = replaceMemberCallOps(cn, mn, memberInjectMethodName, instructions,
|
||||
node.owner, node.getOpcode(), rangeStart, i);
|
||||
i = rangeStart;
|
||||
} else {
|
||||
LogUtil.warn("Potential missed mocking at %s:%s", mn.name, getLineNum(instructions, i));
|
||||
}
|
||||
} else if (ConstPool.CONSTRUCTOR.equals(node.name)) {
|
||||
// it's a new operation
|
||||
@@ -82,7 +89,8 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
|
||||
private String getMemberInjectMethodName(List<MethodInfo> memberInjectMethodList, MethodInsnNode node) {
|
||||
for (MethodInfo m : memberInjectMethodList) {
|
||||
if (m.getClazz().equals(node.owner) && m.getName().equals(node.name) && m.getDesc().equals(node.desc)) {
|
||||
String nodeOwner = ClassUtil.fitCompanionClassName(node.owner);
|
||||
if (m.getClazz().equals(nodeOwner) && m.getName().equals(node.name) && m.getDesc().equals(node.desc)) {
|
||||
return m.getMockName();
|
||||
}
|
||||
}
|
||||
@@ -113,19 +121,9 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
}
|
||||
|
||||
private int getMemberMethodStart(AbstractInsnNode[] instructions, int rangeEnd) {
|
||||
int stackLevel = ClassUtil.getParameterTypes(((MethodInsnNode)instructions[rangeEnd]).desc).size();
|
||||
int stackLevel = getInitialStackLevel((MethodInsnNode)instructions[rangeEnd]);
|
||||
for (int i = rangeEnd - 1; i >= 0; i--) {
|
||||
switch (instructions[i].getOpcode()) {
|
||||
case Opcodes.INVOKEINTERFACE:
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
case Opcodes.INVOKESPECIAL:
|
||||
case Opcodes.INVOKEDYNAMIC:
|
||||
case Opcodes.INVOKESTATIC:
|
||||
stackLevel += ClassUtil.getParameterTypes(((MethodInsnNode)instructions[i]).desc).size();
|
||||
break;
|
||||
default:
|
||||
stackLevel -= BytecodeUtil.stackEffect(instructions[i].getOpcode());
|
||||
}
|
||||
stackLevel += getStackLevelChange(instructions[i]);
|
||||
if (stackLevel < 0) {
|
||||
return i;
|
||||
}
|
||||
@@ -133,8 +131,47 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int getInitialStackLevel(MethodInsnNode instruction) {
|
||||
int stackLevel = ClassUtil.getParameterTypes((instruction).desc).size();
|
||||
switch (instruction.getOpcode()) {
|
||||
case Opcodes.INVOKESPECIAL:
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
case Opcodes.INVOKEINTERFACE:
|
||||
return stackLevel;
|
||||
case Opcodes.INVOKESTATIC:
|
||||
case Opcodes.INVOKEDYNAMIC:
|
||||
return stackLevel - 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private int getStackLevelChange(AbstractInsnNode instruction) {
|
||||
switch (instruction.getOpcode()) {
|
||||
case Opcodes.INVOKESPECIAL:
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
case Opcodes.INVOKEINTERFACE:
|
||||
return stackEffectOfInvocation(instruction) + 1;
|
||||
case Opcodes.INVOKESTATIC:
|
||||
case Opcodes.INVOKEDYNAMIC:
|
||||
return stackEffectOfInvocation(instruction);
|
||||
case -1:
|
||||
// either LabelNode or LineNumberNode
|
||||
return 0;
|
||||
default:
|
||||
return -BytecodeUtil.stackEffect(instruction.getOpcode());
|
||||
}
|
||||
}
|
||||
|
||||
private int stackEffectOfInvocation(AbstractInsnNode instruction) {
|
||||
String desc = ((MethodInsnNode)instruction).desc;
|
||||
return ClassUtil.getParameterTypes(desc).size() - (ClassUtil.getReturnType(desc).isEmpty() ? 0 : 1);
|
||||
}
|
||||
|
||||
private AbstractInsnNode[] replaceNewOps(ClassNode cn, MethodNode mn, String newOperatorInjectMethodName,
|
||||
AbstractInsnNode[] instructions, int start, int end) {
|
||||
LogUtil.diagnose(" Line %d, mock method %s used", getLineNum(instructions, start),
|
||||
newOperatorInjectMethodName);
|
||||
String classType = ((TypeInsnNode)instructions[start]).desc;
|
||||
String constructorDesc = ((MethodInsnNode)instructions[end]).desc;
|
||||
String testClassName = ClassUtil.getTestClassName(cn.name);
|
||||
@@ -148,24 +185,48 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
return mn.instructions.toArray();
|
||||
}
|
||||
|
||||
private int getLineNum(AbstractInsnNode[] instructions, int start) {
|
||||
for (int i = start - 1; i >= 0; i--) {
|
||||
if (instructions[i] instanceof LineNumberNode) {
|
||||
return ((LineNumberNode)instructions[i]).line;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private String getConstructorInjectDesc(String constructorDesc, String classType) {
|
||||
return constructorDesc.substring(0, constructorDesc.length() - 1) +
|
||||
ClassUtil.toByteCodeClassName(classType);
|
||||
}
|
||||
|
||||
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions,
|
||||
String ownerClass, String substitutionMethod, int start, int end) {
|
||||
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, String substitutionMethod,
|
||||
AbstractInsnNode[] instructions, String ownerClass,
|
||||
int opcode, int start, int end) {
|
||||
LogUtil.diagnose(" Line %d, mock method %s used", getLineNum(instructions, start), substitutionMethod);
|
||||
mn.maxStack++;
|
||||
MethodInsnNode method = (MethodInsnNode)instructions[end];
|
||||
String testClassName = ClassUtil.getTestClassName(cn.name);
|
||||
mn.instructions.insertBefore(instructions[start], new FieldInsnNode(GETSTATIC, testClassName,
|
||||
ConstPool.TESTABLE_INJECT_REF, ClassUtil.toByteCodeClassName(testClassName)));
|
||||
if (Opcodes.INVOKESTATIC == opcode || isCompanionMethod(ownerClass, opcode)) {
|
||||
// append a null value if it was a static invoke or in kotlin companion class
|
||||
mn.instructions.insertBefore(instructions[start], new InsnNode(ACONST_NULL));
|
||||
if (ClassUtil.isCompanionClassName(ownerClass)) {
|
||||
// for kotlin companion class, remove the byte code of reference to "companion" static field
|
||||
mn.instructions.remove(instructions[end - 1]);
|
||||
}
|
||||
}
|
||||
// method with @MockMethod will be modified as public access, so INVOKEVIRTUAL is used
|
||||
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,
|
||||
substitutionMethod, addFirstParameter(method.desc, ownerClass), false));
|
||||
substitutionMethod, addFirstParameter(method.desc, ClassUtil.fitCompanionClassName(ownerClass)), false));
|
||||
mn.instructions.remove(instructions[end]);
|
||||
return mn.instructions.toArray();
|
||||
}
|
||||
|
||||
private boolean isCompanionMethod(String ownerClass, int opcode) {
|
||||
return Opcodes.INVOKEVIRTUAL == opcode && ClassUtil.isCompanionClassName(ownerClass);
|
||||
}
|
||||
|
||||
private String addFirstParameter(String desc, String ownerClass) {
|
||||
return "(" + ClassUtil.toByteCodeClassName(ownerClass) + desc.substring(1);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package com.alibaba.testable.agent.handler;
|
||||
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.util.AnnotationUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
@@ -16,17 +20,17 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
|
||||
private static final String CLASS_TESTABLE_TOOL = "com/alibaba/testable/core/tool/TestableTool";
|
||||
private static final String CLASS_TESTABLE_UTIL = "com/alibaba/testable/core/util/TestableUtil";
|
||||
private static final String CLASS_INVOKE_RECORD_UTIL = "com/alibaba/testable/core/util/InvokeRecordUtil";
|
||||
private static final String FIELD_TEST_CASE = "TEST_CASE";
|
||||
private static final String FIELD_SOURCE_METHOD = "SOURCE_METHOD";
|
||||
private static final String METHOD_CURRENT_TEST_CASE_NAME = "currentTestCaseName";
|
||||
private static final String METHOD_CURRENT_SOURCE_METHOD_NAME = "currentSourceMethodName";
|
||||
private static final String METHOD_COUNT_MOCK_INVOKE = "countMockInvoke";
|
||||
private static final String SIGNATURE_TESTABLE_UTIL_METHOD = "(Ljava/lang/Object;)Ljava/lang/String;";
|
||||
private static final String SIGNATURE_INVOKE_COUNTER_METHOD = "()V";
|
||||
private static final Map<String, String> FIELD_TO_METHOD_MAPPING = new HashMap<String, String>() {{
|
||||
put(FIELD_TEST_CASE, METHOD_CURRENT_TEST_CASE_NAME);
|
||||
put(FIELD_SOURCE_METHOD, METHOD_CURRENT_SOURCE_METHOD_NAME);
|
||||
}};
|
||||
private static final String METHOD_MARK_TEST_CASE_BEGIN = "markTestCaseBegin";
|
||||
private static final String METHOD_RECORD_MOCK_INVOKE = "recordMockInvoke";
|
||||
private static final String SIGNATURE_CURRENT_TEST_CASE_NAME = "(Ljava/lang/Object;)Ljava/lang/String;";
|
||||
private static final String SIGNATURE_CURRENT_SOURCE_METHOD_NAME = "()Ljava/lang/String;";
|
||||
private static final String SIGNATURE_VOID_METHOD_WITHOUT_PARAMETER = "()V";
|
||||
private static final String SIGNATURE_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;Z)V";
|
||||
|
||||
/**
|
||||
* Handle bytecode of test class
|
||||
@@ -34,11 +38,19 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
*/
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
for (MethodNode m : cn.methods) {
|
||||
transformMethod(cn, m);
|
||||
Iterator<FieldNode> iterator = cn.fields.iterator();
|
||||
if (iterator.hasNext()) {
|
||||
if (ConstPool.TESTABLE_INJECT_REF.equals(iterator.next().name)) {
|
||||
// avoid duplicate injection
|
||||
LogUtil.verbose("Duplicate injection found, ignore " + cn.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
cn.fields.add(new FieldNode(ACC_PUBLIC | ACC_STATIC, ConstPool.TESTABLE_INJECT_REF,
|
||||
ClassUtil.toByteCodeClassName(cn.name), null, null));
|
||||
for (MethodNode m : cn.methods) {
|
||||
transformMethod(cn, m);
|
||||
}
|
||||
}
|
||||
|
||||
private void transformMethod(ClassNode cn, MethodNode mn) {
|
||||
@@ -49,16 +61,19 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
private void handleAnnotation(ClassNode cn, MethodNode mn) {
|
||||
List<String> visibleAnnotationNames = new ArrayList<String>();
|
||||
if (mn.visibleAnnotations == null) {
|
||||
// let's assume test case should has a annotation, e.g. @Test or whatever
|
||||
return;
|
||||
}
|
||||
for (AnnotationNode n : mn.visibleAnnotations) {
|
||||
visibleAnnotationNames.add(n.desc);
|
||||
}
|
||||
if (visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.TESTABLE_MOCK))) {
|
||||
if (visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD)) ||
|
||||
visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.TESTABLE_MOCK)) ||
|
||||
visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR))) {
|
||||
mn.access &= ~ACC_PRIVATE;
|
||||
mn.access &= ~ACC_PROTECTED;
|
||||
mn.access |= ACC_PUBLIC;
|
||||
injectInvokeCounter(mn);
|
||||
injectInvokeRecorder(mn);
|
||||
} else if (couldBeTestMethod(mn)) {
|
||||
injectTestableRef(cn, mn);
|
||||
}
|
||||
@@ -85,36 +100,125 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
|
||||
private AbstractInsnNode[] replaceTestableUtilField(MethodNode mn, AbstractInsnNode[] instructions,
|
||||
String fieldName, int pos) {
|
||||
InsnList insnNodes = new InsnList();
|
||||
// NOTE: will insert in reversed order
|
||||
insnNodes.insert(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, FIELD_TO_METHOD_MAPPING.get(fieldName),
|
||||
SIGNATURE_TESTABLE_UTIL_METHOD, false));
|
||||
insnNodes.insert(new VarInsnNode(ALOAD, 0));
|
||||
mn.instructions.insert(instructions[pos], insnNodes);
|
||||
mn.instructions.remove(instructions[pos]);
|
||||
InsnList il = new InsnList();
|
||||
if (FIELD_TEST_CASE.equals(fieldName)) {
|
||||
il.add(new VarInsnNode(ALOAD, 0));
|
||||
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_CURRENT_TEST_CASE_NAME,
|
||||
SIGNATURE_CURRENT_TEST_CASE_NAME, false));
|
||||
} else if (FIELD_SOURCE_METHOD.equals(fieldName)) {
|
||||
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_CURRENT_SOURCE_METHOD_NAME,
|
||||
SIGNATURE_CURRENT_SOURCE_METHOD_NAME, false));
|
||||
}
|
||||
if (il.size() > 0) {
|
||||
mn.instructions.insert(instructions[pos], il);
|
||||
mn.instructions.remove(instructions[pos]);
|
||||
}
|
||||
return mn.instructions.toArray();
|
||||
}
|
||||
|
||||
private void injectInvokeCounter(MethodNode mn) {
|
||||
MethodInsnNode node = new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_COUNT_MOCK_INVOKE,
|
||||
SIGNATURE_INVOKE_COUNTER_METHOD, false);
|
||||
mn.instructions.insertBefore(mn.instructions.get(0), node);
|
||||
private void injectInvokeRecorder(MethodNode mn) {
|
||||
InsnList il = new InsnList();
|
||||
List<Byte> types = ClassUtil.getParameterTypes(mn.desc);
|
||||
int size = types.size();
|
||||
int parameterOffset = 1;
|
||||
mn.maxStack += 1;
|
||||
il.add(getIntInsn(size));
|
||||
il.add(new TypeInsnNode(ANEWARRAY, ClassUtil.CLASS_OBJECT));
|
||||
for (int i = 0; i < size; i++) {
|
||||
mn.maxStack += 3;
|
||||
il.add(new InsnNode(DUP));
|
||||
il.add(getIntInsn(i));
|
||||
ImmutablePair<Integer, Integer> code = getLoadParameterByteCode(types.get(i));
|
||||
il.add(new VarInsnNode(code.left, parameterOffset));
|
||||
parameterOffset += code.right;
|
||||
MethodInsnNode typeConvertMethodNode = ClassUtil.getPrimaryTypeConvertMethod(types.get(i));
|
||||
if (typeConvertMethodNode != null) {
|
||||
il.add(typeConvertMethodNode);
|
||||
}
|
||||
il.add(new InsnNode(AASTORE));
|
||||
}
|
||||
if (isMockForConstructor(mn)) {
|
||||
il.add(new InsnNode(ICONST_1));
|
||||
} else {
|
||||
il.add(new InsnNode(ICONST_0));
|
||||
}
|
||||
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_INVOKE_RECORD_UTIL, METHOD_RECORD_MOCK_INVOKE,
|
||||
SIGNATURE_INVOKE_RECORDER_METHOD, false));
|
||||
mn.instructions.insertBefore(mn.instructions.get(0), il);
|
||||
}
|
||||
|
||||
private boolean isMockForConstructor(MethodNode mn) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.MOCK_CONSTRUCTOR)) {
|
||||
return true;
|
||||
}
|
||||
String method = AnnotationUtil.getAnnotationParameter
|
||||
(an, ConstPool.FIELD_TARGET_METHOD, null, String.class);
|
||||
if (ConstPool.CONSTRUCTOR.equals(method)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ImmutablePair<Integer, Integer> getLoadParameterByteCode(Byte type) {
|
||||
switch (type) {
|
||||
case ClassUtil.TYPE_BYTE:
|
||||
case ClassUtil.TYPE_CHAR:
|
||||
case ClassUtil.TYPE_SHORT:
|
||||
case ClassUtil.TYPE_INT:
|
||||
case ClassUtil.TYPE_BOOL:
|
||||
return ImmutablePair.of(ILOAD, 1);
|
||||
case ClassUtil.TYPE_DOUBLE:
|
||||
return ImmutablePair.of(DLOAD, 2);
|
||||
case ClassUtil.TYPE_FLOAT:
|
||||
return ImmutablePair.of(FLOAD, 1);
|
||||
case ClassUtil.TYPE_LONG:
|
||||
return ImmutablePair.of(LLOAD, 2);
|
||||
default:
|
||||
return ImmutablePair.of(ALOAD, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private AbstractInsnNode getIntInsn(int num) {
|
||||
switch (num) {
|
||||
case 0:
|
||||
return new InsnNode(ICONST_0);
|
||||
case 1:
|
||||
return new InsnNode(ICONST_1);
|
||||
case 2:
|
||||
return new InsnNode(ICONST_2);
|
||||
case 3:
|
||||
return new InsnNode(ICONST_3);
|
||||
case 4:
|
||||
return new InsnNode(ICONST_4);
|
||||
case 5:
|
||||
return new InsnNode(ICONST_5);
|
||||
default:
|
||||
return new IntInsnNode(BIPUSH, num);
|
||||
}
|
||||
}
|
||||
|
||||
private void injectTestableRef(ClassNode cn, MethodNode mn) {
|
||||
InsnList il = new InsnList();
|
||||
// Initialize "_testableInternalRef"
|
||||
il.add(new VarInsnNode(ALOAD, 0));
|
||||
il.add(new FieldInsnNode(PUTSTATIC, cn.name, ConstPool.TESTABLE_INJECT_REF,
|
||||
ClassUtil.toByteCodeClassName(cn.name)));
|
||||
mn.instructions.insertBefore(mn.instructions.get(0), il);
|
||||
// Invoke "markTestCaseBegin"
|
||||
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_MARK_TEST_CASE_BEGIN,
|
||||
SIGNATURE_VOID_METHOD_WITHOUT_PARAMETER, false));
|
||||
mn.instructions.insertBefore(mn.instructions.getFirst(), il);
|
||||
}
|
||||
|
||||
/**
|
||||
* Different unit test framework may have different @Test annotation
|
||||
* but they should always NOT private, protected or static
|
||||
* and has neither parameter nor return value
|
||||
*/
|
||||
private boolean couldBeTestMethod(MethodNode mn) {
|
||||
return (mn.access & (ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC)) == 0 ;
|
||||
return (mn.access & (ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC)) == 0 &&
|
||||
mn.desc.equals(SIGNATURE_VOID_METHOD_WITHOUT_PARAMETER);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import com.alibaba.testable.agent.handler.SourceClassHandler;
|
||||
import com.alibaba.testable.agent.handler.TestClassHandler;
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.model.MethodInfo;
|
||||
import com.alibaba.testable.agent.tool.ComparableWeakRef;
|
||||
import com.alibaba.testable.agent.util.AnnotationUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import com.alibaba.testable.core.model.MockDiagnose;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
@@ -14,11 +16,8 @@ import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.net.URLClassLoader;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
|
||||
|
||||
@@ -27,44 +26,69 @@ import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassNa
|
||||
*/
|
||||
public class TestableClassTransformer implements ClassFileTransformer {
|
||||
|
||||
private final Set<ComparableWeakRef<String>> loadedClassNames = ComparableWeakRef.getWeekHashSet();
|
||||
private static final String TARGET_METHOD = "targetMethod";
|
||||
private static final String FIELD_DIAGNOSE = "diagnose";
|
||||
|
||||
/**
|
||||
* Just avoid spend time to scan those surely non-user classes
|
||||
* Should keep these lists as tiny as possible
|
||||
*/
|
||||
private final String[] WHITELIST_PREFIXES = new String[] { "com/alibaba/testable/demo/" };
|
||||
private final String[] BLACKLIST_PREFIXES = new String[] { "jdk/", "java/", "javax/", "com/sun/",
|
||||
"org/apache/maven/", "com/alibaba/testable/", "junit/", "org/junit/", "org/testng/" };
|
||||
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
|
||||
ProtectionDomain protectionDomain, byte[] classFileBuffer) {
|
||||
if (isSystemClass(loader, className) || loadedClassNames.contains(new ComparableWeakRef<String>(className))) {
|
||||
if (isSystemClass(className)) {
|
||||
// Ignore system class and reloaded class
|
||||
LogUtil.verbose("Ignore class: " + (className == null ? "<lambda>" : className));
|
||||
return null;
|
||||
}
|
||||
LogUtil.verbose("Handle class: " + className);
|
||||
byte[] bytes = null;
|
||||
try {
|
||||
if (shouldTransformAsSourceClass(className)) {
|
||||
// it's a source class with testable enabled
|
||||
loadedClassNames.add(new ComparableWeakRef<String>(className));
|
||||
LogUtil.diagnose("Handling source class %s", className);
|
||||
List<MethodInfo> injectMethods = getTestableMockMethods(ClassUtil.getTestClassName(className));
|
||||
return new SourceClassHandler(injectMethods).getBytes(classFileBuffer);
|
||||
bytes = new SourceClassHandler(injectMethods).getBytes(classFileBuffer);
|
||||
resetMockContext();
|
||||
} else if (shouldTransformAsTestClass(className)) {
|
||||
// it's a test class with testable enabled
|
||||
loadedClassNames.add(new ComparableWeakRef<String>(className));
|
||||
return new TestClassHandler().getBytes(classFileBuffer);
|
||||
LogUtil.diagnose("Handling test class %s", className);
|
||||
bytes = new TestClassHandler().getBytes(classFileBuffer);
|
||||
resetMockContext();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
LogUtil.warn("Failed to transform class " + className);
|
||||
}
|
||||
return null;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private boolean shouldTransformAsSourceClass(String className) {
|
||||
return ClassUtil.anyMethodHasAnnotation(ClassUtil.getTestClassName(className), ConstPool.TESTABLE_MOCK);
|
||||
return hasMockAnnotation(ClassUtil.getTestClassName(className));
|
||||
}
|
||||
|
||||
private boolean shouldTransformAsTestClass(String className) {
|
||||
return className.endsWith(ConstPool.TEST_POSTFIX) &&
|
||||
ClassUtil.anyMethodHasAnnotation(className, ConstPool.TESTABLE_MOCK);
|
||||
return className.endsWith(ConstPool.TEST_POSTFIX) && hasMockAnnotation(className);
|
||||
}
|
||||
|
||||
private boolean isSystemClass(ClassLoader loader, String className) {
|
||||
return !(loader instanceof URLClassLoader) || null == className || className.startsWith("jdk/");
|
||||
private boolean isSystemClass(String className) {
|
||||
// className can be null for Java 8 lambdas
|
||||
if (null == className) {
|
||||
return true;
|
||||
}
|
||||
for (String prefix : WHITELIST_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (String prefix : BLACKLIST_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<MethodInfo> getTestableMockMethods(String className) {
|
||||
@@ -75,6 +99,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
for (MethodNode mn : cn.methods) {
|
||||
checkMethodAnnotation(cn, methodInfos, mn);
|
||||
}
|
||||
LogUtil.diagnose(" Found %d mock methods", methodInfos.size());
|
||||
return methodInfos;
|
||||
} catch (Exception e) {
|
||||
return new ArrayList<MethodInfo>();
|
||||
@@ -87,20 +112,81 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
return;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_MOCK)) {
|
||||
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
|
||||
String targetMethod = getAnnotationParameter(an, TARGET_METHOD, mn.name);
|
||||
String fullClassName = toDotSeparateFullClassName(an.desc);
|
||||
if (fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
|
||||
addMockConstructor(cn, methodInfos, mn);
|
||||
} else if (fullClassName.equals(ConstPool.MOCK_METHOD) ||
|
||||
fullClassName.equals(ConstPool.TESTABLE_MOCK)) {
|
||||
String targetMethod = AnnotationUtil.getAnnotationParameter(
|
||||
an, ConstPool.FIELD_TARGET_METHOD, mn.name, String.class);
|
||||
if (targetMethod.equals(ConstPool.CONSTRUCTOR)) {
|
||||
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
|
||||
methodInfos.add(new MethodInfo(sourceClassName, targetMethod, mn.name, mn.desc));
|
||||
addMockConstructor(cn, methodInfos, mn);
|
||||
} else {
|
||||
methodInfos.add(new MethodInfo(targetClass, targetMethod, mn.name, methodDescPair.right));
|
||||
addMockMethod(methodInfos, mn, methodDescPair, targetMethod);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addMockMethod(List<MethodInfo> methodInfos, MethodNode mn,
|
||||
ImmutablePair<String, String> methodDescPair, String targetMethod) {
|
||||
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
|
||||
methodInfos.add(new MethodInfo(targetClass, targetMethod, mn.name, methodDescPair.right));
|
||||
}
|
||||
|
||||
private void addMockConstructor(ClassNode cn, List<MethodInfo> methodInfos, MethodNode mn) {
|
||||
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
|
||||
methodInfos.add(new MethodInfo(sourceClassName, ConstPool.CONSTRUCTOR, mn.name, mn.desc));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether any method in specified class has specified annotation
|
||||
* @param className class that need to explore
|
||||
* @return found annotation or not
|
||||
*/
|
||||
private boolean hasMockAnnotation(String className) {
|
||||
try {
|
||||
ClassNode cn = new ClassNode();
|
||||
new ClassReader(className).accept(cn, 0);
|
||||
if (cn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.MOCK_WITH)) {
|
||||
setupMockContext(an);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (mn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
String fullClassName = toDotSeparateFullClassName(an.desc);
|
||||
if (fullClassName.equals(ConstPool.MOCK_METHOD) ||
|
||||
fullClassName.equals(ConstPool.TESTABLE_MOCK) ||
|
||||
fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Usually class not found, return without record
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setupMockContext(AnnotationNode an) {
|
||||
MockDiagnose diagnose = AnnotationUtil.getAnnotationParameter(an, FIELD_DIAGNOSE, null, MockDiagnose.class);
|
||||
if (diagnose != null) {
|
||||
LogUtil.enableDiagnose(diagnose == MockDiagnose.ENABLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetMockContext() {
|
||||
LogUtil.resetLogLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Split desc to "first parameter" and "desc of rest parameters"
|
||||
* @param desc method desc
|
||||
@@ -111,18 +197,4 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
return pos < 0 ? null : ImmutablePair.of(desc.substring(1, pos + 1), "(" + desc.substring(pos + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read value of annotation parameter
|
||||
*/
|
||||
private <T> T getAnnotationParameter(AnnotationNode an, String key, T defaultValue) {
|
||||
if (an.values != null) {
|
||||
for (int i = 0; i < an.values.size(); i += 2) {
|
||||
if (an.values.get(i).equals(key)) {
|
||||
return (T)(an.values.get(i + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class AnnotationUtil {
|
||||
|
||||
/**
|
||||
* Read value of annotation parameter
|
||||
* @param <T> template of target parameter type
|
||||
* @param an annotation node
|
||||
* @param key name of parameter to look for
|
||||
* @param defaultValue value if parameter not exist
|
||||
* @param clazz type of target parameter
|
||||
* @return value of parameter
|
||||
*/
|
||||
public static <T> T getAnnotationParameter(AnnotationNode an, String key, T defaultValue, Class<T> clazz) {
|
||||
if (an != null && an.values != null) {
|
||||
for (int i = 0; i < an.values.size(); i += 2) {
|
||||
if (an.values.get(i).equals(key)) {
|
||||
if (clazz.isEnum()) {
|
||||
// Enum type are stored as String[] in annotation parameter
|
||||
String[] values = (String[])an.values.get(i + 1);
|
||||
if (values == null || values.length != 2) {
|
||||
return defaultValue;
|
||||
}
|
||||
Class<? extends Enum> enumClazz = (Class<? extends Enum>)clazz;
|
||||
return (T)Enum.valueOf(enumClazz, values[1]);
|
||||
}
|
||||
return clazz.cast(an.values.get(i + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,14 +51,14 @@ public class BytecodeUtil {
|
||||
put(FSTORE, -1);
|
||||
put(DSTORE, -1);
|
||||
put(ASTORE, -1);
|
||||
put(IASTORE, -1);
|
||||
put(LASTORE, -1);
|
||||
put(FASTORE, -1);
|
||||
put(DASTORE, -1);
|
||||
put(AASTORE, -1);
|
||||
put(BASTORE, -1);
|
||||
put(CASTORE, -1);
|
||||
put(SASTORE, -1);
|
||||
put(IASTORE, -3);
|
||||
put(LASTORE, -3);
|
||||
put(FASTORE, -3);
|
||||
put(DASTORE, -3);
|
||||
put(AASTORE, -3);
|
||||
put(BASTORE, -3);
|
||||
put(CASTORE, -3);
|
||||
put(SASTORE, -3);
|
||||
put(POP, -1);
|
||||
put(POP2, -2);
|
||||
put(DUP, 1);
|
||||
|
||||
@@ -1,80 +1,82 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.tool.ComparableWeakRef;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class ClassUtil {
|
||||
|
||||
private static final char TYPE_BYTE = 'B';
|
||||
private static final char TYPE_CHAR = 'C';
|
||||
private static final char TYPE_DOUBLE = 'D';
|
||||
private static final char TYPE_FLOAT = 'F';
|
||||
private static final char TYPE_INT = 'I';
|
||||
private static final char TYPE_LONG = 'J';
|
||||
private static final char TYPE_CLASS = 'L';
|
||||
private static final char TYPE_SHORT = 'S';
|
||||
private static final char TYPE_BOOL = 'Z';
|
||||
private static final char PARAM_END = ')';
|
||||
private static final char CLASS_END = ';';
|
||||
private static final char TYPE_ARRAY = '[';
|
||||
public static final byte TYPE_BYTE = 'B';
|
||||
public static final byte TYPE_CHAR = 'C';
|
||||
public static final byte TYPE_DOUBLE = 'D';
|
||||
public static final byte TYPE_FLOAT = 'F';
|
||||
public static final byte TYPE_INT = 'I';
|
||||
public static final byte TYPE_LONG = 'J';
|
||||
public static final byte TYPE_CLASS = 'L';
|
||||
public static final byte TYPE_SHORT = 'S';
|
||||
public static final byte TYPE_BOOL = 'Z';
|
||||
public static final byte TYPE_VOID = 'V';
|
||||
private static final byte PARAM_END = ')';
|
||||
private static final byte CLASS_END = ';';
|
||||
private static final byte TYPE_ARRAY = '[';
|
||||
|
||||
private static final Map<Character, String> TYPE_MAPPING = new HashMap<Character, String>();
|
||||
private static final Map<ComparableWeakRef<String>, Boolean> loadedClass =
|
||||
new WeakHashMap<ComparableWeakRef<String>, Boolean>();
|
||||
public static final String CLASS_OBJECT = "java/lang/Object";
|
||||
private static final String CLASS_BYTE = "java/lang/Byte";
|
||||
private static final String CLASS_CHARACTER = "java/lang/Character";
|
||||
private static final String CLASS_DOUBLE = "java/lang/Double";
|
||||
private static final String CLASS_FLOAT = "java/lang/Float";
|
||||
private static final String CLASS_INTEGER = "java/lang/Integer";
|
||||
private static final String CLASS_LONG = "java/lang/Long";
|
||||
private static final String CLASS_SHORT = "java/lang/Short";
|
||||
private static final String CLASS_BOOLEAN = "java/lang/Boolean";
|
||||
private static final String EMPTY = "";
|
||||
private static final String METHOD_VALUE_OF = "valueOf";
|
||||
|
||||
private static final Map<Byte, String> TYPE_MAPPING = new HashMap<Byte, String>();
|
||||
|
||||
static {
|
||||
TYPE_MAPPING.put(TYPE_BYTE, "java/lang/Byte");
|
||||
TYPE_MAPPING.put(TYPE_CHAR, "java/lang/Character");
|
||||
TYPE_MAPPING.put(TYPE_DOUBLE, "java/lang/Double");
|
||||
TYPE_MAPPING.put(TYPE_FLOAT, "java/lang/Float");
|
||||
TYPE_MAPPING.put(TYPE_INT, "java/lang/Integer");
|
||||
TYPE_MAPPING.put(TYPE_LONG, "java/lang/Long");
|
||||
TYPE_MAPPING.put(TYPE_SHORT, "java/lang/Short");
|
||||
TYPE_MAPPING.put(TYPE_BOOL, "java/lang/Boolean");
|
||||
TYPE_MAPPING.put(TYPE_BYTE, CLASS_BYTE);
|
||||
TYPE_MAPPING.put(TYPE_CHAR, CLASS_CHARACTER);
|
||||
TYPE_MAPPING.put(TYPE_DOUBLE, CLASS_DOUBLE);
|
||||
TYPE_MAPPING.put(TYPE_FLOAT, CLASS_FLOAT);
|
||||
TYPE_MAPPING.put(TYPE_INT, CLASS_INTEGER);
|
||||
TYPE_MAPPING.put(TYPE_LONG, CLASS_LONG);
|
||||
TYPE_MAPPING.put(TYPE_SHORT, CLASS_SHORT);
|
||||
TYPE_MAPPING.put(TYPE_BOOL, CLASS_BOOLEAN);
|
||||
TYPE_MAPPING.put(TYPE_VOID, EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether any method in specified class has specified annotation
|
||||
* @param className class that need to explore
|
||||
* @param annotationName annotation to look for
|
||||
* fit kotlin companion class name to original name
|
||||
* @param name a class name (which could be a companion class)
|
||||
* @return is companion class or not
|
||||
*/
|
||||
public static boolean anyMethodHasAnnotation(String className, String annotationName) {
|
||||
Boolean found = loadedClass.get(new ComparableWeakRef<String>(className));
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
try {
|
||||
ClassNode cn = new ClassNode();
|
||||
new ClassReader(className).accept(cn, 0);
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (mn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (toDotSeparateFullClassName(an.desc).equals(annotationName)) {
|
||||
loadedClass.put(new ComparableWeakRef<String>(className), true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
loadedClass.put(new ComparableWeakRef<String>(className), false);
|
||||
return false;
|
||||
public static boolean isCompanionClassName(String name) {
|
||||
return name.endsWith("$Companion");
|
||||
}
|
||||
|
||||
/**
|
||||
* fit kotlin companion class name to original name
|
||||
* @param name a class name (which could be a companion class)
|
||||
* @return original name
|
||||
*/
|
||||
public static String fitCompanionClassName(String name) {
|
||||
return name.replaceAll("\\$Companion$", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* get test class name from source class name
|
||||
* @param sourceClassName source class name
|
||||
* @return test class name
|
||||
*/
|
||||
public static String getTestClassName(String sourceClassName) {
|
||||
return sourceClassName + ConstPool.TEST_POSTFIX;
|
||||
@@ -83,6 +85,7 @@ public class ClassUtil {
|
||||
/**
|
||||
* get source class name from test class name
|
||||
* @param testClassName test class name
|
||||
* @return source class name
|
||||
*/
|
||||
public static String getSourceClassName(String testClassName) {
|
||||
return testClassName.substring(0, testClassName.length() - ConstPool.TEST_POSTFIX.length());
|
||||
@@ -90,6 +93,8 @@ public class ClassUtil {
|
||||
|
||||
/**
|
||||
* parse method desc, fetch parameter types
|
||||
* @param desc method description
|
||||
* @return list of parameter types
|
||||
*/
|
||||
public static List<Byte> getParameterTypes(String desc) {
|
||||
List<Byte> parameterTypes = new ArrayList<Byte>();
|
||||
@@ -115,6 +120,8 @@ public class ClassUtil {
|
||||
|
||||
/**
|
||||
* parse method desc, fetch return value types
|
||||
* @param desc method description
|
||||
* @return types of return value
|
||||
*/
|
||||
public static String getReturnType(String desc) {
|
||||
int returnTypeEdge = desc.lastIndexOf(PARAM_END);
|
||||
@@ -123,15 +130,28 @@ public class ClassUtil {
|
||||
return desc.substring(returnTypeEdge + 1);
|
||||
} else if (typeChar == TYPE_CLASS) {
|
||||
return desc.substring(returnTypeEdge + 2, desc.length() - 1);
|
||||
} else if (TYPE_MAPPING.containsKey(typeChar)) {
|
||||
return TYPE_MAPPING.get(typeChar);
|
||||
} else if (TYPE_MAPPING.containsKey((byte)typeChar)) {
|
||||
return TYPE_MAPPING.get((byte)typeChar);
|
||||
} else {
|
||||
return "";
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method node to convert primary type to object type
|
||||
* @param type primary type to convert
|
||||
* @return converter method node
|
||||
*/
|
||||
public static MethodInsnNode getPrimaryTypeConvertMethod(Byte type) {
|
||||
String objectType = TYPE_MAPPING.get(type);
|
||||
return (objectType == null) ? null :
|
||||
new MethodInsnNode(INVOKESTATIC, objectType, METHOD_VALUE_OF, toDescriptor(type, objectType), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* convert slash separated name to dot separated name
|
||||
* @param name original name
|
||||
* @return converted name
|
||||
*/
|
||||
public static String toDotSeparatedName(String name) {
|
||||
return name.replace(ConstPool.SLASH, ConstPool.DOT);
|
||||
@@ -139,6 +159,8 @@ public class ClassUtil {
|
||||
|
||||
/**
|
||||
* convert dot separated name to slash separated name
|
||||
* @param name original name
|
||||
* @return converted name
|
||||
*/
|
||||
public static String toSlashSeparatedName(String name) {
|
||||
return name.replace(ConstPool.DOT, ConstPool.SLASH);
|
||||
@@ -146,13 +168,17 @@ public class ClassUtil {
|
||||
|
||||
/**
|
||||
* convert dot separated name to byte code class name
|
||||
* @param className original name
|
||||
* @return converted name
|
||||
*/
|
||||
public static String toByteCodeClassName(String className) {
|
||||
return TYPE_CLASS + toSlashSeparatedName(className) + CLASS_END;
|
||||
return (char)TYPE_CLASS + toSlashSeparatedName(className) + (char)CLASS_END;
|
||||
}
|
||||
|
||||
/**
|
||||
* convert byte code class name to dot separated human readable name
|
||||
* @param className original name
|
||||
* @return converted name
|
||||
*/
|
||||
public static String toDotSeparateFullClassName(String className) {
|
||||
return toDotSeparatedName(className).substring(1, className.length() - 1);
|
||||
@@ -160,14 +186,19 @@ public class ClassUtil {
|
||||
|
||||
/**
|
||||
* convert byte code class name to slash separated human readable name
|
||||
* @param className original name
|
||||
* @return converted name
|
||||
*/
|
||||
public static String toSlashSeparateFullClassName(String className) {
|
||||
return toSlashSeparatedName(className).substring(1, className.length() - 1);
|
||||
}
|
||||
|
||||
private static String toDescriptor(Byte type, String objectType) {
|
||||
return "(" + (char)type.byteValue() + ")L" + objectType + ";";
|
||||
}
|
||||
|
||||
private static boolean isPrimaryType(byte b) {
|
||||
return b == TYPE_BYTE || b == TYPE_CHAR || b == TYPE_DOUBLE || b == TYPE_FLOAT
|
||||
|| b == TYPE_INT || b == TYPE_LONG || b == TYPE_SHORT || b == TYPE_BOOL;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ public class CollectionUtil {
|
||||
* Check two collection has any equaled item
|
||||
* @param collectionLeft the first collection
|
||||
* @param collectionRight the second collection
|
||||
* @return found or not
|
||||
*/
|
||||
public static boolean containsAny(Collection collectionLeft, Collection collectionRight) {
|
||||
public static boolean containsAny(Collection<?> collectionLeft, Collection<?> collectionRight) {
|
||||
for (Object o : collectionLeft) {
|
||||
for (Object i : collectionRight) {
|
||||
if (o.equals(i)) {
|
||||
@@ -26,6 +27,7 @@ public class CollectionUtil {
|
||||
/**
|
||||
* Generate a list of item
|
||||
* @param items elements to add
|
||||
* @return a ArrayList of provided elements
|
||||
*/
|
||||
public static <T> List<T> listOf(T... items) {
|
||||
List<T> list = new ArrayList<T>(items.length);
|
||||
|
||||
@@ -9,6 +9,7 @@ public class StringUtil {
|
||||
* repeat a text many times
|
||||
* @param text content to repeat
|
||||
* @param times count of repeating
|
||||
* @return joined string
|
||||
*/
|
||||
public static String repeat(String text, int times) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -27,7 +27,7 @@ class SourceClassHandlerTest {
|
||||
new InsnNode(ICONST_1),
|
||||
new InsnNode(ICONST_2),
|
||||
new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "substring", "(II)Ljava/lang/String;", false),
|
||||
new MethodInsnNode(INVOKESPECIAL, "com/alibaba/testable/demo/DemoServiceTest", "blackBox", "(Ljava/lang/String;)Lcom/alibaba/testable/demo/BlackBox;", false),
|
||||
new MethodInsnNode(INVOKESPECIAL, "com/alibaba/testable/demo/DemoTest", "blackBox", "(Ljava/lang/String;)Lcom/alibaba/testable/demo/BlackBox;", false),
|
||||
new MethodInsnNode(INVOKEVIRTUAL, "com/alibaba/testable/demo/BlackBox", "callMe", "()Ljava/lang/String;", false),
|
||||
new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "startsWith", "(Ljava/lang/String;)Z", false)
|
||||
};
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.alibaba.testable.agent.transformer;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
|
||||
import static com.alibaba.testable.agent.util.CollectionUtil.listOf;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TestableClassTransformerTest {
|
||||
|
||||
private TestableClassTransformer transformer = new TestableClassTransformer();
|
||||
|
||||
@Test
|
||||
void should_get_annotation_parameter() {
|
||||
AnnotationNode an = new AnnotationNode("");
|
||||
an.values = listOf((Object)"testKey", "testValue", "demoKey", "demoValue");
|
||||
assertEquals("testValue", PrivateAccessor.invoke(transformer, "getAnnotationParameter", an, "testKey", "none"));
|
||||
assertEquals("demoValue", PrivateAccessor.invoke(transformer, "getAnnotationParameter", an, "demoKey", "none"));
|
||||
assertEquals("none", PrivateAccessor.invoke(transformer, "getAnnotationParameter", an, "testValue", "none"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
|
||||
import static com.alibaba.testable.agent.util.CollectionUtil.listOf;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class AnnotationUtilTest {
|
||||
|
||||
@Test
|
||||
void should_get_annotation_parameter() {
|
||||
AnnotationNode an = new AnnotationNode("");
|
||||
an.values = listOf((Object)"testKey", "testValue", "demoKey", "demoValue");
|
||||
assertEquals("testValue", AnnotationUtil.getAnnotationParameter(an, "testKey", "none", String.class));
|
||||
assertEquals("demoValue", AnnotationUtil.getAnnotationParameter(an, "demoKey", "none", String.class));
|
||||
assertEquals("none", AnnotationUtil.getAnnotationParameter(an, "testValue", "none", String.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,21 +2,10 @@ package com.alibaba.testable.agent.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ClassUtilTest {
|
||||
|
||||
@Test
|
||||
void should_able_to_get_annotation() {
|
||||
assertEquals(false, ClassUtil.anyMethodHasAnnotation("class.not.exist", ""));
|
||||
assertEquals(false, ClassUtil.anyMethodHasAnnotation("org.junit.jupiter.api.Assertions", "annotation.not.exist"));
|
||||
assertEquals(true, ClassUtil.anyMethodHasAnnotation("org.junit.jupiter.api.Assertions", "org.apiguardian.api.API"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_parameter_count() {
|
||||
assertEquals(0, ClassUtil.getParameterTypes("()V").size());
|
||||
@@ -40,5 +29,17 @@ class ClassUtilTest {
|
||||
assertEquals("Ljava/lang/String;", ClassUtil.toByteCodeClassName("java.lang.String"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_fit_companion_class_name() {
|
||||
assertEquals("com/intellij/rt/debugger/agent/CaptureAgent$ParamKeyProvider",
|
||||
ClassUtil.fitCompanionClassName("com/intellij/rt/debugger/agent/CaptureAgent$ParamKeyProvider"));
|
||||
assertEquals("com/alibaba/testable/demo/BlackBox",
|
||||
ClassUtil.fitCompanionClassName("com/alibaba/testable/demo/BlackBox"));
|
||||
assertEquals("com/alibaba/testable/demo/BlackBox$Companion",
|
||||
ClassUtil.fitCompanionClassName("com/alibaba/testable/demo/BlackBox$Companion$Companion"));
|
||||
assertEquals("com/alibaba/testable/demo/BlackBox",
|
||||
ClassUtil.fitCompanionClassName("com/alibaba/testable/demo/BlackBox$Companion"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
105
testable-all/pom.xml
Normal file
105
testable-all/pom.xml
Normal file
@@ -0,0 +1,105 @@
|
||||
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.2</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>testable-all</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-processor</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-core</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>release</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>${plugin.source.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>${plugin.javadoc.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>${plugin.gpg.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>${plugin.staging.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<serverId>oss</serverId>
|
||||
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
|
||||
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<distributionManagement>
|
||||
<snapshotRepository>
|
||||
<id>oss</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
|
||||
</snapshotRepository>
|
||||
<repository>
|
||||
<id>oss</id>
|
||||
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
</project>
|
||||
@@ -3,32 +3,25 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<description>Unit test enhancement toolkit</description>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.2</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-core</artifactId>
|
||||
<version>0.2.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>testable-core</name>
|
||||
|
||||
<properties>
|
||||
<java.version>1.6</java.version>
|
||||
<project.compiler.level>1.6</project.compiler.level>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<plugin.compiler.version>3.8.1</plugin.compiler.version>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>sun.jdk</groupId>
|
||||
<artifactId>tools</artifactId>
|
||||
<version>${java.version}</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${java.home}/../lib/tools.jar</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.6.2</version>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
@@ -43,10 +36,84 @@
|
||||
<source>${project.compiler.level}</source>
|
||||
<target>${project.compiler.level}</target>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
<compilerArgument>-proc:none</compilerArgument>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${plugin.surefire.version}</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>release</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>${plugin.source.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>${plugin.javadoc.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>${plugin.gpg.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>${plugin.staging.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<serverId>oss</serverId>
|
||||
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
|
||||
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<distributionManagement>
|
||||
<snapshotRepository>
|
||||
<id>oss</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
|
||||
</snapshotRepository>
|
||||
<repository>
|
||||
<id>oss</id>
|
||||
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
@@ -10,13 +10,15 @@ import java.lang.reflect.Method;
|
||||
*/
|
||||
public class PrivateAccessor {
|
||||
|
||||
private static final String KOTLIN_COMPANION_FIELD = "Companion";
|
||||
|
||||
public static <T> T get(Object ref, String field) {
|
||||
try {
|
||||
Field declaredField = ref.getClass().getDeclaredField(field);
|
||||
declaredField.setAccessible(true);
|
||||
return (T)declaredField.get(ref);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.err.println("Failed to get private field \"" + field + "\": " + e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -27,7 +29,7 @@ public class PrivateAccessor {
|
||||
declaredField.setAccessible(true);
|
||||
declaredField.set(ref, value);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.err.println("Failed to set private field \"" + field + "\": " + e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +42,54 @@ public class PrivateAccessor {
|
||||
return (T)declaredMethod.invoke(ref, args);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.err.println("Failed to invoke private method \"" + method + "\": " + e.toString());
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static <T> T getStatic(Class<?> clazz, String field) {
|
||||
try {
|
||||
Field declaredField = clazz.getDeclaredField(field);
|
||||
declaredField.setAccessible(true);
|
||||
return (T)declaredField.get(null);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to get private static field \"" + field + "\": " + e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> void setStatic(Class<?> clazz, String field, T value) {
|
||||
try {
|
||||
Field declaredField = clazz.getDeclaredField(field);
|
||||
declaredField.setAccessible(true);
|
||||
declaredField.set(null, value);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to set private static field \"" + field + "\": " + e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T invokeStatic(Class<?> clazz, String method, Object... args) {
|
||||
try {
|
||||
Class<?>[] cls = TypeUtil.getClassesFromObjects(args);
|
||||
Method declaredMethod = TypeUtil.getMethodByNameAndParameterTypes(clazz.getDeclaredMethods(), method, cls);
|
||||
if (declaredMethod != null) {
|
||||
declaredMethod.setAccessible(true);
|
||||
return (T)declaredMethod.invoke(null, args);
|
||||
}
|
||||
// fit kotlin companion object, will throw 'NoSuchFieldException' otherwise
|
||||
Field companionClassField = clazz.getDeclaredField(KOTLIN_COMPANION_FIELD);
|
||||
declaredMethod = TypeUtil.getMethodByNameAndParameterTypes(
|
||||
companionClassField.getType().getDeclaredMethods(), method, cls);
|
||||
Object companionInstance = getStatic(clazz, KOTLIN_COMPANION_FIELD);
|
||||
if (declaredMethod != null && companionInstance != null) {
|
||||
declaredMethod.setAccessible(true);
|
||||
return (T)declaredMethod.invoke(companionInstance, args);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to invoke private static method \"" + method + "\": " + e.toString());
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.alibaba.testable.core.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Mark method as mock constructor
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
public @interface MockConstructor {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.alibaba.testable.core.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Mark method as mock method
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
public @interface MockMethod {
|
||||
|
||||
/**
|
||||
* mock specified method instead of method with same name
|
||||
* @return target method name
|
||||
*/
|
||||
String targetMethod() default "";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.alibaba.testable.core.annotation;
|
||||
|
||||
import com.alibaba.testable.core.model.MockDiagnose;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Set extra mock parameter to test class
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
public @interface MockWith {
|
||||
|
||||
/**
|
||||
* switch of mock diagnose information of current test class
|
||||
* @return enable or disable
|
||||
*/
|
||||
MockDiagnose diagnose() default MockDiagnose.DISABLE;
|
||||
|
||||
}
|
||||
@@ -3,17 +3,20 @@ package com.alibaba.testable.core.annotation;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Use marked method to replace the ones in source class
|
||||
* Mark method as mock method
|
||||
* @deprecated will be remove in v0.5.0, use @MockMethod or @MockConstructor instead
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
@Deprecated
|
||||
public @interface TestableMock {
|
||||
|
||||
/**
|
||||
* mock specified method instead of method with same name
|
||||
* @return target method name
|
||||
*/
|
||||
String targetMethod() default "";
|
||||
|
||||
|
||||
@@ -5,12 +5,24 @@ package com.alibaba.testable.core.error;
|
||||
*/
|
||||
public class VerifyFailedError extends AssertionError {
|
||||
|
||||
public VerifyFailedError(int actualCount, int expectedCount) {
|
||||
super(getErrorMessage(actualCount, expectedCount));
|
||||
public VerifyFailedError(String message) {
|
||||
super(getErrorMessage(message));
|
||||
}
|
||||
|
||||
private static String getErrorMessage(int actualCount, int expectedCount) {
|
||||
return "\nExpected times : " + expectedCount + "\nActual times : " + actualCount;
|
||||
public VerifyFailedError(String expected, String actual) {
|
||||
super(getErrorMessage(expected, actual));
|
||||
}
|
||||
|
||||
public VerifyFailedError(String message, String expected, String actual) {
|
||||
super(getErrorMessage(message) + getErrorMessage(expected, actual));
|
||||
}
|
||||
|
||||
private static String getErrorMessage(String message) {
|
||||
return "\n" + message.substring(0, 1).toUpperCase() + message.substring(1);
|
||||
}
|
||||
|
||||
private static String getErrorMessage(String expected, String actual) {
|
||||
return "\nExpected " + expected + "\n Actual " + actual;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.alibaba.testable.core.function;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public interface MatchFunction {
|
||||
|
||||
/**
|
||||
* Judge whether real argument value match exception
|
||||
*
|
||||
* @param value real argument value when mock method invoked
|
||||
* @return match result
|
||||
*/
|
||||
boolean check(Object value);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
package com.alibaba.testable.core.matcher;
|
||||
|
||||
import com.alibaba.testable.core.function.MatchFunction;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class InvokeMatcher {
|
||||
|
||||
public MatchFunction matchFunction;
|
||||
|
||||
private InvokeMatcher(MatchFunction matchFunction) {
|
||||
this.matchFunction = matchFunction;
|
||||
}
|
||||
|
||||
public static InvokeMatcher any(MatchFunction matcher) {
|
||||
return new InvokeMatcher(matcher);
|
||||
}
|
||||
|
||||
public static InvokeMatcher any() {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyString() {
|
||||
return any(String.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyNumber() {
|
||||
return anyTypeOf(Short.class, Integer.class, Long.class, Float.class, Double.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyBoolean() {
|
||||
return any(Boolean.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyByte() {
|
||||
return any(Byte.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyChar() {
|
||||
return any(Character.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyInt() {
|
||||
return any(Integer.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyLong() {
|
||||
return any(Long.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyFloat() {
|
||||
return any(Float.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyDouble() {
|
||||
return any(Double.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyShort() {
|
||||
return any(Short.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyArray() {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return value != null &&
|
||||
value.getClass().isArray();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyArrayOf(final Class<?> clazz) {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return value != null &&
|
||||
value.getClass().isArray() &&
|
||||
value.getClass().getComponentType().equals(clazz);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyList() {
|
||||
return any(List.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyListOf(final Class<?> clazz) {
|
||||
return anyClassWithCollectionOf(List.class, clazz);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anySet() {
|
||||
return any(Set.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anySetOf(final Class<?> clazz) {
|
||||
return anyClassWithCollectionOf(Set.class, clazz);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyMap() {
|
||||
return any(Map.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyMapOf(final Class<?> keyClass, final Class<?> valueClass) {
|
||||
return anyClassWithMapOf(keyClass, valueClass);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyCollection() {
|
||||
return any(Collection.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyCollectionOf(final Class<?> clazz) {
|
||||
return anyClassWithCollectionOf(Collection.class, clazz);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyIterable() {
|
||||
return any(Iterable.class);
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyIterableOf(final Class<?> clazz) {
|
||||
return anyClassWithCollectionOf(Iterable.class, clazz);
|
||||
}
|
||||
|
||||
public static InvokeMatcher any(final Class<?> clazz) {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return value != null && clazz.isAssignableFrom(value.getClass());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher anyTypeOf(final Class<?>... classes) {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
for (Class<?> c : classes) {
|
||||
if (value.getClass().equals(c)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher eq(final Object obj) {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return obj.equals(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher refEq(final Object obj) {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return obj == value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher isNull() {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return value == null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher notNull() {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return value != null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher nullable(final Class<?> clazz) {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return value == null || clazz.isAssignableFrom(value.getClass());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher contains(final String substring) {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return value instanceof String && ((String)value).contains(substring);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher matches(final String regex) {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return value instanceof String && ((String)value).matches(regex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher endsWith(final String suffix) {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return value instanceof String && ((String)value).endsWith(suffix);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static InvokeMatcher startsWith(final String prefix) {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return value instanceof String && ((String)value).startsWith(prefix);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static InvokeMatcher anyClassWithCollectionOf(final Class<?> collectionClass, final Class<?> clazz) {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return value != null &&
|
||||
collectionClass.isAssignableFrom(value.getClass()) &&
|
||||
allElementsHasType((Collection<?>)value, clazz);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static InvokeMatcher anyClassWithMapOf(final Class<?> keyClass, final Class<?> valueClass) {
|
||||
return any(new MatchFunction() {
|
||||
@Override
|
||||
public boolean check(Object value) {
|
||||
return value != null &&
|
||||
Map.class.isAssignableFrom(value.getClass()) &&
|
||||
allElementsHasType((Map<?, ?>)value, keyClass, valueClass);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Because of type erase, there's no way to directly fetch original type of collection template
|
||||
* this could be a temporary solution
|
||||
*/
|
||||
private static boolean allElementsHasType(Map<?, ?> items, Class<?> keyClass, Class<?> valueClass) {
|
||||
for (Map.Entry<?, ?> e : items.entrySet()) {
|
||||
if (!(keyClass.isAssignableFrom(e.getKey().getClass()) &&
|
||||
valueClass.isAssignableFrom(e.getValue().getClass()))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Because of type erase, there's no way to directly fetch original type of collection template
|
||||
* this could be a temporary solution
|
||||
*/
|
||||
private static boolean allElementsHasType(Collection<?> values, Class<?> clazz) {
|
||||
for (Object v : values.toArray()) {
|
||||
if (!clazz.isAssignableFrom(v.getClass())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.alibaba.testable.core.matcher;
|
||||
|
||||
import com.alibaba.testable.core.error.VerifyFailedError;
|
||||
import com.alibaba.testable.core.model.Verification;
|
||||
import com.alibaba.testable.core.util.InvokeRecordUtil;
|
||||
import com.alibaba.testable.core.util.TestableUtil;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class InvokeVerifier {
|
||||
|
||||
private final List<Object[]> records;
|
||||
private Verification lastVerification = null;
|
||||
|
||||
private InvokeVerifier(List<Object[]> records) {
|
||||
this.records = records;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get counter to check whether specified mock method invoked
|
||||
* @param mockMethodName name of a mock method
|
||||
* @return the verifier object
|
||||
*/
|
||||
public static InvokeVerifier verify(String mockMethodName) {
|
||||
String testClass = Thread.currentThread().getStackTrace()[InvokeRecordUtil.INDEX_OF_TEST_CLASS].getClassName();
|
||||
String testCaseName = TestableUtil.currentTestCaseName(testClass);
|
||||
String recordIdentify = InvokeRecordUtil.getInvokeIdentify(mockMethodName, testClass, testCaseName);
|
||||
return new InvokeVerifier(InvokeRecordUtil.getInvokeRecord(recordIdentify));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect mock method invoked with specified parameters
|
||||
* @param args parameters to compare
|
||||
* @return the verifier object
|
||||
*/
|
||||
public InvokeVerifier with(Object... args) {
|
||||
boolean found = false;
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
try {
|
||||
withInternal(args, i);
|
||||
found = true;
|
||||
break;
|
||||
} catch (AssertionError e) {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
throw new VerifyFailedError("has not invoke with " + desc(args));
|
||||
}
|
||||
lastVerification = new Verification(args, false);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect next mock method call was invoked with specified parameters
|
||||
* @param args parameters to compare
|
||||
* @return the verifier object
|
||||
*/
|
||||
public InvokeVerifier withInOrder(Object... args) {
|
||||
withInternal(args, 0);
|
||||
lastVerification = new Verification(args, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect mock method had never invoked with specified parameters
|
||||
* @param args parameters to compare
|
||||
* @return the verifier object
|
||||
*/
|
||||
public InvokeVerifier without(Object... args) {
|
||||
for (Object[] r : records) {
|
||||
if (r.length == args.length) {
|
||||
for (int i = 0; i < r.length; i++) {
|
||||
if (!matches(args[i], r[i])) {
|
||||
break;
|
||||
}
|
||||
if (i == r.length - 1) {
|
||||
// all arguments are equal
|
||||
throw new VerifyFailedError("was invoked with " + desc(args));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect mock method have been invoked specified times
|
||||
* @param expectedCount times to compare
|
||||
* @return the verifier object
|
||||
*/
|
||||
public InvokeVerifier withTimes(int expectedCount) {
|
||||
if (expectedCount != records.size()) {
|
||||
throw new VerifyFailedError("times: " + expectedCount, "times: " + records.size());
|
||||
}
|
||||
lastVerification = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect several consecutive invocations with the same parameters
|
||||
* @param count number of invocations
|
||||
* @return the verifier object
|
||||
*/
|
||||
public InvokeVerifier times(int count) {
|
||||
if (lastVerification == null) {
|
||||
// when used independently, equals to `withTimes()`
|
||||
System.out.println("Warning: [" + TestableUtil.previousStackLocation() + "] using \"times()\" method "
|
||||
+ "without \"with()\" or \"withInOrder()\" is not recommended, please use \"withTimes()\" instead.");
|
||||
return withTimes(count);
|
||||
}
|
||||
if (count < 2) {
|
||||
throw new InvalidParameterException("should only use times() method with count equal or larger than 2.");
|
||||
}
|
||||
for (int i = 0; i < count - 1; i++) {
|
||||
if (lastVerification.inOrder) {
|
||||
withInOrder(lastVerification.parameters);
|
||||
} else {
|
||||
with(lastVerification.parameters);
|
||||
}
|
||||
}
|
||||
lastVerification = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void withInternal(Object[] args, int order) {
|
||||
if (records.isEmpty()) {
|
||||
throw new VerifyFailedError("has not more invoke");
|
||||
}
|
||||
Object[] record = records.get(order);
|
||||
if (record.length != args.length) {
|
||||
throw new VerifyFailedError(desc(args), desc(record));
|
||||
}
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (!(args[i] instanceof InvokeMatcher || args[i].getClass().equals(record[i].getClass()))) {
|
||||
throw new VerifyFailedError("parameter " + (i + 1) + " type mismatch",
|
||||
": " + args[i].getClass(), ": " + record[i].getClass());
|
||||
}
|
||||
if (!matches(args[i], record[i])) {
|
||||
throw new VerifyFailedError("parameter " + (i + 1) + " mismatched", desc(args), desc(record));
|
||||
}
|
||||
}
|
||||
records.remove(order);
|
||||
}
|
||||
|
||||
private boolean matches(Object expectValue, Object realValue) {
|
||||
return expectValue instanceof InvokeMatcher ?
|
||||
((InvokeMatcher) expectValue).matchFunction.check(realValue) :
|
||||
expectValue.equals(realValue);
|
||||
}
|
||||
|
||||
private String desc(Object[] args) {
|
||||
StringBuilder sb = new StringBuilder(": ");
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (i > 0) {
|
||||
sb.append(", ");
|
||||
}
|
||||
sb.append(args[i]);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.alibaba.testable.core.model;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
|
||||
public enum MockDiagnose {
|
||||
|
||||
/**
|
||||
* Be quiet
|
||||
*/
|
||||
DISABLE,
|
||||
|
||||
/**
|
||||
* Print diagnose logs
|
||||
*/
|
||||
ENABLE
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.alibaba.testable.core.model;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class Verification {
|
||||
|
||||
public Object[] parameters;
|
||||
|
||||
public boolean inOrder;
|
||||
|
||||
public Verification(Object[] parameters, boolean inOrder) {
|
||||
this.parameters = parameters;
|
||||
this.inOrder = inOrder;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.alibaba.testable.core.tool;
|
||||
|
||||
import com.alibaba.testable.core.error.VerifyFailedError;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class InvokeCounter {
|
||||
|
||||
private final int actualCount;
|
||||
|
||||
public InvokeCounter(int actualCount) {
|
||||
this.actualCount = actualCount;
|
||||
}
|
||||
|
||||
public void times(int expectedCount) {
|
||||
if (expectedCount != actualCount) {
|
||||
throw new VerifyFailedError(actualCount, expectedCount);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +1,10 @@
|
||||
package com.alibaba.testable.core.tool;
|
||||
|
||||
import com.alibaba.testable.core.util.TestableUtil;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class TestableTool {
|
||||
|
||||
/**
|
||||
* Name of the constructor method
|
||||
*/
|
||||
public static final String CONSTRUCTOR = "<init>";
|
||||
|
||||
/**
|
||||
* Name of current test case method
|
||||
*/
|
||||
@@ -22,14 +15,4 @@ public class TestableTool {
|
||||
*/
|
||||
public static String SOURCE_METHOD;
|
||||
|
||||
/**
|
||||
* Get counter to check whether specified mock method invoked
|
||||
* @param mockMethodName name of a mock method
|
||||
*/
|
||||
public static InvokeCounter verify(String mockMethodName) {
|
||||
String testClass = Thread.currentThread().getStackTrace()[TestableUtil.INDEX_OF_TEST_CLASS].getClassName();
|
||||
String testCaseName = TestableUtil.currentTestCaseName(testClass);
|
||||
return new InvokeCounter(TestableUtil.getInvokeCount(mockMethodName, testCaseName));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class InvokeRecordUtil {
|
||||
|
||||
/**
|
||||
* Mock method name → List of invoke parameters
|
||||
*/
|
||||
private static final Map<String, List<Object[]>> INVOKE_RECORDS = new HashMap<String, List<Object[]>>();
|
||||
private final static String JOINER = "::";
|
||||
|
||||
/**
|
||||
* [0]Thread → [1]TestableUtil/TestableTool → [2]TestClass
|
||||
*/
|
||||
public static final int INDEX_OF_TEST_CLASS = 2;
|
||||
|
||||
/**
|
||||
* Record mock method invoke event
|
||||
* @param args invocation parameters
|
||||
* @param isConstructor whether mocked method is constructor
|
||||
*/
|
||||
public static void recordMockInvoke(Object[] args, boolean isConstructor) {
|
||||
StackTraceElement mockMethodTraceElement = Thread.currentThread().getStackTrace()[INDEX_OF_TEST_CLASS];
|
||||
String mockMethodName = mockMethodTraceElement.getMethodName();
|
||||
String testClass = mockMethodTraceElement.getClassName();
|
||||
String testCaseName = TestableUtil.currentTestCaseName(testClass);
|
||||
String identify = getInvokeIdentify(mockMethodName, testClass, testCaseName);
|
||||
List<Object[]> records = getInvokeRecord(identify);
|
||||
if (isConstructor) {
|
||||
records.add(args);
|
||||
LogUtil.verbose(" Mock constructor invoked \"%s\"", identify);
|
||||
} else {
|
||||
records.add(slice(args, 1));
|
||||
LogUtil.verbose(" Mock method invoked \"%s\"", identify);
|
||||
}
|
||||
INVOKE_RECORDS.put(identify, records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get identify key for mock invocation record
|
||||
* @param mockMethodName mock method name
|
||||
* @param testClass test class name
|
||||
* @param testCaseName test case name
|
||||
* @return identify key
|
||||
*/
|
||||
public static String getInvokeIdentify(String mockMethodName, String testClass, String testCaseName) {
|
||||
return testClass + JOINER + testCaseName + JOINER + mockMethodName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mock method invoke count
|
||||
* @param identify key of invocation record
|
||||
* @return parameters used when specified method invoked in specified test case
|
||||
*/
|
||||
public static List<Object[]> getInvokeRecord(String identify) {
|
||||
List<Object[]> records = INVOKE_RECORDS.get(identify);
|
||||
return (records == null) ? new LinkedList<Object[]>() : records;
|
||||
}
|
||||
|
||||
private static Object[] slice(Object[] args, int firstIndex) {
|
||||
int size = args.length - firstIndex;
|
||||
if (size <= 0) {
|
||||
return new Object[0];
|
||||
}
|
||||
Object[] slicedArgs = new Object[size];
|
||||
System.arraycopy(args, firstIndex, slicedArgs, 0, size);
|
||||
return slicedArgs;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class LogUtil {
|
||||
|
||||
public enum LogLevel {
|
||||
/**
|
||||
* Mute
|
||||
*/
|
||||
LEVEL_MUTE(0),
|
||||
/**
|
||||
* Warn only
|
||||
*/
|
||||
LEVEL_WARN(1),
|
||||
/**
|
||||
* Show diagnose messages
|
||||
*/
|
||||
LEVEL_DIAGNOSE(2),
|
||||
/**
|
||||
* Show detail progress logs
|
||||
*/
|
||||
LEVEL_VERBOSE(3);
|
||||
|
||||
int level;
|
||||
LogLevel(int l) {
|
||||
level = l;
|
||||
}
|
||||
}
|
||||
|
||||
private static LogLevel defaultLogLevel = LogLevel.LEVEL_WARN;
|
||||
private static LogLevel currentLogLevel = LogLevel.LEVEL_WARN;
|
||||
|
||||
public static void verbose(String msg, Object... args) {
|
||||
if (currentLogLevel.level >= LogLevel.LEVEL_VERBOSE.level) {
|
||||
System.out.println(String.format("[VERBOSE] " + msg, args));
|
||||
}
|
||||
}
|
||||
|
||||
public static void diagnose(String msg, Object... args) {
|
||||
if (currentLogLevel.level >= LogLevel.LEVEL_DIAGNOSE.level) {
|
||||
System.out.println(String.format("[DIAGNOSE] " + msg, args));
|
||||
}
|
||||
}
|
||||
|
||||
public static void warn(String msg, Object... args) {
|
||||
if (currentLogLevel.level >= LogLevel.LEVEL_WARN.level) {
|
||||
System.out.println(String.format("[WARN] " + msg, args));
|
||||
}
|
||||
}
|
||||
|
||||
public static void enableDiagnose(boolean enable) {
|
||||
currentLogLevel = enable ? LogLevel.LEVEL_DIAGNOSE : LogLevel.LEVEL_MUTE;
|
||||
}
|
||||
|
||||
public static void setDefaultLevel(LogLevel level) {
|
||||
defaultLogLevel = level;
|
||||
resetLogLevel();
|
||||
}
|
||||
|
||||
public static void resetLogLevel() {
|
||||
currentLogLevel = defaultLogLevel;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +1,35 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
|
||||
import com.alibaba.testable.core.constant.ConstPool;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class TestableUtil {
|
||||
|
||||
private static final Map<String, Integer> INVOKE_RECORDS = new HashMap<String, Integer>();
|
||||
private final static String JOINER = "->";
|
||||
/**
|
||||
* [0]Thread -> [1]TestableUtil/TestableTool -> [2]TestClass
|
||||
* [0]Thread.getStackTrace() → [1]currentSourceMethodName() → [2]MockMethod -> [3]SourceMethod
|
||||
*/
|
||||
public static final int INDEX_OF_TEST_CLASS = 2;
|
||||
|
||||
private static final int INDEX_OF_SOURCE_METHOD = 3;
|
||||
/**
|
||||
* Record mock method invoke event
|
||||
* [0]Thread.getStackTrace() → [1]previousStackLocation() → [2]Invoker -> [3]Caller of invoker
|
||||
*/
|
||||
public static void countMockInvoke() {
|
||||
StackTraceElement mockMethodTraceElement = Thread.currentThread().getStackTrace()[INDEX_OF_TEST_CLASS];
|
||||
String mockMethodName = mockMethodTraceElement.getMethodName();
|
||||
String testClass = mockMethodTraceElement.getClassName();
|
||||
String testCaseName = TestableUtil.currentTestCaseName(testClass);
|
||||
String key = testCaseName + JOINER + mockMethodName;
|
||||
int count = getInvokeCount(mockMethodName, testCaseName);
|
||||
INVOKE_RECORDS.put(key, count + 1);
|
||||
}
|
||||
private static final int INDEX_OF_CALLER_METHOD = 3;
|
||||
/**
|
||||
* [0]Thread.getStackTrace() → [1]markTestCaseBegin() → [2]TestCaseMethod
|
||||
*/
|
||||
private static final int INDEX_OF_TEST_CASE_METHOD = 2;
|
||||
/**
|
||||
* Just a special number to identify test worker thread
|
||||
*/
|
||||
private static final int TEST_WORKER_THREAD_PRIORITY = 55555;
|
||||
|
||||
/**
|
||||
* Get the last visit method in source file
|
||||
* @param testClassRef usually `this` variable of the test class
|
||||
* @return method name
|
||||
*/
|
||||
public static String currentSourceMethodName(Object testClassRef) {
|
||||
Class<?> testClass = testClassRef.getClass();
|
||||
String testClassName = getRealClassName(testClass);
|
||||
String sourceClassName = testClassName.substring(0, testClassName.length() - ConstPool.TEST_POSTFIX.length());
|
||||
String sourceMethod = findLastMethodFromSourceClass(sourceClassName, getMainThread().getStackTrace());
|
||||
if (sourceMethod.isEmpty()) {
|
||||
return findLastMethodFromSourceClass(sourceClassName, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
return sourceMethod;
|
||||
public static String currentSourceMethodName() {
|
||||
return Thread.currentThread().getStackTrace()[INDEX_OF_SOURCE_METHOD].getMethodName();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +39,7 @@ public class TestableUtil {
|
||||
*/
|
||||
public static String currentTestCaseName(Object testClassRef) {
|
||||
Class<?> testClass = testClassRef.getClass();
|
||||
String testClassName = getRealClassName(testClass);
|
||||
String testClassName = getOuterClassName(testClass.getName());
|
||||
return currentTestCaseName(testClassName);
|
||||
}
|
||||
|
||||
@@ -63,47 +49,81 @@ public class TestableUtil {
|
||||
* @return method name
|
||||
*/
|
||||
public static String currentTestCaseName(String testClassName) {
|
||||
StackTraceElement[] stack = getMainThread().getStackTrace();
|
||||
for (int i = stack.length - 1; i >= 0; i--) {
|
||||
if (stack[i].getClassName().equals(testClassName)) {
|
||||
return stack[i].getMethodName();
|
||||
// try current thread
|
||||
String testCaseName = findFirstMethodFromTestClass(testClassName, Thread.currentThread().getStackTrace());
|
||||
if (testCaseName.isEmpty()) {
|
||||
Set<Thread> threads = Thread.getAllStackTraces().keySet();
|
||||
// try find previously marked thread
|
||||
Thread testWorkerThread = findTestWorkerThread(threads);
|
||||
if (testWorkerThread != null) {
|
||||
testCaseName = findFirstMethodFromTestClass(testClassName, testWorkerThread.getStackTrace());
|
||||
if (!testCaseName.isEmpty()) {
|
||||
return testCaseName;
|
||||
}
|
||||
}
|
||||
// travel all possible threads
|
||||
for (Thread t : threads) {
|
||||
testCaseName = findFirstMethodFromTestClass(testClassName, t.getStackTrace());
|
||||
if (!testCaseName.isEmpty()) {
|
||||
t.setPriority(TEST_WORKER_THREAD_PRIORITY);
|
||||
return testCaseName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
return testCaseName;
|
||||
}
|
||||
|
||||
public static int getInvokeCount(String mockMethodName, String testCaseName) {
|
||||
String key = testCaseName + JOINER + mockMethodName;
|
||||
Integer count = INVOKE_RECORDS.get(key);
|
||||
if (count == null) {
|
||||
count = 0;
|
||||
}
|
||||
return count;
|
||||
/**
|
||||
* Get file name and line number of where current method was called
|
||||
* @return in "filename:linenumber" format
|
||||
*/
|
||||
public static String previousStackLocation() {
|
||||
StackTraceElement stack = Thread.currentThread().getStackTrace()[INDEX_OF_CALLER_METHOD];
|
||||
return stack.getFileName() + ":" + stack.getLineNumber();
|
||||
}
|
||||
|
||||
private static String findLastMethodFromSourceClass(String sourceClassName, StackTraceElement[] stack) {
|
||||
for (StackTraceElement element : stack) {
|
||||
if (element.getClassName().equals(sourceClassName)) {
|
||||
return element.getMethodName();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
/**
|
||||
* Do prepare when probable test case start
|
||||
*/
|
||||
public static void markTestCaseBegin() {
|
||||
String testCaseName = Thread.currentThread().getStackTrace()[INDEX_OF_TEST_CASE_METHOD].getMethodName();
|
||||
LogUtil.verbose("Start testcase: " + testCaseName);
|
||||
}
|
||||
|
||||
private static String getRealClassName(Class<?> testClass) {
|
||||
String className = testClass.getName();
|
||||
int posOfInnerClass = className.lastIndexOf('$');
|
||||
return posOfInnerClass > 0 ? className.substring(0, posOfInnerClass) : className;
|
||||
}
|
||||
|
||||
private static Thread getMainThread() {
|
||||
for (Thread t : Thread.getAllStackTraces().keySet()) {
|
||||
if (t.getId() == 1L) {
|
||||
private static Thread findTestWorkerThread(Set<Thread> threads) {
|
||||
for (Thread t : threads) {
|
||||
if (t.getPriority() == TEST_WORKER_THREAD_PRIORITY) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
// usually impossible to go here
|
||||
return Thread.currentThread();
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String findFirstMethodFromTestClass(String testClassName, StackTraceElement[] stack) {
|
||||
for (int i = stack.length - 1; i >= 0; i--) {
|
||||
if (getOuterClassName(stack[i].getClassName()).equals(testClassName)) {
|
||||
return stack[i].getClassName().indexOf('$') > 0 ?
|
||||
// test case using async call
|
||||
getMethodNameFromLambda(stack[i].getClassName()) :
|
||||
// in case of lambda method
|
||||
getMethodNameFromLambda(stack[i].getMethodName());
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String getMethodNameFromLambda(String originName) {
|
||||
int beginOfMethodName = originName.indexOf('$');
|
||||
if (beginOfMethodName < 0) {
|
||||
return originName;
|
||||
}
|
||||
int endOfMethodName = originName.indexOf('$', beginOfMethodName + 1);
|
||||
return originName.substring(beginOfMethodName + 1, endOfMethodName);
|
||||
}
|
||||
|
||||
private static String getOuterClassName(String className) {
|
||||
int posOfInnerClass = className.indexOf('$');
|
||||
return posOfInnerClass > 0 ? className.substring(0, posOfInnerClass) : className;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ public class TypeUtil {
|
||||
|
||||
/**
|
||||
* get classes of parameter objects
|
||||
* @param parameterObjects objects
|
||||
* @return class of those objects
|
||||
*/
|
||||
public static Class<?>[] getClassesFromObjects(Object[] parameterObjects) {
|
||||
Class<?>[] cs = new Class[parameterObjects.length];
|
||||
@@ -20,13 +22,16 @@ public class TypeUtil {
|
||||
|
||||
/**
|
||||
* get method by name and parameter matching
|
||||
* @param availableMethods available methods
|
||||
* @param methodName method to look for
|
||||
* @param parameterTypes class to look for
|
||||
* @return method which match the name and class
|
||||
*/
|
||||
public static Method getMethodByNameAndParameterTypes(Method[] availableMethods,
|
||||
String methodName,
|
||||
Class<?>[] parameterTypes) {
|
||||
for (Method m : availableMethods) {
|
||||
if (m.getName().equals(methodName) &&
|
||||
typeEquals(m.getParameterTypes(), parameterTypes)) {
|
||||
if (m.getName().equals(methodName) && typeEquals(m.getParameterTypes(), parameterTypes)) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
@@ -35,8 +40,11 @@ public class TypeUtil {
|
||||
|
||||
/**
|
||||
* type equals
|
||||
* @param classesLeft class to be compared
|
||||
* @param classesRight class to compare
|
||||
* @return whether all class equals
|
||||
*/
|
||||
public static boolean typeEquals(Class<?>[] classesLeft, Class<?>[] classesRight) {
|
||||
private static boolean typeEquals(Class<?>[] classesLeft, Class<?>[] classesRight) {
|
||||
if (classesLeft.length != classesRight.length) {
|
||||
return false;
|
||||
}
|
||||
@@ -53,6 +61,7 @@ public class TypeUtil {
|
||||
* fuzzy equal
|
||||
* @param factTypes fact types (can be primary type)
|
||||
* @param userTypes user types
|
||||
* @return whether all class equals
|
||||
*/
|
||||
private static boolean fuzzyEqual(Class<?> factTypes, Class<?> userTypes) {
|
||||
return (factTypes.equals(int.class) && userTypes.equals(Integer.class)) ||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
com.alibaba.testable.core.processor.EnablePrivateAccessProcessor
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class InvokeRecordUtilTest {
|
||||
|
||||
@Test
|
||||
void should_slice_array() {
|
||||
Object[] args = new Object[]{"1", "2", "3"};
|
||||
Object[] slicedArgs = PrivateAccessor.invokeStatic(InvokeRecordUtil.class, "slice", args, 1);
|
||||
assertEquals(2, slicedArgs.length);
|
||||
assertEquals("2", slicedArgs[0]);
|
||||
assertEquals("3", slicedArgs[1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TestableUtilTest {
|
||||
|
||||
@Test
|
||||
void should_get_outer_class_name() {
|
||||
assertEquals("com.alibaba.testable.demo.DemoMockTest",
|
||||
PrivateAccessor.<String>invokeStatic(TestableUtil.class, "getOuterClassName", "com.alibaba.testable.demo.DemoMockTest$should_able_to_get_source_method_name$1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_get_method_name_from_lambda_class_or_method() {
|
||||
assertEquals("should_able_to_get_source_method_name",
|
||||
PrivateAccessor.<String>invokeStatic(TestableUtil.class, "getMethodNameFromLambda", "com.alibaba.testable.demo.DemoMockTest$should_able_to_get_source_method_name$1"));
|
||||
assertEquals("should_able_to_get_source_method_name",
|
||||
PrivateAccessor.<String>invokeStatic(TestableUtil.class, "getMethodNameFromLambda", "lambda$should_able_to_get_source_method_name$0"));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user