Compare commits

..

49 Commits

Author SHA1 Message Date
金戟
dd7499f3cf bump version to 0.3.2 2020-12-01 23:54:04 +08:00
金戟
1d98e3c618 support invoke private companion object method in kotlin 2020-12-01 23:49:48 +08:00
金戟
001001bfd3 fully support gradle build and test 2020-12-01 20:20:44 +08:00
金戟
472f90e7e4 add static access methods to private accessor 2020-11-30 22:57:37 +08:00
金戟
1af6b8c8b6 fit private access for gradle build 2020-11-30 21:32:13 +08:00
金戟
1bf6ab3b21 fit test case name fetcher for gradle build 2020-11-29 10:39:55 +08:00
金戟
9ab737ce57 add a gradle demo 2020-11-28 22:51:09 +08:00
金戟
35dbc1f497 also look current thread for test case name 2020-11-28 22:42:25 +08:00
金戟
92f9c27561 dump to snapshot version 2020-11-28 20:36:20 +08:00
金戟
7b273affe3 testable-agent dependence is required in order to have agent jar downloaded 2020-11-28 19:52:20 +08:00
金戟
b4d6a8357e remove dependence on internal class 2020-11-28 19:50:00 +08:00
金戟
ff29172a93 moe descriptions and more faq 2020-11-28 17:03:39 +08:00
金戟
d94daf6910 highlight java code 2020-11-26 07:14:08 +08:00
金戟
fb62ab63cc add usage of verify void method 2020-11-26 07:04:41 +08:00
金戟
d75fdcd4e9 update docs and release 0.3.1 2020-11-24 23:18:12 +08:00
金戟
df9da81f75 use docsify generate doc page 2020-11-22 22:12:51 +08:00
金戟
a8ed46e60d add introduction doc 2020-11-22 10:25:44 +08:00
金戟
47115b22af remove spring framework from demos 2020-11-22 00:46:50 +08:00
金戟
b8b4844c38 add testcase for inherition 2020-11-21 22:53:32 +08:00
金戟
2ae34e0b41 update release note 2020-11-18 11:03:53 +08:00
金戟
401b5f83d7 support enable diagnose log by test class 2020-11-18 10:31:33 +08:00
金戟
5770a702cc add test for times method 2020-11-18 10:29:44 +08:00
金戟
15c7e9c48d add matcher doc, add kotlin matcher test case 2020-11-17 19:47:21 +08:00
金戟
0657c19da3 set to next snapshot version 2020-11-17 19:45:31 +08:00
金戟
b275f32e44 bump version to 0.3.0, fix javadoc 2020-11-17 15:33:39 +08:00
金戟
85276e1c55 maven plugin is usually required 2020-11-17 14:08:14 +08:00
金戟
c68289920f move verify method to verifier class 2020-11-17 14:06:59 +08:00
金戟
7e77f77fa2 print count of mock method found 2020-11-17 13:09:34 +08:00
金戟
40a1a999a7 use variable length args 2020-11-17 13:07:53 +08:00
金戟
7d2dd83410 consider return type when calculating stack change 2020-11-16 23:13:45 +08:00
金戟
43cd4aeeec add test case for matchers 2020-11-16 21:53:32 +08:00
金戟
525e252f96 skip type comparation for macher 2020-11-16 19:19:02 +08:00
金戟
f63b694b0c move matchers to standalone package 2020-11-16 19:11:31 +08:00
金戟
809b1aa57f let's handle invoke and array correctly 2020-11-16 19:01:29 +08:00
金戟
f7dba2d33d add instrument process debug log 2020-11-16 14:47:49 +08:00
金戟
36e260688e need to increase stack by 1 2020-11-16 14:25:20 +08:00
金戟
648e4aa9cc print code location in warning message 2020-11-16 14:16:58 +08:00
Fan Lin
80499f0da2 update release note 2020-11-15 14:37:44 +08:00
Fan Lin
d7ffbcbc32 remove unused mock type 2020-11-15 14:36:57 +08:00
Fan Lin
e586d5db79 handle matcher in verification 2020-11-15 14:34:29 +08:00
金戟
bf9ceeea0f use release profile 2020-11-14 07:06:36 +08:00
金戟
73ef418f39 complete pom file information 2020-11-14 06:54:56 +08:00
金戟
c6ea4c168d add verify matcher 2020-11-13 23:41:34 +08:00
金戟
3f59e9e098 opensourced with MIT license 2020-11-10 14:12:25 +08:00
金戟
754d9ce08d update upcoming version feature 2020-11-10 14:10:14 +08:00
金戟
72269fbcd8 rename to testable mock 2020-11-10 13:06:48 +08:00
金戟
6e47057d7f add test case for final field modification 2020-11-10 11:20:21 +08:00
金戟
854c062e93 implement getter of private field 2020-11-10 11:01:22 +08:00
金戟
baf3c8d657 add verify method to doc 2020-11-10 06:54:01 +08:00
92 changed files with 2976 additions and 950 deletions

15
.gitignore vendored
View File

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

21
LICENSE Normal file
View 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.

View File

@@ -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`命令安装文档生成工具。

View 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()
}

View File

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

View File

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

View File

@@ -0,0 +1,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();
}
}

View File

@@ -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"));
}
}

View File

@@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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);
}
}

View File

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

View File

@@ -0,0 +1,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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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"));
}
}

View 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()
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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()
}
}

View File

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

View File

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

View File

@@ -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>()
}
}
}

View File

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

View File

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

View File

@@ -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"))
}
}

View File

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

View File

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

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>TestableMock</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
</head>
<body>
<div id="app">Loading...</div>
<script>
window.$docsify = {
name: '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>

View File

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

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

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

View 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`框架。

View File

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

View File

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

View File

@@ -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
View 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)文件。

View 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");
}
}
```

View 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替换的调用和相应代码行号。

View 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`替换成实际使用的版本号
![idea-vm-option](https://testable-code.oss-cn-beijing.aliyuncs.com/idea-vm-option.png)
这样实际上还是该了`maven-surefire-plugin`插件的配置,因此目前除了需要考虑美观因素的场景以外,直接在`pom.xml`文件中修改`maven-surefire-plugin`插件配置依然是更加实用的方案。

View File

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

@@ -0,0 +1,2 @@
* [首页](/zh-cn/)
* [问题和反馈](https://github.com/alibaba/testable-mock/issues)

14
docs/zh-cn/sidebar.md Normal file
View 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)

View File

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

View File

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

View File

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

View File

@@ -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";
}

View File

@@ -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]);

View File

@@ -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++) {

View File

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

View File

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

View File

@@ -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));
}
}

View File

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

View File

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

View File

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

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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)
};

View File

@@ -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());

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +0,0 @@
package com.alibaba.testable.core.constant;
/**
* @author flin
*/
public final class ConstPool {
public static final String TEST_POSTFIX = "Test";
}

View File

@@ -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);
}

View File

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

View File

@@ -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++) {

View File

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

View File

@@ -1,14 +0,0 @@
package com.alibaba.testable.core.model;
/**
* Type of mock method
*
* @author flin
*/
public enum MockType {
MEMBER_METHOD,
STATIC_METHOD,
CONSTRUCTOR
}

View File

@@ -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));
}
}

View File

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

View File

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

View File

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

View File

@@ -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"));
}
}

View File

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

View File

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

View File

@@ -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"));

View File

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

View File

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