mirror of
https://github.com/alibaba/testable-mock.git
synced 2026-08-21 02:33:31 +08:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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.
|
||||
20
README.md
20
README.md
@@ -1,9 +1,10 @@
|
||||
# Testable
|
||||
# TestableMock
|
||||
|
||||
基于代码和字节码增强的Java单元测试辅助工具,包含以下功能:
|
||||
换种思路写Mock,让单元测试更简单。
|
||||
|
||||
- 使单元测试能直接调用和访问被测类的私有成员,解决私有方法无法测试的问题
|
||||
- 使被测类的任意方法调用快速替换为Mock,实现"指哪换哪",解决传统Mock工具使用繁琐的问题
|
||||
无需初始化,不挑测试框架,甭管要换的方法是被测类的私有方法、静态方法还是其他任何类的成员方法,也甭管要换的对象是怎么创建的。写好Mock方法,加个`@TestableMock`注解,一切统统搞定。
|
||||
|
||||
文档:https://alibaba.github.io/testable-mock/
|
||||
|
||||
## 目录结构
|
||||
|
||||
@@ -22,8 +23,15 @@
|
||||
|
||||
主项目使用JDK 1.6+和Maven 3+版本构建,其中`demo`子项目需要JDK 1.8+版本。
|
||||
|
||||
由于`Testable`的测试也用到了`Testable`本身,本地首次构建时候需要使用`install`而不能只做`package`:
|
||||
|
||||
```bash
|
||||
mvn clean install
|
||||
```
|
||||
|
||||
## 本地生成文档
|
||||
|
||||
```bash
|
||||
docsify serve docs
|
||||
```
|
||||
|
||||
> Testable文档使用`docsify`工具生成,构建前请安装[nodejs](https://nodejs.org/en/download/)运行时,并使用`npm install -g docsify`命令安装文档生成工具。
|
||||
|
||||
|
||||
23
demo/java-demo/build.gradle
Normal file
23
demo/java-demo/build.gradle
Normal file
@@ -0,0 +1,23 @@
|
||||
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-processor:0.3.2')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.3.2')
|
||||
testRuntimeOnly('com.alibaba.testable:testable-agent:0.3.2')
|
||||
}
|
||||
|
||||
test {
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -1,105 +1,92 @@
|
||||
<?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.2-SNAPSHOT</testable.version>
|
||||
</properties>
|
||||
<properties>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<testable.version>0.3.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-processor</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.6.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-processor</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.junit.vintage</groupId>
|
||||
<artifactId>junit-vintage-engine</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<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,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,14 +1,12 @@
|
||||
package com.alibaba.testable.demo.service;
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.model.Box;
|
||||
import org.springframework.stereotype.Service;
|
||||
import sun.net.www.http.HttpClient;
|
||||
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class DemoMockService {
|
||||
public class DemoMock {
|
||||
|
||||
/**
|
||||
* method with new operation
|
||||
@@ -39,15 +37,6 @@ public class DemoMockService {
|
||||
return BlackBox.secretBox();
|
||||
}
|
||||
|
||||
/**
|
||||
* method with override method invoke
|
||||
*/
|
||||
public Box putBox() {
|
||||
Box box = new BlackBox("");
|
||||
box.put("data");
|
||||
return box;
|
||||
}
|
||||
|
||||
/**
|
||||
* two methods invoke same private method
|
||||
*/
|
||||
@@ -60,7 +49,7 @@ public class DemoMockService {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
|
||||
public class BlackBox implements Box {
|
||||
|
||||
private String data;
|
||||
|
||||
@Override
|
||||
public void put(String something) {
|
||||
data = something;
|
||||
}
|
||||
public class BlackBox extends Box implements Color {
|
||||
|
||||
public BlackBox(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public String get() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public static BlackBox secretBox() {
|
||||
return new BlackBox("secret");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String something) {
|
||||
data = something;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColor() {
|
||||
return "black";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
|
||||
public interface Box {
|
||||
abstract public class Box {
|
||||
|
||||
void put(String something);
|
||||
protected String data;
|
||||
|
||||
abstract public void put(String something);
|
||||
|
||||
public String get() {
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
|
||||
public interface Color {
|
||||
|
||||
String getColor();
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.alibaba.testable.demo.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DemoPrivateAccessService {
|
||||
|
||||
private int count;
|
||||
|
||||
/**
|
||||
* private method
|
||||
*/
|
||||
private String privateFunc(String s, int i) {
|
||||
return s + " - " + i;
|
||||
}
|
||||
|
||||
/**
|
||||
* method with private field access
|
||||
*/
|
||||
public String privateFieldAccessFunc() {
|
||||
count += 2;
|
||||
return String.valueOf(count);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class DemoApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock;
|
||||
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.*;
|
||||
|
||||
class DemoInheritTest {
|
||||
|
||||
@TestableMock(targetMethod = "put")
|
||||
private void put_into_box(Box self, String something) {
|
||||
self.put("put_" + something + "_into_box");
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "put")
|
||||
private void put_into_blackbox(BlackBox self, String something) {
|
||||
self.put("put_" + something + "_into_blackbox");
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "get")
|
||||
private String get_from_box(Box self) {
|
||||
return "get_from_box";
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "get")
|
||||
private String get_from_blackbox(BlackBox self) {
|
||||
return "get_from_blackbox";
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "getColor")
|
||||
private String get_color_from_color(Color self) {
|
||||
return "color_from_color";
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "getColor")
|
||||
private String get_color_from_blackbox(BlackBox self) {
|
||||
return "color_from_blackbox";
|
||||
}
|
||||
|
||||
private DemoInherit demoInherit = new DemoInherit();
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_call_sub_object_method_by_parent_object() throws Exception {
|
||||
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() throws Exception {
|
||||
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() throws Exception {
|
||||
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() throws Exception {
|
||||
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() throws Exception {
|
||||
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() throws Exception {
|
||||
String color = demoInherit.getColorViaBox();
|
||||
verify("get_color_from_blackbox").withTimes(1);
|
||||
assertEquals("color_from_blackbox", color);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock;
|
||||
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;
|
||||
|
||||
class DemoMatcherTest {
|
||||
|
||||
private DemoMatcher demoMatcher = new DemoMatcher();
|
||||
|
||||
@TestableMock(targetMethod = "methodToBeMocked")
|
||||
private void methodWithoutArgument(DemoMatcher self) {}
|
||||
|
||||
@TestableMock(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArguments(DemoMatcher self, Object a1, Object a2) {}
|
||||
|
||||
@TestableMock(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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
package com.alibaba.testable.demo.service;
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.model.Box;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class DemoMockServiceTest {
|
||||
class DemoMockTest {
|
||||
|
||||
@TestableMock(targetMethod = CONSTRUCTOR)
|
||||
private BlackBox createBlackBox(String text) {
|
||||
@@ -18,7 +18,7 @@ class DemoMockServiceTest {
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
private String innerFunc(DemoMockService self, String text) {
|
||||
private String innerFunc(DemoMock self, String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
|
||||
@@ -43,12 +43,7 @@ class DemoMockServiceTest {
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
private void put(Box self, String something) {
|
||||
self.put("put_" + something + "_mocked");
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
private String callFromDifferentMethod(DemoMockService self) {
|
||||
private String callFromDifferentMethod(DemoMock self) {
|
||||
if (TEST_CASE.equals("should_able_to_get_test_case_name")) {
|
||||
return "mock_special";
|
||||
}
|
||||
@@ -58,23 +53,23 @@ class DemoMockServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
private DemoMockService demoService = new DemoMockService();
|
||||
private DemoMock demoMock = new DemoMock();
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_new_object() throws Exception {
|
||||
assertEquals("mock_something", demoService.newFunc());
|
||||
assertEquals("mock_something", demoMock.newFunc());
|
||||
verify("createBlackBox").with("something");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello"));
|
||||
assertEquals("{ \"res\": \"mock_hello\"}", demoMock.outerFunc("hello"));
|
||||
verify("innerFunc").with("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_common_method() throws Exception {
|
||||
assertEquals("trim_string__sub_string__false", demoService.commonFunc());
|
||||
assertEquals("trim_string__sub_string__false", demoMock.commonFunc());
|
||||
verify("trim").withTimes(1);
|
||||
verify("sub").withTimes(1);
|
||||
verify("startsWith").withTimes(1);
|
||||
@@ -82,33 +77,26 @@ class DemoMockServiceTest {
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_static_method() throws Exception {
|
||||
assertEquals("not_secret_box", demoService.getBox().get());
|
||||
assertEquals("not_secret_box", demoMock.getBox().get());
|
||||
verify("secretBox").withTimes(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_override_method() throws Exception {
|
||||
BlackBox box = (BlackBox)demoService.putBox();
|
||||
verify("put").withTimes(1);
|
||||
assertEquals("put_data_mocked", box.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_source_method_name() throws Exception {
|
||||
// synchronous
|
||||
assertEquals("mock_one_mock_others", demoService.callerOne() + "_" + demoService.callerTwo());
|
||||
assertEquals("mock_one_mock_others", demoMock.callerOne() + "_" + demoMock.callerTwo());
|
||||
// asynchronous
|
||||
assertEquals("mock_one_mock_others",
|
||||
Executors.newSingleThreadExecutor().submit(() -> demoService.callerOne() + "_" + demoService.callerTwo()).get());
|
||||
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", demoService.callerOne());
|
||||
assertEquals("mock_special", demoMock.callerOne());
|
||||
// asynchronous
|
||||
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoService.callerOne()).get());
|
||||
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoMock.callerOne()).get());
|
||||
verify("callFromDifferentMethod").withTimes(2);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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;
|
||||
|
||||
@EnablePrivateAccess
|
||||
class DemoPrivateAccessTest {
|
||||
|
||||
private DemoPrivateAccess demoPrivateAccess = new DemoPrivateAccess();
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_method() throws Exception {
|
||||
assertEquals("hello - 1", demoPrivateAccess.privateFunc("hello", 1));
|
||||
assertEquals("hello - 1", PrivateAccessor.invoke(demoPrivateAccess, "privateFunc", "hello", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_field() throws Exception {
|
||||
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() throws Exception {
|
||||
//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() throws Exception {
|
||||
//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() throws Exception {
|
||||
demoPrivateAccess.pi = 4.13;
|
||||
assertEquals(4.13, demoPrivateAccess.pi);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.alibaba.testable.demo.service;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@EnablePrivateAccess
|
||||
class DemoPrivateAccessServiceTest {
|
||||
|
||||
private DemoPrivateAccessService demoService = new DemoPrivateAccessService();
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_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_mock_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"));
|
||||
}
|
||||
|
||||
}
|
||||
34
demo/kotlin-demo/build.gradle.kts
Normal file
34
demo/kotlin-demo/build.gradle.kts
Normal file
@@ -0,0 +1,34 @@
|
||||
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-processor:0.3.2")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.3.2")
|
||||
testRuntimeOnly("com.alibaba.testable:testable-agent:0.3.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,40 +2,30 @@
|
||||
<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.2-SNAPSHOT</testable.version>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<testable.version>0.3.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>
|
||||
@@ -44,27 +34,28 @@
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.6.2</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 +77,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 +101,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,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,62 @@
|
||||
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)
|
||||
methodToBeMocked(listOf(1), setOf(1.0f))
|
||||
methodToBeMocked(1.0, mapOf(1 to 1.0f))
|
||||
methodToBeMocked(floatList, floatList)
|
||||
methodToBeMocked(longArray)
|
||||
methodToBeMocked(arrayOf(1.0, 2.0))
|
||||
}
|
||||
|
||||
fun callMethodWithStringArgument() {
|
||||
methodToBeMocked("hello", "world")
|
||||
methodToBeMocked("testable", "mock")
|
||||
methodToBeMocked(arrayOf("demo"))
|
||||
}
|
||||
|
||||
fun callMethodWithObjectArgument() {
|
||||
methodToBeMocked(BlackBox("hello"), BlackBox("world"))
|
||||
methodToBeMocked(BlackBox("demo"), null)
|
||||
methodToBeMocked(null, BlackBox("demo"))
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,16 @@
|
||||
package com.alibaba.testable.demo.service
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.Box
|
||||
import com.alibaba.testable.demo.model.ColorBox
|
||||
import org.springframework.stereotype.Service
|
||||
import sun.net.www.http.HttpClient
|
||||
import java.net.URL
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
|
||||
@Service
|
||||
class DemoMockService {
|
||||
class DemoMock {
|
||||
|
||||
/**
|
||||
* method with new operation
|
||||
*/
|
||||
fun newFunc(): String {
|
||||
fun newFunc(): String? {
|
||||
return BlackBox("something").get()
|
||||
}
|
||||
|
||||
@@ -40,15 +36,6 @@ class DemoMockService {
|
||||
return ColorBox.createBox("Red", BlackBox.secretBox())
|
||||
}
|
||||
|
||||
/**
|
||||
* method with override method invoke
|
||||
*/
|
||||
fun putBox(): Box {
|
||||
val box: Box = BlackBox("")
|
||||
box.put("data")
|
||||
return box
|
||||
}
|
||||
|
||||
/**
|
||||
* two methods invoke same private method
|
||||
*/
|
||||
@@ -60,7 +47,9 @@ class DemoMockService {
|
||||
return callFromDifferentMethod()
|
||||
}
|
||||
|
||||
private fun innerFunc(s: String) = HttpClient.New(URL("http:/xxx/$s")).urlFile
|
||||
private fun innerFunc(s: String): String {
|
||||
return Files.readAllLines(Paths.get("/a-not-exist-file")).joinToString()
|
||||
}
|
||||
|
||||
private fun callFromDifferentMethod() = "realOne"
|
||||
}
|
||||
@@ -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,26 +1,29 @@
|
||||
package com.alibaba.testable.demo.model
|
||||
|
||||
|
||||
class BlackBox(private var data: String) : Box {
|
||||
class BlackBox(var input: String) : Box(), Color {
|
||||
|
||||
init {
|
||||
this.content = input
|
||||
}
|
||||
|
||||
override fun put(something: String) {
|
||||
data = something
|
||||
content = something
|
||||
}
|
||||
|
||||
fun get(): String {
|
||||
return data
|
||||
override val color: String
|
||||
get() = "black"
|
||||
|
||||
fun trim(): String? {
|
||||
return content?.trim()
|
||||
}
|
||||
|
||||
fun trim(): String {
|
||||
return data.trim()
|
||||
}
|
||||
|
||||
fun substring(from: Int, to: Int): String {
|
||||
return data.substring(from, to)
|
||||
fun substring(from: Int, to: Int): String? {
|
||||
return content?.substring(from, to)
|
||||
}
|
||||
|
||||
fun startsWith(prefix: String): Boolean {
|
||||
return data.startsWith(prefix)
|
||||
return content?.startsWith(prefix) == true
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package com.alibaba.testable.demo.model
|
||||
|
||||
interface Box {
|
||||
abstract class Box {
|
||||
|
||||
fun put(something: String)
|
||||
var content: String? = null
|
||||
|
||||
abstract fun put(something: String)
|
||||
|
||||
open fun get(): String? {
|
||||
return content
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.alibaba.testable.demo.model
|
||||
|
||||
interface Color {
|
||||
|
||||
val color: String
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.alibaba.testable.demo.service
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
|
||||
@Service
|
||||
class DemoPrivateAccessService {
|
||||
|
||||
private var count = 0
|
||||
|
||||
/**
|
||||
* private method
|
||||
*/
|
||||
private fun privateFunc(s: String, i: Int): String {
|
||||
return "$s - $i"
|
||||
}
|
||||
|
||||
/**
|
||||
* method with private field access
|
||||
*/
|
||||
fun privateFieldAccessFunc(): String {
|
||||
count += 2
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class DemoApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock
|
||||
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
|
||||
|
||||
internal class DemoInheritTest {
|
||||
|
||||
@TestableMock(targetMethod = "put")
|
||||
private fun put_into_box(self: Box, something: String) {
|
||||
self.put("put_" + something + "_into_box")
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "put")
|
||||
private fun put_into_blackbox(self: BlackBox, something: String) {
|
||||
self.put("put_" + something + "_into_blackbox")
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "get")
|
||||
private fun get_from_box(self: Box): String {
|
||||
return "get_from_box"
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "get")
|
||||
private fun get_from_blackbox(self: BlackBox): String {
|
||||
return "get_from_blackbox"
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "getColor")
|
||||
private fun get_color_from_color(self: Color): String {
|
||||
return "color_from_color"
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "getColor")
|
||||
private fun get_color_from_blackbox(self: BlackBox): String {
|
||||
return "color_from_blackbox"
|
||||
}
|
||||
|
||||
private val demoInherit = DemoInherit()
|
||||
|
||||
@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,82 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock
|
||||
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
|
||||
|
||||
|
||||
internal class DemoMatcherTest {
|
||||
|
||||
@TestableMock(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithoutArgument(self: DemoMatcher) {
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArguments(self: DemoMatcher, a1: Any, a2: Any) {
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArrayArgument(self: DemoMatcher, a: Array<Any>) {
|
||||
}
|
||||
|
||||
private val demoMatcher = DemoMatcher()
|
||||
|
||||
@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))
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyList(), InvokeMatcher.anySetOf(Float::class.javaObjectType))
|
||||
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(3)
|
||||
|
||||
demoMatcher.callMethodWithNumberArguments()
|
||||
var gotError = false
|
||||
try {
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(4)
|
||||
} catch (e: VerifyFailedError) {
|
||||
gotError = true
|
||||
}
|
||||
if (!gotError) {
|
||||
Assertions.fail<Any>()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
package com.alibaba.testable.demo.service
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import com.alibaba.testable.core.tool.TestableTool.*
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.Box
|
||||
import com.alibaba.testable.demo.model.ColorBox
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
internal class DemoMockServiceTest {
|
||||
internal class DemoMockTest {
|
||||
|
||||
@TestableMock(targetMethod = CONSTRUCTOR)
|
||||
private fun createBlackBox(text: String) = BlackBox("mock_$text")
|
||||
|
||||
@TestableMock
|
||||
private fun innerFunc(self: DemoMockService, text: String) = "mock_$text"
|
||||
private fun innerFunc(self: DemoMock, text: String) = "mock_$text"
|
||||
|
||||
@TestableMock
|
||||
private fun trim(self: BlackBox) = "trim_string"
|
||||
@@ -38,12 +37,7 @@ internal class DemoMockServiceTest {
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
private fun put(self: Box, something: String) {
|
||||
self.put("put_" + something + "_mocked")
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
private fun callFromDifferentMethod(self: DemoMockService): String {
|
||||
private fun callFromDifferentMethod(self: DemoMock): String {
|
||||
return if (TEST_CASE == "should_able_to_get_test_case_name") {
|
||||
"mock_special"
|
||||
} else {
|
||||
@@ -54,23 +48,23 @@ internal class DemoMockServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
private val demoService = DemoMockService()
|
||||
private val demoMock = DemoMock()
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_new_object() {
|
||||
assertEquals("mock_something", demoService.newFunc())
|
||||
assertEquals("mock_something", demoMock.newFunc())
|
||||
verify("createBlackBox").with("something")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_member_method() {
|
||||
assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello"))
|
||||
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", demoService.commonFunc())
|
||||
assertEquals("trim_string__sub_string__false", demoMock.commonFunc())
|
||||
verify("trim").withTimes(1)
|
||||
verify("sub").withTimes(1)
|
||||
verify("startsWith").withTimes(1)
|
||||
@@ -78,25 +72,18 @@ internal class DemoMockServiceTest {
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_static_method() {
|
||||
assertEquals("White_not_secret_box", demoService.getBox().get())
|
||||
assertEquals("White_not_secret_box", demoMock.getBox().get())
|
||||
verify("secretBox").withTimes(1)
|
||||
verify("createBox").withTimes(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_override_method() {
|
||||
val box = demoService.putBox() as BlackBox
|
||||
verify("put").withTimes(1)
|
||||
assertEquals("put_data_mocked", box.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_get_source_method_name() {
|
||||
// synchronous
|
||||
assertEquals("mock_one_mock_others", demoService.callerOne() + "_" + demoService.callerTwo())
|
||||
assertEquals("mock_one_mock_others", demoMock.callerOne() + "_" + demoMock.callerTwo())
|
||||
// asynchronous
|
||||
assertEquals("mock_one_mock_others", Executors.newSingleThreadExecutor().submit<String> {
|
||||
demoService.callerOne() + "_" + demoService.callerTwo()
|
||||
demoMock.callerOne() + "_" + demoMock.callerTwo()
|
||||
}.get())
|
||||
verify("callFromDifferentMethod").withTimes(4)
|
||||
}
|
||||
@@ -104,10 +91,10 @@ internal class DemoMockServiceTest {
|
||||
@Test
|
||||
fun should_able_to_get_test_case_name() {
|
||||
// synchronous
|
||||
assertEquals("mock_special", demoService.callerOne())
|
||||
assertEquals("mock_special", demoMock.callerOne())
|
||||
// asynchronous
|
||||
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit<String> {
|
||||
demoService.callerOne()
|
||||
demoMock.callerOne()
|
||||
}.get())
|
||||
verify("callFromDifferentMethod").withTimes(2)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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
|
||||
|
||||
|
||||
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,23 +0,0 @@
|
||||
package com.alibaba.testable.demo.service
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
|
||||
internal class DemoPrivateAccessServiceTest {
|
||||
|
||||
private val demoService = DemoPrivateAccessService()
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_private_method() {
|
||||
assertEquals("hello - 1", PrivateAccessor.invoke(demoService, "privateFunc", "hello", 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_private_field() {
|
||||
PrivateAccessor.set(demoService, "count", 3)
|
||||
assertEquals("5", demoService.privateFieldAccessFunc())
|
||||
assertEquals(5, PrivateAccessor.get(demoService, "count"))
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.alibaba.testable.demo.util
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import com.alibaba.testable.core.annotation.TestableMock
|
||||
import com.alibaba.testable.core.tool.TestableTool.verify
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
|
||||
class PathUtilTest {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
常见用户问题
|
||||
---
|
||||
|
||||
**1. 如何Mock被测类中通过`@Autowired`初始化的字段?**
|
||||
|
||||
直接创建被测类对象,然后利用`Testable`访问私有成员的能力直接给这些字段赋值即可。
|
||||
|
||||
**2. 通过<u>接口对象或基类对象</u>指向派生类的实例,调用执行了派生类实现的方法。使用`@TestableMock`定义Mock方法时,首个参数类型应该用 接口/基类 还是 派生类?**
|
||||
|
||||
应该使用 接口/基类 类型,参见`should_able_to_mock_override_method`测试用例。
|
||||
41
docs/index.html
Normal file
41
docs/index.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>TestableMock</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="description" content="Description">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">Loading...</div>
|
||||
<script>
|
||||
window.$docsify = {
|
||||
name: 'Testable',
|
||||
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>
|
||||
@@ -1,46 +0,0 @@
|
||||
已知问题
|
||||
---
|
||||
|
||||
**1. 访问私有方法或私有成员代码在IDE提示语法错误**
|
||||
|
||||
使用`@EnablePrivateAccessor`注解后访问私有方法或成员变量,虽然能正常通过编译,但在IDE上依然会提示语法错误。
|
||||
这个问题与使用`Lombok`工具库后使用生成的`getter`和`setter`会被IDE报语法错误一样,需要通过IDE插件来解决。
|
||||
当前`Testable`尚未提供相关插件。也可以改用`PrivateAccessor`工具类来访问私有成员,来避免IDE的异常信息。
|
||||
|
||||
**2. 通过IDE运行单个测试用例时,Mock功能失效**
|
||||
|
||||
这是由于IDE运行单个测试用例时只会运行`maven-surefire-plugin`插件,跳过了`testable-maven-plugin`插件执行,导致Mock功能所需的JavaAgent没有随测试启动。
|
||||
|
||||
解决方法有两种:
|
||||
|
||||
**方法一**:在单元测试配置的"虚拟机参数(VM Option)"属性值末尾添加JavaAgent启动参数:`-javaagent:${HOME}/.m2/repository/com/alibaba/testable/testable-agent/0.2.2/testable-agent-0.2.2.jar`
|
||||
|
||||
> PS:请将路径中的版本号替换成实际使用的版本号
|
||||
|
||||

|
||||
|
||||
**方法二**:不使用`testable-maven-plugin`插件,直接配置JavaAgent参数到`maven-surefire-plugin`插件上。(`JMockit`也是使用了这种方法)配置方法为:
|
||||
|
||||
> PS:请将路径中的版本号替换成实际使用的版本号
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/0.2.2/testable-agent-0.2.2.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
用这种方法需要注意,如果项目同时还使用了`Jacoco`的`on-the-fly`模式(默认模式)统计单元测试覆盖率,则需要在参数中再添加一个`@{argLine}`参数,完整配置如下:
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/0.2.2/testable-agent-0.2.2.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
```
|
||||
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工具**。
|
||||
|
||||

|
||||
28
docs/zh-cn/doc/frequency-asked-questions.md
Normal file
28
docs/zh-cn/doc/frequency-asked-questions.md
Normal file
@@ -0,0 +1,28 @@
|
||||
常见使用问题
|
||||
---
|
||||
|
||||
#### 1. 如何Mock被测类中通过`@Autowired`初始化的字段?
|
||||
|
||||
直接创建被测类对象,然后利用`TestableMock`访问私有成员的能力直接给这些字段赋值即可。
|
||||
|
||||
#### 2. 父类变量指向子类对象时,如何实现Mock方法?
|
||||
|
||||
在代码中,经常会有使用<u>接口变量或父类变量</u>指向子类实例,调用父类或子类方法的情况。
|
||||
|
||||
这时候遵循一个原则,Mock方法的首个参数类型**始终与发起调用的变量类型一致**。
|
||||
|
||||
因此,不论被调用方法来自父类还是子类,也不论子类是否覆写该方法,Mock方法的首个参数类型都应该使用变量自身的接口或父类类型。
|
||||
|
||||
参见Java和Kotlin示例中`DemoInheritTest`测试类的用例。
|
||||
|
||||
#### 3. 在Kotlin项目对`String`类中的方法进行Mock不生效?
|
||||
|
||||
Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.String`。但在构建生成自字节码的时候又会被替换为Java的`java.lang.String`类,因此无论将Mock目标写为`kotlin.String`或`java.lang.String`均无法正常匹配到原始的被调用方法。
|
||||
|
||||
实际场景中需要对`String`类中的方法进行Mock的场景很少,`TestableMock`暂未对这种情况做特别处理。
|
||||
|
||||
#### 4. `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)` → 匹配符合指定表达式的值
|
||||
19
docs/zh-cn/doc/private-accessor.md
Normal file
19
docs/zh-cn/doc/private-accessor.md
Normal file
@@ -0,0 +1,19 @@
|
||||
访问私有成员字段和方法
|
||||
---
|
||||
|
||||
如今关于私有方法是否应该做单元测试的争论正逐渐消停,开发者的普遍实践已经给出事实答案。通过公有方法间接测私有方法在很多情况下难以进行,开发者们更愿意通过修改方法可见性的办法来让原本私有的方法在测试用例中变得可测。
|
||||
|
||||
此外,在单元测试中时常会需要对被测对象进行特定的成员字段初始化,但有时由于被测类的构造方法限制,使得无法便捷的对这些字段进行赋值。那么,能否在不破坏被测类型封装的情况下,允许单元测试用例内的代码直接访问被测类的私有方法和成员变量呢?TestableMock提供了一种简单的解决方案。
|
||||
|
||||
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
|
||||
|
||||
- 调用被测类的私有方法
|
||||
- 读取被测类的私有成员
|
||||
- 修改被测类的私有成员
|
||||
- 修改被测类的常量成员(使用final修饰的成员)
|
||||
|
||||
访问和修改私有、常量成员时,IDE可能会提示语法有误,但编译器将能够正常运行测试。(使用编译期代码增强,目前仅实现了Java语言的适配)
|
||||
|
||||
若不希望看到IDE的语法错误提醒,或是在非Java语言的JVM工程(譬如Kotlin语言)里,也可以借助`PrivateAccessor`工具类来实现私有成员的访问。
|
||||
|
||||
效果见`java-demo`和`kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
|
||||
@@ -1,5 +1,16 @@
|
||||
# Release Note
|
||||
|
||||
## 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+
|
||||
85
docs/zh-cn/doc/setup.md
Normal file
85
docs/zh-cn/doc/setup.md
Normal file
@@ -0,0 +1,85 @@
|
||||
使用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-processor`依赖和`maven-surefire-plugin`配置,具体方法如下。
|
||||
|
||||
建议先添加一个标识TestableMock版本的`property`,便于统一管理:
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.3.2</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
在`dependencies`列表添加TestableMock依赖:
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-processor</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-agent</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-processor:0.3.2')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.3.2')
|
||||
testRuntimeOnly('com.alibaba.testable:testable-agent:0.3.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)文件。
|
||||
90
docs/zh-cn/doc/test-void-method.md
Normal file
90
docs/zh-cn/doc/test-void-method.md
Normal file
@@ -0,0 +1,90 @@
|
||||
测试无返回值的方法
|
||||
---
|
||||
|
||||
如何对void类型的方法进行测试一直是许多单元测试框架在悄悄回避的话题,由于以往的单元测试手段主要是对被测单元的返回结果进行校验,当遇到方法没有返回值时就会变得无从下手。
|
||||
|
||||
从功能的角度来说,虽然void方法不返回任何值,但它的执行一定会对外界产生某些潜在影响,我们将其称为方法的"副作用",比如:
|
||||
|
||||
1. 初始化某些外部变量(私有成员变量或者全局静态变量)
|
||||
2. 在方法体内对外部对象实例进行赋值
|
||||
3. 输出了日志
|
||||
4. 调用了其他外部方法
|
||||
5. ... ...
|
||||
|
||||
|
||||
> 不返回任何值也不产生任何"副作用"的方法没有存在意义。
|
||||
|
||||
这些"副作用"归纳来说可分为两类:**修改外部变量**和**调用外部方法**。
|
||||
|
||||
通过TestableMock的私有字段访问和Mock校验器可以很方便的实现对"副作用"的结果检查。
|
||||
|
||||
#### 修改外部变量的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")); // 读取私有变量
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 调用外部方法的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`调用,然后用`InvokeVerifier.verify()`方法校验传入的打印内容:
|
||||
|
||||
```java
|
||||
class DemoTest {
|
||||
private Demo demo = new Demo();
|
||||
|
||||
// 拦截`System.out.println`调用
|
||||
@TestableMock
|
||||
public void println(PrintStream ps, String 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`插件配置依然是更加实用的方案。
|
||||
@@ -1,69 +1,17 @@
|
||||
使用说明
|
||||
快速Mock被测类的任意方法调用
|
||||
---
|
||||
|
||||
## 引入Testable
|
||||
相比以往Mock工具以类为粒度的Mock方式,TestableMock允许用户直接定义需要Mock的单个方法,并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测方法中的指定方法调用。
|
||||
|
||||
首先在项目`pom.xml`文件中添加`testable-processor`依赖:
|
||||
具体的Mock方法定义约定如下:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-processor</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>
|
||||
```
|
||||
|
||||
> PS:其中`${testable.version}`需替换为具体版本号,当前最新版本为`0.2.2-SNAPSHOT`
|
||||
|
||||
## 使用Testable
|
||||
|
||||
`Testable`目前能为测试类提供两项增强能力:__直接访问被测类的私有成员__ 和 __极速Mock被测方法中的调用__
|
||||
|
||||
### 访问私有成员字段和方法
|
||||
|
||||
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
|
||||
|
||||
- 调用被测类的私有方法
|
||||
- 读取被测类的私有成员
|
||||
- 修改被测类的私有成员
|
||||
- 修改被测类的常量成员(使用final或static final修饰的成员)
|
||||
|
||||
访问和修改私有、常量成员时,IDE可能会提示语法有误,但编译器将能够正常运行测试。
|
||||
|
||||
若不希望看到IDE的语法错误提醒,或是在基于JVM的非Java语言项目里(譬如Kotlin语言),也可以借助`PrivateAccessor`工具类来实现私有成员的访问。
|
||||
|
||||
效果见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_private_method()`和`should_able_to_mock_private_field()`测试用例。
|
||||
|
||||
### Mock被测类的任意方法调用
|
||||
|
||||
**1. <u>覆写任意类的方法调用</u>**
|
||||
#### 1. 覆写任意类的方法调用
|
||||
|
||||
在测试类里定义一个有`@TestableMock`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
|
||||
|
||||
此时被测类中所有对该需覆写方法的调用,将在单元测试运行时,将自动被替换为对上述自定义Mock方法的调用。
|
||||
|
||||
**注意**:也可以将需覆写的方法名写到`@TestableMock`注解的`targetMethod`参数里,这样Mock方法自身就可以随意命名了(当遇到重名的待覆写方法时特别有用)。
|
||||
**注意**:当遇到待覆写方法有重名时,可以将需覆写的方法名写到`@TestableMock`注解的`targetMethod`参数里,这样Mock方法自身就可以随意命名了。
|
||||
|
||||
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在测试类定义如下方法:
|
||||
|
||||
@@ -78,9 +26,20 @@ private String substring(String self, int i, int j) {
|
||||
}
|
||||
```
|
||||
|
||||
下面这个例子展示了`targetMethod`参数的用法,其效果与上述示例相同:
|
||||
|
||||
```java
|
||||
// 使用`targetMethod`指定需Mock的方法名
|
||||
// 此方法本身现在可以随意命名,但方法参数依然需要遵循相同的匹配规则
|
||||
@TestableMock(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. <u>覆写被测类自身的成员方法</u>**
|
||||
#### 2. 覆写被测类自身的成员方法
|
||||
|
||||
有时候,在对某些方法进行测试时,希望将被测类自身的另外一些成员方法Mock掉。
|
||||
|
||||
@@ -89,17 +48,17 @@ private String substring(String self, int i, int j) {
|
||||
例如,被测类中有一个签名为`String innerFunc(String)`的私有方法,我们希望在测试的时候将它替换掉,则只需在测试类定义如下方法:
|
||||
|
||||
```java
|
||||
// 被测类型是`DemoMockService`
|
||||
// 因此在定义Mock方法时,在目标方法参数首位加一个类型为`DemoMockService`的参数(名字随意)
|
||||
// 被测类型是`DemoMock`
|
||||
// 因此在定义Mock方法时,在目标方法参数首位加一个类型为`DemoMock`的参数(名字随意)
|
||||
@TestableMock
|
||||
private String innerFunc(DemoMockService self, String text) {
|
||||
private String innerFunc(DemoMock self, String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
```
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_member_method()`测试用例。
|
||||
|
||||
**3. <u>覆写任意类的静态方法</u>**
|
||||
#### 3. 覆写任意类的静态方法
|
||||
|
||||
对于静态方法的Mock与普通方法相同。但需要注意的是,对于静态方法,传入Mock方法的第一个参数实际值始终是`null`。
|
||||
|
||||
@@ -117,7 +76,7 @@ private BlackBox secretBox(BlackBox ignore) {
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_static_method()`测试用例。
|
||||
|
||||
**4. <u>覆写任意类的new操作</u>**
|
||||
#### 4. 覆写任意类的new操作
|
||||
|
||||
在测试类里定义一个有`@TestableMock`注解的普通方法,将注解的`targetMethod`参数写为"<init>",然后使该方法与要被创建类型的构造函数参数、返回值类型完全一致,方法名称随意。
|
||||
|
||||
@@ -137,8 +96,14 @@ private BlackBox createBlackBox(String text) {
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_new_object()`测试用例。
|
||||
|
||||
**5. <u>识别当前测试用例和调用来源</u>**
|
||||
#### 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)
|
||||
14
docs/zh-cn/sidebar.md
Normal file
14
docs/zh-cn/sidebar.md
Normal file
@@ -0,0 +1,14 @@
|
||||
- 快速上手
|
||||
- [使用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)
|
||||
|
||||
- 其他文档
|
||||
- [Release Note](zh-cn/doc/release-note.md)
|
||||
2
pom.xml
2
pom.xml
@@ -4,7 +4,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>parent</artifactId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
|
||||
@@ -1,102 +1,196 @@
|
||||
<?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>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
<version>0.3.2</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>testable-agent</name>
|
||||
<description>Unit test enhancement toolkit</description>
|
||||
<url>https://github.com/alibaba/testable-mock</url>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://github.com/alibaba/testable-mock/blob/master/LICENSE</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>金戟</name>
|
||||
<email>jinji.lf@alibaba-inc.com</email>
|
||||
</developer>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection>scm:git:git@github.com:alibaba/testable-mock.git</connection>
|
||||
<url>git@github.com:alibaba/testable-mock.git</url>
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
<version>0.2.2-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>testable-agent</name>
|
||||
<properties>
|
||||
<java.version>1.6</java.version>
|
||||
<project.compiler.level>1.6</project.compiler.level>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<asm.lib.version>8.0.1</asm.lib.version>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.3.2</testable.version>
|
||||
<plugin.compiler.version>3.8.1</plugin.compiler.version>
|
||||
<plugin.surefire.version>3.0.0-M5</plugin.surefire.version>
|
||||
<plugin.jar.version>3.2.0</plugin.jar.version>
|
||||
<plugin.shade.version>3.2.4</plugin.shade.version>
|
||||
<plugin.source.version>3.2.0</plugin.source.version>
|
||||
<plugin.javadoc.version>3.2.0</plugin.javadoc.version>
|
||||
<plugin.gpg.version>1.6</plugin.gpg.version>
|
||||
<plugin.staging.version>1.6.8</plugin.staging.version>
|
||||
</properties>
|
||||
|
||||
<properties>
|
||||
<java.version>1.6</java.version>
|
||||
<project.compiler.level>1.6</project.compiler.level>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<asm.lib.version>8.0.1</asm.lib.version>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.2.2-SNAPSHOT</testable.version>
|
||||
<plugin.compiler.version>3.8.1</plugin.compiler.version>
|
||||
<plugin.surefire.version>3.0.0-M5</plugin.surefire.version>
|
||||
<plugin.jar.version>3.2.0</plugin.jar.version>
|
||||
<plugin.shade.version>3.2.4</plugin.shade.version>
|
||||
</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>${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>
|
||||
<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>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${plugin.surefire.version}</version>
|
||||
</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.agent.util.LogUtil;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
|
||||
@@ -10,8 +11,23 @@ import java.lang.instrument.Instrumentation;
|
||||
*/
|
||||
public class PreMain {
|
||||
|
||||
private static final String AND = "&";
|
||||
private static final String DEBUG = "debug";
|
||||
|
||||
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)) {
|
||||
if (a.equals(DEBUG)) {
|
||||
LogUtil.globalDebugEnable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@ public class ConstPool {
|
||||
|
||||
public static final String FIELD_TARGET_METHOD = "targetMethod";
|
||||
|
||||
public static final String MOCK_WITH = "com.alibaba.testable.core.annotation.MockWith";
|
||||
public static final String TESTABLE_MOCK = "com.alibaba.testable.core.annotation.TestableMock";
|
||||
}
|
||||
|
||||
@@ -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.agent.util.LogUtil;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
@@ -51,6 +52,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
|
||||
private void transformMethod(ClassNode cn, MethodNode mn, Set<MethodInfo> memberInjectMethods,
|
||||
Set<MethodInfo> newOperatorInjectMethods) {
|
||||
LogUtil.debug(" Handling method %s", mn.name);
|
||||
AbstractInsnNode[] instructions = mn.instructions.toArray();
|
||||
List<MethodInfo> memberInjectMethodList = new ArrayList<MethodInfo>(memberInjectMethods);
|
||||
int i = 0;
|
||||
@@ -120,12 +122,18 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
int stackLevel = ClassUtil.getParameterTypes(((MethodInsnNode)instructions[rangeEnd]).desc).size();
|
||||
for (int i = rangeEnd - 1; i >= 0; i--) {
|
||||
switch (instructions[i].getOpcode()) {
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
case Opcodes.INVOKESPECIAL:
|
||||
case Opcodes.INVOKESTATIC:
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
case Opcodes.INVOKEINTERFACE:
|
||||
stackLevel += stackEffectOfInvocation(instructions[i]) + 1;
|
||||
if (((MethodInsnNode)instructions[i]).name.equals(ConstPool.CONSTRUCTOR)) {
|
||||
// constructor must be INVOKESPECIAL and implicitly pop 1 more stack
|
||||
stackLevel++;
|
||||
}
|
||||
break;
|
||||
case Opcodes.INVOKESTATIC:
|
||||
case Opcodes.INVOKEDYNAMIC:
|
||||
stackLevel += ClassUtil.getParameterTypes(((MethodInsnNode)instructions[i]).desc).size();
|
||||
stackLevel += stackEffectOfInvocation(instructions[i]);
|
||||
break;
|
||||
case -1:
|
||||
// reach LineNumberNode or LabelNode
|
||||
@@ -140,8 +148,15 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
return -1;
|
||||
}
|
||||
|
||||
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.debug(" 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);
|
||||
@@ -155,6 +170,15 @@ 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);
|
||||
@@ -163,6 +187,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, String substitutionMethod,
|
||||
AbstractInsnNode[] instructions, String ownerClass,
|
||||
int opcode, int start, int end) {
|
||||
LogUtil.debug(" Line %d, mock method %s used", getLineNum(instructions, start), substitutionMethod);
|
||||
mn.maxStack++;
|
||||
MethodInsnNode method = (MethodInsnNode)instructions[end];
|
||||
String testClassName = ClassUtil.getTestClassName(cn.name);
|
||||
@@ -176,6 +201,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
mn.instructions.remove(instructions[end - 1]);
|
||||
}
|
||||
}
|
||||
// method with @TestableMock will be modified as public access, so INVOKEVIRTUAL is used
|
||||
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,
|
||||
substitutionMethod, addFirstParameter(method.desc, ClassUtil.fitCompanionClassName(ownerClass)), false));
|
||||
mn.instructions.remove(instructions[end]);
|
||||
|
||||
@@ -103,6 +103,7 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
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++) {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.alibaba.testable.agent.model;
|
||||
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
|
||||
/**
|
||||
* Record parameter fetch from @MockWith annotation
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
public class CachedMockParameter {
|
||||
|
||||
private final boolean classExist;
|
||||
private final AnnotationNode mockWith;
|
||||
|
||||
private CachedMockParameter(boolean classExist, AnnotationNode mockWith) {
|
||||
this.classExist = classExist;
|
||||
this.mockWith = mockWith;
|
||||
}
|
||||
|
||||
public static CachedMockParameter notExist() {
|
||||
return new CachedMockParameter(false, null);
|
||||
}
|
||||
|
||||
public static CachedMockParameter exist() {
|
||||
return new CachedMockParameter(true, null);
|
||||
}
|
||||
|
||||
public static CachedMockParameter exist(AnnotationNode mockWith) {
|
||||
return new CachedMockParameter(true, mockWith);
|
||||
}
|
||||
|
||||
public boolean isClassExist() {
|
||||
return classExist;
|
||||
}
|
||||
|
||||
public AnnotationNode getMockWith() {
|
||||
return mockWith;
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,14 @@ package com.alibaba.testable.agent.transformer;
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.handler.SourceClassHandler;
|
||||
import com.alibaba.testable.agent.handler.TestClassHandler;
|
||||
import com.alibaba.testable.agent.model.CachedMockParameter;
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.model.MethodInfo;
|
||||
import com.alibaba.testable.agent.tool.ComparableWeakRef;
|
||||
import com.alibaba.testable.agent.util.AnnotationUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.agent.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;
|
||||
@@ -17,9 +20,7 @@ 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;
|
||||
|
||||
@@ -28,26 +29,29 @@ import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassNa
|
||||
*/
|
||||
public class TestableClassTransformer implements ClassFileTransformer {
|
||||
|
||||
private final Set<ComparableWeakRef<String>> loadedClassNames = ComparableWeakRef.getWeekHashSet();
|
||||
private static final String FIELD_DIAGNOSE = "diagnose";
|
||||
private final Map<ComparableWeakRef<String>, CachedMockParameter> loadedClass =
|
||||
new WeakHashMap<ComparableWeakRef<String>, CachedMockParameter>();
|
||||
|
||||
@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(loader, className) || loadedClass.containsKey(new ComparableWeakRef<String>(className))) {
|
||||
// Ignore system class and reloaded class
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (shouldTransformAsSourceClass(className)) {
|
||||
// it's a source class with testable enabled
|
||||
loadedClassNames.add(new ComparableWeakRef<String>(className));
|
||||
LogUtil.debug("Handling source class %s", className);
|
||||
List<MethodInfo> injectMethods = getTestableMockMethods(ClassUtil.getTestClassName(className));
|
||||
return new SourceClassHandler(injectMethods).getBytes(classFileBuffer);
|
||||
} else if (shouldTransformAsTestClass(className)) {
|
||||
// it's a test class with testable enabled
|
||||
loadedClassNames.add(new ComparableWeakRef<String>(className));
|
||||
LogUtil.debug("Handling test class %s", className);
|
||||
return new TestClassHandler().getBytes(classFileBuffer);
|
||||
}
|
||||
resetMockContext();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
@@ -55,12 +59,11 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -76,6 +79,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
for (MethodNode mn : cn.methods) {
|
||||
checkMethodAnnotation(cn, methodInfos, mn);
|
||||
}
|
||||
LogUtil.debug(" Found %d mock methods", methodInfos.size());
|
||||
return methodInfos;
|
||||
} catch (Exception e) {
|
||||
return new ArrayList<MethodInfo>();
|
||||
@@ -103,6 +107,60 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
CachedMockParameter cache = loadedClass.get(new ComparableWeakRef<String>(className));
|
||||
if (cache != null) {
|
||||
setupMockContext(cache.getMockWith());
|
||||
return cache.isClassExist();
|
||||
}
|
||||
try {
|
||||
ClassNode cn = new ClassNode();
|
||||
new ClassReader(className).accept(cn, 0);
|
||||
if (cn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.MOCK_WITH)) {
|
||||
setupMockContext(an);
|
||||
loadedClass.put(new ComparableWeakRef<String>(className), CachedMockParameter.exist(an));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (mn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_MOCK)) {
|
||||
loadedClass.put(new ComparableWeakRef<String>(className), CachedMockParameter.exist());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Usually class not found, return without record
|
||||
return false;
|
||||
}
|
||||
loadedClass.put(new ComparableWeakRef<String>(className), CachedMockParameter.notExist());
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setupMockContext(AnnotationNode an) {
|
||||
MockDiagnose mockDebug = AnnotationUtil.getAnnotationParameter(an, FIELD_DIAGNOSE, null, MockDiagnose.class);
|
||||
if (MockDiagnose.ENABLE.equals(mockDebug)) {
|
||||
LogUtil.enableDebugLog();
|
||||
} else if (MockDiagnose.DISABLE.equals(mockDebug)) {
|
||||
LogUtil.disableDebugLog();
|
||||
}
|
||||
}
|
||||
|
||||
private void resetMockContext() {
|
||||
LogUtil.resetDebugLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Split desc to "first parameter" and "desc of rest parameters"
|
||||
* @param desc method desc
|
||||
|
||||
@@ -9,11 +9,26 @@ 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.values != null) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -26,6 +26,7 @@ public class ClassUtil {
|
||||
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 = '[';
|
||||
@@ -39,13 +40,10 @@ public class ClassUtil {
|
||||
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 final static String JOINER = "::";
|
||||
|
||||
private static final Map<Byte, String> TYPE_MAPPING = new HashMap<Byte, String>();
|
||||
private static final Map<ComparableWeakRef<String>, Boolean> loadedClass =
|
||||
new WeakHashMap<ComparableWeakRef<String>, Boolean>();
|
||||
|
||||
static {
|
||||
TYPE_MAPPING.put(TYPE_BYTE, CLASS_BYTE);
|
||||
@@ -56,42 +54,13 @@ public class ClassUtil {
|
||||
TYPE_MAPPING.put(TYPE_LONG, CLASS_LONG);
|
||||
TYPE_MAPPING.put(TYPE_SHORT, CLASS_SHORT);
|
||||
TYPE_MAPPING.put(TYPE_BOOL, CLASS_BOOLEAN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether any method in specified class has specified annotation
|
||||
* @param className class that need to explore
|
||||
* @param annotationName annotation to look for
|
||||
*/
|
||||
public static boolean anyMethodHasAnnotation(String className, String annotationName) {
|
||||
String cacheKey = className + JOINER + annotationName;
|
||||
Boolean found = loadedClass.get(new ComparableWeakRef<String>(cacheKey));
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
try {
|
||||
ClassNode cn = new ClassNode();
|
||||
new ClassReader(className).accept(cn, 0);
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (mn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (toDotSeparateFullClassName(an.desc).equals(annotationName)) {
|
||||
loadedClass.put(new ComparableWeakRef<String>(cacheKey), true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
loadedClass.put(new ComparableWeakRef<String>(cacheKey), false);
|
||||
return false;
|
||||
TYPE_MAPPING.put(TYPE_VOID, EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* fit kotlin companion class name to original name
|
||||
* @param name a class name (which could be a companion class)
|
||||
* @return is companion class or not
|
||||
*/
|
||||
public static boolean isCompanionClassName(String name) {
|
||||
return name.endsWith("$Companion");
|
||||
@@ -100,6 +69,7 @@ public class ClassUtil {
|
||||
/**
|
||||
* 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$", "");
|
||||
@@ -108,6 +78,7 @@ public class ClassUtil {
|
||||
/**
|
||||
* 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;
|
||||
@@ -116,6 +87,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());
|
||||
@@ -123,6 +95,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>();
|
||||
@@ -148,6 +122,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);
|
||||
@@ -159,13 +135,14 @@ public class ClassUtil {
|
||||
} 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);
|
||||
@@ -173,12 +150,10 @@ public class ClassUtil {
|
||||
new MethodInsnNode(INVOKESTATIC, objectType, METHOD_VALUE_OF, toDescriptor(type, objectType), false);
|
||||
}
|
||||
|
||||
private static String toDescriptor(Byte type, String objectType) {
|
||||
return "(" + (char)type.byteValue() + ")L" + objectType + ";";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@@ -186,6 +161,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);
|
||||
@@ -193,6 +170,8 @@ 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 (char)TYPE_CLASS + toSlashSeparatedName(className) + (char)CLASS_END;
|
||||
@@ -200,6 +179,8 @@ public class ClassUtil {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@@ -207,11 +188,17 @@ 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);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class LogUtil {
|
||||
|
||||
private static final int LEVEL_ERROR = 0;
|
||||
private static final int LEVEL_WARN = 1;
|
||||
private static final int LEVEL_DIAGNOSE = 2;
|
||||
private static final ThreadLocal<Integer> LEVEL = new ThreadLocal<Integer>();
|
||||
|
||||
public static boolean globalDebugEnable = false;
|
||||
|
||||
public static void debug(String msg, Object... args) {
|
||||
if (LEVEL.get() >= LEVEL_DIAGNOSE) {
|
||||
System.out.println(String.format("[DIAGNOSE] " + msg, args));
|
||||
}
|
||||
}
|
||||
|
||||
public static void enableDebugLog() {
|
||||
LEVEL.remove();
|
||||
LEVEL.set(LEVEL_DIAGNOSE);
|
||||
}
|
||||
|
||||
public static void disableDebugLog() {
|
||||
LEVEL.remove();
|
||||
LEVEL.set(LEVEL_ERROR);
|
||||
}
|
||||
|
||||
public static void resetDebugLog() {
|
||||
LEVEL.set(globalDebugEnable ? LEVEL_DIAGNOSE : LEVEL_WARN);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
assertFalse(ClassUtil.anyMethodHasAnnotation("class.not.exist", ""));
|
||||
assertFalse(ClassUtil.anyMethodHasAnnotation("com.alibaba.testable.agent.util.ClassUtilTest", "annotation.not.exist"));
|
||||
assertTrue(ClassUtil.anyMethodHasAnnotation("com.alibaba.testable.agent.util.ClassUtilTest", "org.junit.jupiter.api.Test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_parameter_count() {
|
||||
assertEquals(0, ClassUtil.getParameterTypes("()V").size());
|
||||
|
||||
@@ -3,20 +3,43 @@
|
||||
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>
|
||||
<artifactId>testable-core</artifactId>
|
||||
<version>0.2.2-SNAPSHOT</version>
|
||||
<version>0.3.2</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>testable-core</name>
|
||||
<description>Unit test enhancement toolkit</description>
|
||||
<url>https://github.com/alibaba/testable-mock</url>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://github.com/alibaba/testable-mock/blob/master/LICENSE</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>金戟</name>
|
||||
<email>jinji.lf@alibaba-inc.com</email>
|
||||
</developer>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection>scm:git:git@github.com:alibaba/testable-mock.git</connection>
|
||||
<url>git@github.com:alibaba/testable-mock.git</url>
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
|
||||
<properties>
|
||||
<java.version>1.6</java.version>
|
||||
<project.compiler.level>1.6</project.compiler.level>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<plugin.compiler.version>3.8.1</plugin.compiler.version>
|
||||
<plugin.surefire.version>3.0.0-M5</plugin.surefire.version>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<plugin.source.version>3.2.0</plugin.source.version>
|
||||
<plugin.javadoc.version>3.2.0</plugin.javadoc.version>
|
||||
<plugin.gpg.version>1.6</plugin.gpg.version>
|
||||
<plugin.staging.version>1.6.8</plugin.staging.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -48,4 +71,74 @@
|
||||
</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,12 +42,33 @@ 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);
|
||||
@@ -54,8 +77,17 @@ public class PrivateAccessor {
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
System.err.println("Failed to invoke private static method \"" + method + "\": " + e.toString());
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -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.WARN_ONLY;
|
||||
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.alibaba.testable.core.annotation;
|
||||
|
||||
import com.alibaba.testable.core.model.MockType;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
@@ -14,13 +12,9 @@ import java.lang.annotation.*;
|
||||
@Documented
|
||||
public @interface TestableMock {
|
||||
|
||||
/**
|
||||
* type of mock method
|
||||
*/
|
||||
MockType value() default MockType.MEMBER_METHOD;
|
||||
|
||||
/**
|
||||
* mock specified method instead of method with same name
|
||||
* @return target method name
|
||||
*/
|
||||
String targetMethod() default "";
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.alibaba.testable.core.constant;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public final class ConstPool {
|
||||
|
||||
public static final String TEST_POSTFIX = "Test";
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.alibaba.testable.core.tool;
|
||||
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;
|
||||
@@ -14,55 +16,27 @@ public class InvokeVerifier {
|
||||
private final List<Object[]> records;
|
||||
private Verification lastVerification = null;
|
||||
|
||||
public InvokeVerifier(List<Object[]> records) {
|
||||
private InvokeVerifier(List<Object[]> records) {
|
||||
this.records = records;
|
||||
}
|
||||
|
||||
public InvokeVerifier with(Object arg1) {
|
||||
return with(new Object[]{arg1});
|
||||
}
|
||||
|
||||
public InvokeVerifier with(Object arg1, Object arg2) {
|
||||
return with(new Object[]{arg1, arg2});
|
||||
}
|
||||
|
||||
public InvokeVerifier with(Object arg1, Object arg2, Object arg3) {
|
||||
return with(new Object[]{arg1, arg2, arg3});
|
||||
}
|
||||
|
||||
public InvokeVerifier with(Object arg1, Object arg2, Object arg3, Object arg4) {
|
||||
return with(new Object[]{arg1, arg2, arg3, arg4});
|
||||
}
|
||||
|
||||
public InvokeVerifier with(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) {
|
||||
return with(new Object[]{arg1, arg2, arg3, arg4, arg5});
|
||||
}
|
||||
|
||||
public InvokeVerifier withInOrder(Object arg1) {
|
||||
return withInOrder(new Object[]{arg1});
|
||||
}
|
||||
|
||||
public InvokeVerifier withInOrder(Object arg1, Object arg2) {
|
||||
return withInOrder(new Object[]{arg1, arg2});
|
||||
}
|
||||
|
||||
public InvokeVerifier withInOrder(Object arg1, Object arg2, Object arg3) {
|
||||
return withInOrder(new Object[]{arg1, arg2, arg3});
|
||||
}
|
||||
|
||||
public InvokeVerifier withInOrder(Object arg1, Object arg2, Object arg3, Object arg4) {
|
||||
return withInOrder(new Object[]{arg1, arg2, arg3, arg4});
|
||||
}
|
||||
|
||||
public InvokeVerifier withInOrder(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) {
|
||||
return withInOrder(new Object[]{arg1, arg2, arg3, arg4, arg5});
|
||||
/**
|
||||
* 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);
|
||||
return new InvokeVerifier(InvokeRecordUtil.getInvokeRecord(mockMethodName, testCaseName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect mock method invoked with specified parameters
|
||||
* @param args parameters to compare
|
||||
* @return the verifier object
|
||||
*/
|
||||
public InvokeVerifier with(Object[] args) {
|
||||
public InvokeVerifier with(Object... args) {
|
||||
boolean found = false;
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
try {
|
||||
@@ -83,8 +57,9 @@ public class InvokeVerifier {
|
||||
/**
|
||||
* Expect next mock method call was invoked with specified parameters
|
||||
* @param args parameters to compare
|
||||
* @return the verifier object
|
||||
*/
|
||||
public InvokeVerifier withInOrder(Object[] args) {
|
||||
public InvokeVerifier withInOrder(Object... args) {
|
||||
withInternal(args, 0);
|
||||
lastVerification = new Verification(args, true);
|
||||
return this;
|
||||
@@ -93,16 +68,20 @@ public class InvokeVerifier {
|
||||
/**
|
||||
* Expect mock method had never invoked with specified parameters
|
||||
* @param args parameters to compare
|
||||
* @return the verifier object
|
||||
*/
|
||||
public InvokeVerifier without(Object[] args) {
|
||||
public InvokeVerifier without(Object... args) {
|
||||
for (Object[] r : records) {
|
||||
if (r.length == args.length) {
|
||||
for (int i = 0; i < r.length; i++) {
|
||||
if (!r[i].equals(args[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));
|
||||
}
|
||||
}
|
||||
throw new VerifyFailedError("was invoked with " + desc(args));
|
||||
}
|
||||
}
|
||||
return this;
|
||||
@@ -111,10 +90,11 @@ public class InvokeVerifier {
|
||||
/**
|
||||
* 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: " + records.size(), "times: " + expectedCount);
|
||||
throw new VerifyFailedError("times: " + expectedCount, "times: " + records.size());
|
||||
}
|
||||
lastVerification = null;
|
||||
return this;
|
||||
@@ -123,12 +103,17 @@ public class InvokeVerifier {
|
||||
/**
|
||||
* 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.");
|
||||
} else if (lastVerification == null) {
|
||||
throw new InvalidParameterException("should only use times() after with() or withInOrder() method.");
|
||||
}
|
||||
for (int i = 0; i < count - 1; i++) {
|
||||
if (lastVerification.inOrder) {
|
||||
@@ -150,17 +135,23 @@ public class InvokeVerifier {
|
||||
throw new VerifyFailedError(desc(args), desc(record));
|
||||
}
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (!args[i].getClass().equals(record[i].getClass())) {
|
||||
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 (!args[i].equals(record[i])) {
|
||||
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++) {
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.alibaba.testable.core.model;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
|
||||
public enum MockDiagnose {
|
||||
|
||||
/**
|
||||
* Be quiet
|
||||
*/
|
||||
DISABLE,
|
||||
|
||||
/**
|
||||
* Only show warning message
|
||||
*/
|
||||
WARN_ONLY,
|
||||
|
||||
/**
|
||||
* Print detail diagnose logs
|
||||
*/
|
||||
ENABLE
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.alibaba.testable.core.model;
|
||||
|
||||
/**
|
||||
* Type of mock method
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
public enum MockType {
|
||||
|
||||
MEMBER_METHOD,
|
||||
STATIC_METHOD,
|
||||
CONSTRUCTOR
|
||||
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
package com.alibaba.testable.core.tool;
|
||||
|
||||
import com.alibaba.testable.core.util.InvokeRecordUtil;
|
||||
import com.alibaba.testable.core.util.TestableUtil;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
@@ -23,14 +20,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 InvokeVerifier verify(String mockMethodName) {
|
||||
String testClass = Thread.currentThread().getStackTrace()[InvokeRecordUtil.INDEX_OF_TEST_CLASS].getClassName();
|
||||
String testCaseName = TestableUtil.currentTestCaseName(testClass);
|
||||
return new InvokeVerifier(InvokeRecordUtil.getInvokeRecord(mockMethodName, testCaseName));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,18 +11,20 @@ import java.util.Map;
|
||||
public class InvokeRecordUtil {
|
||||
|
||||
/**
|
||||
* Mock method name -> List of invoke parameters
|
||||
* 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
|
||||
* [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];
|
||||
@@ -41,6 +43,9 @@ public class InvokeRecordUtil {
|
||||
|
||||
/**
|
||||
* Get mock method invoke count
|
||||
* @param mockMethodName mock method name
|
||||
* @param testCaseName test case name
|
||||
* @return parameters used when specified method invoked in specified test case
|
||||
*/
|
||||
public static List<Object[]> getInvokeRecord(String mockMethodName, String testCaseName) {
|
||||
String key = testCaseName + JOINER + mockMethodName;
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
|
||||
import com.alibaba.testable.core.constant.ConstPool;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class TestableUtil {
|
||||
|
||||
/**
|
||||
* [0]Thread.getStackTrace() → [1]currentSourceMethodName() → [2]MockMethod -> [3]SourceMethod
|
||||
*/
|
||||
private static final int INDEX_OF_SOURCE_METHOD = 3;
|
||||
/**
|
||||
* [0]Thread.getStackTrace() → [1]previousStackLocation() → [2]Invoker -> [3]Caller of invoker
|
||||
*/
|
||||
private static final int INDEX_OF_CALLER_METHOD = 3;
|
||||
/**
|
||||
* 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;
|
||||
return Thread.currentThread().getStackTrace()[INDEX_OF_SOURCE_METHOD].getMethodName();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,7 +36,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);
|
||||
}
|
||||
|
||||
@@ -41,38 +46,73 @@ 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;
|
||||
}
|
||||
|
||||
private static String findLastMethodFromSourceClass(String sourceClassName, StackTraceElement[] stack) {
|
||||
for (StackTraceElement element : stack) {
|
||||
if (element.getClassName().equals(sourceClassName)) {
|
||||
return element.getMethodName();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
/**
|
||||
* 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 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,6 +40,9 @@ public class TypeUtil {
|
||||
|
||||
/**
|
||||
* type equals
|
||||
* @param classesLeft class to be compared
|
||||
* @param classesRight class to compare
|
||||
* @return whether all class equals
|
||||
*/
|
||||
private static boolean typeEquals(Class<?>[] classesLeft, Class<?>[] classesRight) {
|
||||
if (classesLeft.length != classesRight.length) {
|
||||
@@ -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)) ||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,160 @@
|
||||
<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/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
<packaging>maven-plugin</packaging>
|
||||
<version>0.2.2-SNAPSHOT</version>
|
||||
<name>testable-maven-plugin</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
<?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/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
<version>0.3.2</version>
|
||||
<packaging>maven-plugin</packaging>
|
||||
<name>testable-maven-plugin</name>
|
||||
<description>Unit test enhancement toolkit</description>
|
||||
<url>https://github.com/alibaba/testable-mock</url>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://github.com/alibaba/testable-mock/blob/master/LICENSE</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>金戟</name>
|
||||
<email>jinji.lf@alibaba-inc.com</email>
|
||||
</developer>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection>scm:git:git@github.com:alibaba/testable-mock.git</connection>
|
||||
<url>git@github.com:alibaba/testable-mock.git</url>
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
|
||||
<properties>
|
||||
<testable.version>0.2.2-SNAPSHOT</testable.version>
|
||||
<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>
|
||||
<maven.plugin.version>3.6.0</maven.plugin.version>
|
||||
</properties>
|
||||
<properties>
|
||||
<testable.version>0.3.2</testable.version>
|
||||
<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>
|
||||
<plugin.maven.version>3.6.0</plugin.maven.version>
|
||||
<plugin.source.version>3.2.0</plugin.source.version>
|
||||
<plugin.javadoc.version>3.2.0</plugin.javadoc.version>
|
||||
<plugin.gpg.version>1.6</plugin.gpg.version>
|
||||
<plugin.staging.version>1.6.8</plugin.staging.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-plugin-api</artifactId>
|
||||
<version>3.6.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-core</artifactId>
|
||||
<version>3.6.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugin-tools</groupId>
|
||||
<artifactId>maven-plugin-annotations</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-plugin-api</artifactId>
|
||||
<version>3.6.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-core</artifactId>
|
||||
<version>3.6.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugin-tools</groupId>
|
||||
<artifactId>maven-plugin-annotations</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<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-plugin-plugin</artifactId>
|
||||
<version>${maven.plugin.version}</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<build>
|
||||
<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-plugin-plugin</artifactId>
|
||||
<version>${plugin.maven.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>
|
||||
|
||||
@@ -3,21 +3,44 @@
|
||||
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>
|
||||
<artifactId>testable-processor</artifactId>
|
||||
<version>0.2.2-SNAPSHOT</version>
|
||||
<version>0.3.2</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>testable-processor</name>
|
||||
<description>Unit test enhancement toolkit</description>
|
||||
<url>https://github.com/alibaba/testable-mock</url>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://github.com/alibaba/testable-mock/blob/master/LICENSE</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>金戟</name>
|
||||
<email>jinji.lf@alibaba-inc.com</email>
|
||||
</developer>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection>scm:git:git@github.com:alibaba/testable-mock.git</connection>
|
||||
<url>git@github.com:alibaba/testable-mock.git</url>
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
|
||||
<properties>
|
||||
<java.version>1.6</java.version>
|
||||
<project.compiler.level>1.6</project.compiler.level>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.3.2</testable.version>
|
||||
<plugin.compiler.version>3.8.1</plugin.compiler.version>
|
||||
<plugin.surefire.version>3.0.0-M5</plugin.surefire.version>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.2.2-SNAPSHOT</testable.version>
|
||||
<plugin.source.version>3.2.0</plugin.source.version>
|
||||
<plugin.javadoc.version>3.2.0</plugin.javadoc.version>
|
||||
<plugin.gpg.version>1.6</plugin.gpg.version>
|
||||
<plugin.staging.version>1.6.8</plugin.staging.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -34,26 +57,6 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>tools-jar</id>
|
||||
<activation>
|
||||
<file>
|
||||
<exists>${java.home}/../lib/tools.jar</exists>
|
||||
</file>
|
||||
</activation>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>sun.jdk</groupId>
|
||||
<artifactId>tools</artifactId>
|
||||
<version>${java.version}</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${java.home}/../lib/tools.jar</systemPath>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
@@ -75,4 +78,92 @@
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>tools-jar</id>
|
||||
<activation>
|
||||
<file>
|
||||
<exists>${java.home}/../lib/tools.jar</exists>
|
||||
</file>
|
||||
</activation>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>sun.jdk</groupId>
|
||||
<artifactId>tools</artifactId>
|
||||
<version>${java.version}</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${java.home}/../lib/tools.jar</systemPath>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
<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>
|
||||
|
||||
@@ -15,6 +15,13 @@ public class PrivateAccessStatementGenerator extends BaseGenerator {
|
||||
super(cx);
|
||||
}
|
||||
|
||||
public JCExpression fetchGetterStatement(JCFieldAccess access) {
|
||||
JCFieldAccess getter = cx.treeMaker.Select(nameToExpression(ConstPool.TESTABLE_PRIVATE_ACCESSOR),
|
||||
cx.names.fromString("get"));
|
||||
return cx.treeMaker.Apply(List.<JCExpression>nil(), getter, List.of(access.selected,
|
||||
cx.treeMaker.Literal(access.name.toString())));
|
||||
}
|
||||
|
||||
public JCExpression fetchSetterStatement(JCAssign assign) {
|
||||
JCFieldAccess setter = cx.treeMaker.Select(nameToExpression(ConstPool.TESTABLE_PRIVATE_ACCESSOR),
|
||||
cx.names.fromString("set"));
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.sun.tools.javac.util.Name;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
/**
|
||||
* Travel AST
|
||||
@@ -18,17 +20,42 @@ import java.lang.reflect.Modifier;
|
||||
*/
|
||||
public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
|
||||
/**
|
||||
* Name of source class
|
||||
*/
|
||||
private final String sourceClassName;
|
||||
/**
|
||||
* Fields of source class instance in the test class
|
||||
*/
|
||||
private final ListBuffer<Name> sourceClassIns = new ListBuffer<Name>();
|
||||
/**
|
||||
* Record private and final fields
|
||||
*/
|
||||
private final ListBuffer<String> privateOrFinalFields = new ListBuffer<String>();
|
||||
/**
|
||||
* Record private methods
|
||||
*/
|
||||
private final ListBuffer<String> privateMethods = new ListBuffer<String>();
|
||||
|
||||
private final PrivateAccessStatementGenerator privateAccessStatementGenerator;
|
||||
|
||||
public EnablePrivateAccessTranslator(String pkgName, String testClassName, TestableContext cx) {
|
||||
this.sourceClassName = testClassName.substring(0, testClassName.length() - ConstPool.TEST_POSTFIX.length());
|
||||
this.privateAccessStatementGenerator = new PrivateAccessStatementGenerator(cx);
|
||||
try {
|
||||
Class<?> cls = Class.forName(pkgName + "." + sourceClassName);
|
||||
Class<?> cls = null;
|
||||
String sourceClassFullName = pkgName + "." + sourceClassName;
|
||||
try {
|
||||
cls = Class.forName(sourceClassFullName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
// fit for gradle build
|
||||
String path = "file:" + System.getProperty("user.dir") + "/build/classes/java/main/";
|
||||
cls = new URLClassLoader(new URL[]{new URL(path)}).loadClass(sourceClassFullName);
|
||||
}
|
||||
if (cls == null) {
|
||||
System.err.println("Failed to load source class: " + sourceClassFullName);
|
||||
return;
|
||||
}
|
||||
Field[] fields = cls.getDeclaredFields();
|
||||
for (Field f : fields) {
|
||||
if (Modifier.isFinal(f.getModifiers()) || Modifier.isPrivate(f.getModifiers())) {
|
||||
@@ -56,25 +83,29 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
}
|
||||
|
||||
/**
|
||||
* d.privateField = val -> PrivateAccessor.set(d, "privateField", val)
|
||||
* d.privateMethod(args) -> PrivateAccessor.invoke(d, "privateMethod", args)
|
||||
* d.privateField = val → PrivateAccessor.set(d, "privateField", val)
|
||||
* d.privateMethod(args) → PrivateAccessor.invoke(d, "privateMethod", args)
|
||||
*/
|
||||
@Override
|
||||
public void visitExec(JCExpressionStatement jcExpressionStatement) {
|
||||
// visitExec could be an assign statement to a private field
|
||||
if (jcExpressionStatement.expr.getClass().equals(JCAssign.class) &&
|
||||
isPrivateField((JCAssign)jcExpressionStatement.expr)) {
|
||||
jcExpressionStatement.expr = privateAccessStatementGenerator.fetchSetterStatement(
|
||||
(JCAssign)jcExpressionStatement.expr);
|
||||
}
|
||||
// visitExec could be an invoke
|
||||
jcExpressionStatement.expr = checkAndExchange(jcExpressionStatement.expr);
|
||||
super.visitExec(jcExpressionStatement);
|
||||
}
|
||||
|
||||
/**
|
||||
* For private invoke invocation break point
|
||||
* call(d.privateMethod(args)) → call(PrivateAccessor.invoke(d, "privateMethod", args))
|
||||
*/
|
||||
@Override
|
||||
public void visitApply(JCMethodInvocation tree) {
|
||||
// parameter of invocation could be an invoke or field access
|
||||
tree.args = checkAndExchange(tree.args);
|
||||
super.visitApply(tree);
|
||||
}
|
||||
@@ -97,6 +128,12 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
|
||||
@Override
|
||||
protected JCExpression checkAndExchange(JCExpression expr) {
|
||||
// check is accessing a private field of source class
|
||||
if (expr.getClass().equals(JCFieldAccess.class) &&
|
||||
isPrivateField((JCFieldAccess)expr)) {
|
||||
expr = privateAccessStatementGenerator.fetchGetterStatement((JCFieldAccess)expr);
|
||||
}
|
||||
// check is invoking a private method of source class
|
||||
if (expr.getClass().equals(JCMethodInvocation.class) &&
|
||||
isPrivateMethod((JCMethodInvocation)expr)) {
|
||||
expr = privateAccessStatementGenerator.fetchInvokeStatement((JCMethodInvocation)expr);
|
||||
@@ -104,11 +141,17 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
return expr;
|
||||
}
|
||||
|
||||
private boolean isPrivateField(JCAssign expr) {
|
||||
return expr.lhs.getClass().equals(JCFieldAccess.class) &&
|
||||
((JCFieldAccess)(expr).lhs).selected.getClass().equals(JCIdent.class) &&
|
||||
sourceClassIns.contains(((JCIdent)((JCFieldAccess)(expr).lhs).selected).name) &&
|
||||
privateOrFinalFields.contains(((JCFieldAccess)(expr).lhs).name.toString());
|
||||
private boolean isPrivateField(JCFieldAccess access) {
|
||||
return access.selected.getClass().equals(JCIdent.class) &&
|
||||
sourceClassIns.contains(((JCIdent)access.selected).name) &&
|
||||
privateOrFinalFields.contains(access.name.toString());
|
||||
}
|
||||
|
||||
private boolean isPrivateField(JCAssign assign) {
|
||||
return assign.lhs.getClass().equals(JCFieldAccess.class) &&
|
||||
((JCFieldAccess)(assign).lhs).selected.getClass().equals(JCIdent.class) &&
|
||||
sourceClassIns.contains(((JCIdent)((JCFieldAccess)(assign).lhs).selected).name) &&
|
||||
privateOrFinalFields.contains(((JCFieldAccess)(assign).lhs).name.toString());
|
||||
}
|
||||
|
||||
private boolean isPrivateMethod(JCMethodInvocation expr) {
|
||||
|
||||
@@ -11,6 +11,7 @@ public class StringUtil {
|
||||
* Join strings
|
||||
* @param list strings to join
|
||||
* @param conjunction connection character
|
||||
* @return joined string
|
||||
*/
|
||||
static public String join(List<String> list, String conjunction)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user