Compare commits

...

59 Commits

Author SHA1 Message Date
金戟
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
金戟
7e88813518 v0.2.2 released 2020-11-07 23:39:21 +08:00
金戟
88fddf2bcc allow to check method never invoke with parameters 2020-11-07 23:24:53 +08:00
金戟
ba2a823c19 add second solution for IDE test problem 2020-11-05 16:53:01 +08:00
金戟
4b282179f4 fix tools.jar issue with java 9+ 2020-11-04 11:48:07 +08:00
金戟
6454ae547e quick check several invocation with same parameters 2020-11-01 13:43:56 +08:00
金戟
5b7382e9b0 require more stack space 2020-11-01 13:42:44 +08:00
金戟
dc904496fc rename verify method names 2020-11-01 09:41:01 +08:00
金戟
6f74ba771c upgrade surefire plugin to fix conflict with junit 5 2020-11-01 09:02:00 +08:00
金戟
df63bc5c74 fix unsafe access warning 2020-11-01 07:53:04 +08:00
金戟
da3eca3d49 mn.parameters could be null 2020-10-31 22:29:22 +08:00
金戟
d816726f4f implementation parameter verification 2020-10-31 21:47:03 +08:00
金戟
4b1b1c3126 bump version to 0.2.2 snapshot 2020-10-31 18:39:38 +08:00
金戟
649fdebda7 record parameters of mock invoke 2020-10-31 18:27:28 +08:00
金戟
24e4b4f317 refactor invoke record logic to its own util 2020-10-30 21:39:02 +08:00
金戟
0b9a9c7ac6 version tips 2020-10-30 17:59:10 +08:00
金戟
e33c208b8d split private access and mock demo class 2020-10-28 09:12:25 +08:00
金戟
afecf4ddf5 add faq and known issue doc 2020-10-27 23:08:27 +08:00
金戟
ae134ac5ce support mock invoke by interface instance 2020-10-27 20:00:25 +08:00
金戟
36136e878e fit kotlin companion object as static method 2020-10-27 11:54:00 +08:00
金戟
0ef13a8e95 implement static method mock 2020-10-27 09:22:42 +08:00
金戟
989a52a048 split core package into core and processor 2020-10-27 06:55:32 +08:00
金戟
419a10669d add case to test kotlin invoke java method 2020-10-26 20:39:12 +08:00
金戟
2ad671e1f9 use add instead of insert 2020-10-26 07:52:47 +08:00
金戟
f9fc1e6224 0.2.0 released, bump everything to 0.2.1-snapshot 2020-10-25 21:15:26 +08:00
101 changed files with 3564 additions and 991 deletions

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,14 +1,16 @@
# Testable
# TestableMock
基于代码和字节码增强的Java单元测试辅助工具包含以下功能
换种思路写Mock让单元测试更简单。
- 使单元测试能直接调用和访问被测类的私有成员,解决私有方法无法测试的问题
- 使被测类的任意方法调用快速替换为Mock实现"指哪换哪"解决传统Mock工具使用繁琐的问题
无需初始化不挑测试框架甭管要换的方法是被测类的私有方法、静态方法还是其他任何类的成员方法也甭管要换的对象是怎么创建的。写好Mock方法加个`@TestableMock`注解,一切统统搞定。
文档https://alibaba.github.io/testable-mock/
## 目录结构
```bash
|-- testable-core ➜ 核心组件,提供测试辅助功能、注解和工具类
|-- testable-core ➜ 核心组件,提供注解和工具类
|-- testable-processor ➜ 编译期代码预处理组件,提供测试辅助功能
|-- testable-agent ➜ JavaAgent组件提供Mock测试相关功能
|-- testable-maven-plugin ➜ Maven插件组件用于简化JavaAgent注入
|-- demo
@@ -22,5 +24,14 @@
主项目使用JDK 1.6+和Maven 3+版本构建,其中`demo`子项目需要JDK 1.8+版本。
```bash
mvn clean package
mvn clean install
```
## 本地生成文档
```bash
docsify serve docs
```
> Testable文档使用`docsify`工具生成,构建前请安装[nodejs](https://nodejs.org/en/download/)运行时,并使用`npm install -g docsify`命令安装文档生成工具。

View File

@@ -1,105 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>java-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>java-demo</name>
<description>Demo project for Spring Boot</description>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<artifactId>java-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>java-demo</name>
<description>Demo project for TestableMock</description>
<properties>
<java.version>1.8</java.version>
<testable.version>0.2.0-SNAPSHOT</testable.version>
</properties>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<testable.version>0.3.1</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>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-core</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
<exclusion>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
</configuration>
</plugin>
<!-- <plugin>-->
<!-- <groupId>org.apache.maven.plugins</groupId>-->
<!-- <artifactId>maven-surefire-plugin</artifactId>-->
<!-- <configuration>-->
<!-- <argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>-->
<!-- </configuration>-->
<!-- </plugin>-->
<plugin>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-maven-plugin</artifactId>
<version>${testable.version}</version>
<executions>
<execution>
<id>prepare</id>
<goals>
<goal>prepare</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.6</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>target/jacoco.exec</dataFile>
<outputDirectory>target/jacoco-ut</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<!-- <plugin>-->
<!-- <groupId>com.alibaba.testable</groupId>-->
<!-- <artifactId>testable-maven-plugin</artifactId>-->
<!-- <version>${testable.version}</version>-->
<!-- <executions>-->
<!-- <execution>-->
<!-- <id>prepare</id>-->
<!-- <goals>-->
<!-- <goal>prepare</goal>-->
<!-- </goals>-->
<!-- </execution>-->
<!-- </executions>-->
<!-- </plugin>-->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.6</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>target/jacoco.exec</dataFile>
<outputDirectory>target/jacoco-ut</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,15 +0,0 @@
package com.alibaba.testable.demo;
public class BlackBox {
private String data;
public BlackBox(String data) {
this.data = data;
}
public String callMe() {
return data;
}
}

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,52 +1,44 @@
package com.alibaba.testable.demo;
import org.springframework.stereotype.Service;
import com.alibaba.testable.demo.model.BlackBox;
import sun.net.www.http.HttpClient;
import java.net.URL;
@Service
public class DemoService {
private int count;
public class DemoMock {
/**
* Target 1 - private method
*/
private String privateFunc(String s, int i) {
return s + " - " + i;
}
/**
* Target 2 - method with private field access
*/
public String privateFieldAccessFunc() {
count += 2;
return String.valueOf(count);
}
/**
* Target 3 - method with new operation
* method with new operation
*/
public String newFunc() {
BlackBox component = new BlackBox("something");
return component.callMe();
return component.get();
}
/**
* Target 4 - method with member method invoke
* method with member method invoke
*/
public String outerFunc(String s) throws Exception {
return "{ \"res\": \"" + innerFunc(s) + "\"}";
}
/**
* Target 5 - method with common method invoke
* method with common method invoke
*/
public String commonFunc() {
return "anything".trim() + "__" + "anything".substring(1, 2) + "__" + "abc".startsWith("ab");
}
/**
* method with static method invoke
*/
public BlackBox getBox() {
return BlackBox.secretBox();
}
/**
* two methods invoke same private method
*/
public String callerOne() {
return callFromDifferentMethod();
}

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -0,0 +1,37 @@
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("4", demoPrivateAccess.privateFieldAccessFunc());
assertEquals(new Integer(4), demoPrivateAccess.count);
PrivateAccessor.set(demoPrivateAccess, "count", 3);
assertEquals("5", demoPrivateAccess.privateFieldAccessFunc());
assertEquals(new Integer(5), PrivateAccessor.get(demoPrivateAccess, "count"));
}
@Test
void should_able_to_update_final_field() throws Exception {
demoPrivateAccess.pi = 4.13;
assertEquals(4.13, demoPrivateAccess.pi);
}
}

View File

@@ -1,108 +0,0 @@
package com.alibaba.testable.demo;
import com.alibaba.testable.core.accessor.PrivateAccessor;
import com.alibaba.testable.core.annotation.EnablePrivateAccess;
import com.alibaba.testable.core.annotation.TestableMock;
import org.junit.jupiter.api.Test;
import java.util.concurrent.Executors;
import static com.alibaba.testable.core.tool.TestableTool.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
@EnablePrivateAccess
class DemoServiceTest {
@TestableMock(targetMethod = CONSTRUCTOR)
private BlackBox createBlackBox(String text) {
return new BlackBox("mock_" + text);
}
@TestableMock
private String innerFunc(DemoService self, String text) {
return "mock_" + text;
}
@TestableMock
private String trim(String self) {
return "trim_string";
}
@TestableMock(targetMethod = "substring")
private String sub(String self, int i, int j) {
return "sub_string";
}
@TestableMock
private boolean startsWith(String self, String s) {
return false;
}
@TestableMock
private String callFromDifferentMethod(DemoService self) {
if (TEST_CASE.equals("should_able_to_get_test_case_name")) {
return "mock_special";
}
switch (SOURCE_METHOD) {
case "callerOne": return "mock_one";
default: return "mock_others";
}
}
private DemoService demoService = new DemoService();
@Test
void should_able_to_test_private_method() throws Exception {
assertEquals("hello - 1", demoService.privateFunc("hello", 1));
assertEquals("hello - 1", PrivateAccessor.invoke(demoService, "privateFunc", "hello", 1));
}
@Test
void should_able_to_test_private_field() throws Exception {
demoService.count = 2;
assertEquals("4", demoService.privateFieldAccessFunc());
PrivateAccessor.set(demoService, "count", 3);
assertEquals("5", demoService.privateFieldAccessFunc());
assertEquals(new Integer(5), PrivateAccessor.get(demoService, "count"));
}
@Test
void should_able_to_test_new_object() throws Exception {
assertEquals("mock_something", demoService.newFunc());
verify("createBlackBox").times(1);
}
@Test
void should_able_to_test_member_method() throws Exception {
assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello"));
verify("innerFunc").times(1);
}
@Test
void should_able_to_test_common_method() throws Exception {
assertEquals("trim_string__sub_string__false", demoService.commonFunc());
verify("trim").times(1);
verify("sub").times(1);
verify("startsWith").times(1);
}
@Test
void should_able_to_get_source_method_name() throws Exception {
// synchronous
assertEquals("mock_one_mock_others", demoService.callerOne() + "_" + demoService.callerTwo());
// asynchronous
assertEquals("mock_one_mock_others",
Executors.newSingleThreadExecutor().submit(() -> demoService.callerOne() + "_" + demoService.callerTwo()).get());
verify("callFromDifferentMethod").times(4);
}
@Test
void should_able_to_get_test_case_name() throws Exception {
// synchronous
assertEquals("mock_special", demoService.callerOne());
// asynchronous
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoService.callerOne()).get());
verify("callFromDifferentMethod").times(2);
}
}

View File

@@ -2,69 +2,54 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>kotlin-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>1.0.0-SNAPSHOT</version>
<name>kotlin-demo</name>
<description>Demo project for Spring Boot</description>
<description>Demo project for TestableMock</description>
<properties>
<java.version>1.8</java.version>
<kotlin.version>1.3.72</kotlin.version>
<testable.version>0.2.0-SNAPSHOT</testable.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<testable.version>0.3.1</testable.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-core</artifactId>
<artifactId>testable-processor</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>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 +71,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 +95,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

@@ -1,22 +0,0 @@
package com.alibaba.testable.demo
class BlackBox(private val data: String) {
fun callMe(): String {
return data
}
fun trim(): String {
return data.trim()
}
fun substring(from: Int, to: Int): String {
return data.substring(from, to)
}
fun startsWith(prefix: String): Boolean {
return data.startsWith(prefix)
}
}

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,52 +1,44 @@
package com.alibaba.testable.demo
import org.springframework.stereotype.Service
import com.alibaba.testable.demo.model.BlackBox
import com.alibaba.testable.demo.model.ColorBox
import sun.net.www.http.HttpClient
import java.net.URL
@Service
class DemoService {
private var count = 0
class DemoMock {
/**
* Target 1 - private method
* method with new operation
*/
private fun privateFunc(s: String, i: Int): String {
return "$s - $i"
fun newFunc(): String? {
return BlackBox("something").get()
}
/**
* Target 2 - method with private field access
*/
fun privateFieldAccessFunc(): String {
count += 2
return count.toString()
}
/**
* Target 3 - method with new operation
*/
fun newFunc(): String {
return BlackBox("something").callMe()
}
/**
* Target 4 - method with member method invoke
* method with member method invoke
*/
fun outerFunc(s: String): String {
return "{ \"res\": \"" + innerFunc(s) + "\"}"
}
/**
* Target 5 - method with common method invoke
* method with common method invoke
*/
fun commonFunc(): String {
val box = BlackBox("anything")
return box.trim() + "__" + box.substring(1, 2) + "__" + box.startsWith("any")
}
/**
* method with static method invoke
*/
fun getBox(): BlackBox {
return ColorBox.createBox("Red", BlackBox.secretBox())
}
/**
* two methods invoke same private method
*/
fun callerOne(): String {
return callFromDifferentMethod()
}

View File

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

View File

@@ -0,0 +1,41 @@
package com.alibaba.testable.demo.model
class BlackBox(var input: String) : Box(), Color {
init {
this.content = input
}
override fun put(something: String) {
content = something
}
override val color: String
get() = "black"
fun trim(): String? {
return content?.trim()
}
fun substring(from: Int, to: Int): String? {
return content?.substring(from, to)
}
fun startsWith(prefix: String): Boolean {
return content?.startsWith(prefix) == true
}
companion object {
fun secretBox(): BlackBox {
return BlackBox("secret")
}
}
}
object ColorBox {
fun createBox(color: String, box: BlackBox): BlackBox {
return BlackBox("${color}_${box.get()}")
}
}

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
package com.alibaba.testable.demo.util
import java.io.File
import java.io.IOException
object PathUtil {
fun deleteRecursively(file: File) {
if (!file.exists()) {
return
}
val fileList = file.listFiles()
if (fileList != null) {
for (childFile in fileList) {
if (childFile.isDirectory) {
deleteRecursively(childFile)
} else if (!childFile.delete()) {
throw IOException()
}
}
}
if (file.exists() && !file.delete()) {
throw IOException("Unable to delete file " + file.absolutePath)
}
}
}

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

@@ -0,0 +1,101 @@
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.ColorBox
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.util.concurrent.Executors
internal class DemoMockTest {
@TestableMock(targetMethod = CONSTRUCTOR)
private fun createBlackBox(text: String) = BlackBox("mock_$text")
@TestableMock
private fun innerFunc(self: DemoMock, text: String) = "mock_$text"
@TestableMock
private fun trim(self: BlackBox) = "trim_string"
@TestableMock(targetMethod = "substring")
private fun sub(self: BlackBox, i: Int, j: Int) = "sub_string"
@TestableMock
private fun startsWith(self: BlackBox, s: String) = false
@TestableMock
private fun secretBox(ignore: BlackBox): BlackBox {
return BlackBox("not_secret_box")
}
@TestableMock
private fun createBox(ignore: ColorBox, color: String, box: BlackBox): BlackBox {
return BlackBox("White_${box.get()}")
}
@TestableMock
private fun callFromDifferentMethod(self: DemoMock): String {
return if (TEST_CASE == "should_able_to_get_test_case_name") {
"mock_special"
} else {
when (SOURCE_METHOD) {
"callerOne" -> "mock_one"
else -> "mock_others"
}
}
}
private val demoMock = DemoMock()
@Test
fun should_able_to_mock_new_object() {
assertEquals("mock_something", demoMock.newFunc())
verify("createBlackBox").with("something")
}
@Test
fun should_able_to_mock_member_method() {
assertEquals("{ \"res\": \"mock_hello\"}", demoMock.outerFunc("hello"))
verify("innerFunc").with("hello")
}
@Test
fun should_able_to_mock_common_method() {
assertEquals("trim_string__sub_string__false", demoMock.commonFunc())
verify("trim").withTimes(1)
verify("sub").withTimes(1)
verify("startsWith").withTimes(1)
}
@Test
fun should_able_to_mock_static_method() {
assertEquals("White_not_secret_box", demoMock.getBox().get())
verify("secretBox").withTimes(1)
verify("createBox").withTimes(1)
}
@Test
fun should_able_to_get_source_method_name() {
// synchronous
assertEquals("mock_one_mock_others", demoMock.callerOne() + "_" + demoMock.callerTwo())
// asynchronous
assertEquals("mock_one_mock_others", Executors.newSingleThreadExecutor().submit<String> {
demoMock.callerOne() + "_" + demoMock.callerTwo()
}.get())
verify("callFromDifferentMethod").withTimes(4)
}
@Test
fun should_able_to_get_test_case_name() {
// synchronous
assertEquals("mock_special", demoMock.callerOne())
// asynchronous
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit<String> {
demoMock.callerOne()
}.get())
verify("callFromDifferentMethod").withTimes(2)
}
}

View File

@@ -0,0 +1,30 @@
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("5", demoPrivateAccess.privateFieldAccessFunc())
assertEquals(5, PrivateAccessor.get(demoPrivateAccess, "count"))
}
@Test
fun should_able_to_update_final_field() {
PrivateAccessor.set(demoPrivateAccess, "pi", 4.13)
assertEquals(4.13, demoPrivateAccess.pi)
}
}

View File

@@ -1,97 +0,0 @@
package com.alibaba.testable.demo
import com.alibaba.testable.core.accessor.PrivateAccessor
import com.alibaba.testable.core.annotation.EnablePrivateAccess
import com.alibaba.testable.core.annotation.TestableMock
import com.alibaba.testable.core.tool.TestableTool.*
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.util.concurrent.Executors
@EnablePrivateAccess
internal class DemoServiceTest {
@TestableMock(targetMethod = CONSTRUCTOR)
private fun createBlackBox(text: String) = BlackBox("mock_$text")
@TestableMock
private fun innerFunc(self: DemoService, text: String) = "mock_$text"
@TestableMock
private fun trim(self: BlackBox) = "trim_string"
@TestableMock(targetMethod = "substring")
private fun sub(self: BlackBox, i: Int, j: Int) = "sub_string"
@TestableMock
private fun startsWith(self: BlackBox, s: String) = false
@TestableMock
private fun callFromDifferentMethod(self: DemoService): String {
return if (TEST_CASE == "should_able_to_get_test_case_name") {
"mock_special"
} else {
when (SOURCE_METHOD) {
"callerOne" -> "mock_one"
else -> "mock_others"
}
}
}
private val demoService = DemoService()
@Test
fun should_able_to_test_private_method() {
assertEquals("hello - 1", PrivateAccessor.invoke(demoService, "privateFunc", "hello", 1))
}
@Test
fun should_able_to_test_private_field() {
PrivateAccessor.set(demoService, "count", 3)
assertEquals("5", demoService.privateFieldAccessFunc())
assertEquals(5, PrivateAccessor.get(demoService, "count"))
}
@Test
fun should_able_to_test_new_object() {
assertEquals("mock_something", demoService.newFunc())
verify("createBlackBox").times(1)
}
@Test
fun should_able_to_test_member_method() {
assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello"))
verify("innerFunc").times(1)
}
@Test
fun should_able_to_test_common_method() {
assertEquals("trim_string__sub_string__false", demoService.commonFunc())
verify("trim").times(1)
verify("sub").times(1)
verify("startsWith").times(1)
}
@Test
fun should_able_to_get_source_method_name() {
// synchronous
assertEquals("mock_one_mock_others", demoService.callerOne() + "_" + demoService.callerTwo())
// asynchronous
assertEquals("mock_one_mock_others", Executors.newSingleThreadExecutor().submit<String> {
demoService.callerOne() + "_" + demoService.callerTwo()
}.get())
verify("callFromDifferentMethod").times(4)
}
@Test
fun should_able_to_get_test_case_name() {
// synchronous
assertEquals("mock_special", demoService.callerOne())
// asynchronous
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit<String> {
demoService.callerOne()
}.get())
verify("callFromDifferentMethod").times(2)
}
}

View File

@@ -0,0 +1,48 @@
package com.alibaba.testable.demo.util
import com.alibaba.testable.core.annotation.TestableMock
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
import org.junit.jupiter.api.Test
import java.io.File
class PathUtilTest {
@TestableMock
fun exists(f: File): Boolean {
return when (f.absolutePath) {
"/a/b" -> true
"/a/b/c" -> true
else -> f.exists()
}
}
@TestableMock
fun isDirectory(f: File): Boolean {
return when (f.absolutePath) {
"/a/b/c" -> true
else -> f.isDirectory
}
}
@TestableMock
fun delete(f: File): Boolean {
return true
}
@TestableMock
fun listFiles(f: File): Array<File>? {
return when (f.absolutePath) {
"/a/b" -> arrayOf(File("/a/b/c"), File("/a/b/d"))
"/a/b/c" -> arrayOf(File("/a/b/c/e"))
else -> f.listFiles()
}
}
@Test
fun should_able_to_mock_java_method_invoke_in_kotlin() {
PathUtil.deleteRecursively(File("/a/b/"))
verify("listFiles").withTimes(2)
verify("delete").withTimes(4)
}
}

View File

@@ -1,88 +0,0 @@
使用说明
---
## 引入Testable
首先在项目`pom.xml`文件中添加`testable-core`依赖:
```xml
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-core</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
```
此时项目就获得了在单元测试中随意访问被测类私有字段和方法的能力(需配合注解使用,见下文详述)。
若要开启极速Mock功能还需在`pom.xml`里加上`testable-maven-plugin`插件。
```xml
<plugin>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-maven-plugin</artifactId>
<version>${testable.version}</version>
<executions>
<execution>
<id>prepare</id>
<goals>
<goal>prepare</goal>
</goals>
</execution>
</executions>
</plugin>
```
## 使用Testable
`Testable`目前能为测试类提供两项增强能力__直接访问被测类的私有成员__ 和 __极速Mock被测方法中的调用__
### 访问私有成员字段和方法
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
- 调用被测类的私有方法
- 读取被测类的私有成员
- 修改被测类的私有成员
- 修改被测类的常量成员使用final或static final修饰的成员
访问和修改私有、常量成员时IDE可能会提示语法有误但编译器将能够正常运行测试。
若不希望看到IDE的语法错误提醒或是在基于JVM的非Java语言项目里譬如Kotlin语言也可以借助`PrivateAccessor`工具类来实现私有成员的访问。
效果见示例项目文件`DemoServiceTest.java`中的`should_able_to_test_private_method()``should_able_to_test_private_field()`测试用例。
### Mock被测类的任意方法调用
**【1】覆写任意类的方法调用**
在测试类里定义一个有`@TestableMock`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
此时被测类中所有对该需覆写方法的调用将在单元测试运行时将自动被替换为对上述自定义Mock方法的调用。
**注意**:当遇到有两个需覆写的方法重名时,可将需覆写的方法名写到`@TestableMock`注解的`targetMethod`参数里此时Mock方法自身就可以随意命名了。
示例项目文件`DemoServiceTest.java`中的`should_able_to_test_common_method()`用例详细展示了这种用法。
**【2】覆写被测类自身的成员方法**
有时候在对某些方法进行测试时希望将被测类自身的另外一些成员方法Mock掉。
操作方法与前一种情况相同Mock方法的第一个参数类型需与被测类相同即可实现对被测类自身不论是公有或私有成员方法的覆写。
详见示例项目文件`DemoServiceTest.java`中的`should_able_to_test_member_method()`用例。
**【3】覆写任意类的new操作**
在测试类里定义一个有`@TestableMock`注解的普通方法,将注解的`targetMethod`参数写为"<init>",然后使该方法与要被创建类型的构造函数参数、返回值类型完全一致,方法名称随意。
此时被测类中所有用`new`创建指定类的操作并使用了与Mock方法参数一致的构造函数将被替换为对该自定义方法的调用。
详见示例项目文件`DemoServiceTest.java`中的`should_able_to_test_new_object()`用例。
**【4】识别当前测试用例和调用来源**
在Mock方法中可以通过`TestableTool.TEST_CASE``TestableTool.SOURCE_METHOD`来识别**当前运行的测试用例名称**和**进入该Mock方法前的被测类方法名称**,从而区分处理不同的调用场景。
详见示例项目文件`DemoServiceTest.java`中的`should_able_to_get_source_method_name()``should_able_to_get_test_case_name()`用例。

40
docs/index.html Normal file
View File

@@ -0,0 +1,40 @@
<!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="https://cdn.jsdelivr.net/npm/docsify-tabs@1"></script>
<script async defer src="https://buttons.github.io/buttons.js"></script>
</body>
</html>

12
docs/zh-cn/README.md Normal file
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,22 @@
常见使用问题
---
#### 1. 如何Mock被测类中通过`@Autowired`初始化的字段?
直接创建被测类对象,然后利用`TestableMock`访问私有成员的能力直接给这些字段赋值即可。
#### 2. 父类变量指向子类对象时如何实现Mock方法
在代码中,经常会有使用<u>接口变量或父类变量</u>指向子类实例,调用父类或子类方法的情况。
这时候遵循一个原则Mock方法的首个参数类型**始终与发起调用的变量类型一致**。
因此不论被调用方法来自父类还是子类也不论子类是否覆写该方法Mock方法的首个参数类型都应该使用变量自身的接口或父类类型。
参见Java和Kotlin示例中`DemoInheritTest`测试类的用例。
#### 3. `TestableMock`能否用于Android项目的测试
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。
Android系统的`Dalvik``ART`虚拟机采用了与标准JVM不同的字节码体系会影响`TestableMock`的正常工作。`Roboelectric`框架能在普通JVM虚拟机上运行Android单元测试其速度比通过Android虚拟机运行单元测试快非常多绝大多数Android App的单元测试都在使用`Roboelectric`框架。

75
docs/zh-cn/doc/matcher.md Normal file
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,15 @@
访问私有成员字段和方法
---
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
- 调用被测类的私有方法
- 读取被测类的私有成员
- 修改被测类的私有成员
- 修改被测类的常量成员使用final修饰的成员
访问和修改私有、常量成员时IDE可能会提示语法有误但编译器将能够正常运行测试。
若不希望看到IDE的语法错误提醒或是在非Java语言的JVM项目里譬如Kotlin语言也可以借助`PrivateAccessor`工具类来实现私有成员的访问。
效果见`java-demo``kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。

View File

@@ -1,5 +1,24 @@
# Release Note
## upcoming version
- support share mock method between test classes
## 0.3.1
- support detail log of mocking process for diagnosis
## v0.3.0
- add `without()` checker to verify mock method never invoked with specified parameters
- support fuzz matcher when verifying mock invocation
## v0.2.2
- support mock method parameters check
- fix a compatibility issue with jvm 9+
## v0.2.1
- support mock static method
- support mock kotlin companion object method
- support mock invoke by interface / base class object
## v0.2.0
- use `TestableTool` class to expose test context and verify mock invoke
- add `testable-maven-plugin` module to simplify javaagent configuration

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

74
docs/zh-cn/doc/usage.md Normal file
View File

@@ -0,0 +1,74 @@
使用TestableMock
---
`TestableMock`是基于源码和字节码增强的Java单元测试辅助工具包含以下功能
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
- [快速Mock任意方法](zh-cn/doc/use-mock.md)使被测类的任意方法调用快速替换为Mock方法实现"指哪换哪"解决传统Mock工具使用繁琐的问题
## 在Maven项目中使用
在项目`pom.xml`文件中,增加`testable-processor`依赖和`maven-surefire-plugin`配置,具体方法如下。
建议先添加一个标识TestableMock版本的`property`,便于统一管理:
```xml
<properties>
<testable.version>0.3.1</testable.version>
</properties>
```
`dependencies`列表添加`testable-processor`依赖:
```xml
<dependencies>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-processor</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>
```
## 在Gradle项目中使用
`build.gradle`文件中添加`testable-processor`依赖:
```groovy
dependencies {
testCompile('com.alibaba.testable:testable-processor:0.3.1')
}
```
然后在测试配置中添加javaagent
```groovy
test {
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
}
```
> 该配置尚未在Gradle项目上经过实际验证可行性待确认。

View File

@@ -0,0 +1,34 @@
使用Testable Maven插件
---
在使用Maven构建的项目里除了直接修改`maven-surefire-plugin`插件的运行参数,也可通过`testable-maven-plugin`插件获得相同效果:
```xml
<plugin>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-maven-plugin</artifactId>
<version>${testable.version}</version>
<executions>
<execution>
<id>prepare</id>
<goals>
<goal>prepare</goal>
</goals>
</execution>
</executions>
</plugin>
```
> 当使用`testable-maven-plugin`插件时,应该移除`maven-surefire-plugin`插件上的TestableMock相关配置
相比而言,`testable-maven-plugin`插件能够与Jacoco插件直接同时使用无需额外适配。但当通过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`插件配置的方案。

View File

@@ -0,0 +1,94 @@
快速Mock被测类的任意方法调用
---
#### 1. 覆写任意类的方法调用
在测试类里定义一个有`@TestableMock`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
此时被测类中所有对该需覆写方法的调用将在单元测试运行时将自动被替换为对上述自定义Mock方法的调用。
**注意**:也可以将需覆写的方法名写到`@TestableMock`注解的`targetMethod`参数里这样Mock方法自身就可以随意命名了当遇到重名的待覆写方法时特别有用
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在测试类定义如下方法:
```java
// 原方法签名为`String substring(int, int)`
// 调用此方法的对象`"anything"`类型为`String`
// 则Mock方法签名在其参数列表首位增加一个类型为`String`的参数(名字随意)
// 此参数可用于获得当时的实际调用者的值和上下文
@TestableMock
private String substring(String self, int i, int j) {
return "sub_string";
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_common_method()`测试用例。(由于Kotlin对String类型进行了魔改故Kotlin示例中将被测方法在`BlackBox`类里加了一层封装)
#### 2. 覆写被测类自身的成员方法
有时候在对某些方法进行测试时希望将被测类自身的另外一些成员方法Mock掉。
操作方法与前一种情况相同Mock方法的第一个参数类型需与被测类相同即可实现对被测类自身不论是公有或私有成员方法的覆写。
例如,被测类中有一个签名为`String innerFunc(String)`的私有方法,我们希望在测试的时候将它替换掉,则只需在测试类定义如下方法:
```java
// 被测类型是`DemoMock`
// 因此在定义Mock方法时在目标方法参数首位加一个类型为`DemoMock`的参数(名字随意)
@TestableMock
private String innerFunc(DemoMock self, String text) {
return "mock_" + text;
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_member_method()`测试用例。
#### 3. 覆写任意类的静态方法
对于静态方法的Mock与普通方法相同。但需要注意的是对于静态方法传入Mock方法的第一个参数实际值始终是`null`
例如,在被测类中调用了`BlackBox`类型中的静态方法`secretBox()`,改方法签名为`BlackBox secretBox()`则Mock方法如下
```java
// 目标静态方法定义在`BlackBox`类型中
// 在定义Mock方法时在目标方法参数首位加一个类型为`BlackBox`的参数(名字随意)
// 此参数仅用于标识目标类型,实际传入值将始终为`null`
@TestableMock
private BlackBox secretBox(BlackBox ignore) {
return new BlackBox("not_secret_box");
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_static_method()`测试用例。
#### 4. 覆写任意类的new操作
在测试类里定义一个有`@TestableMock`注解的普通方法,将注解的`targetMethod`参数写为"<init>",然后使该方法与要被创建类型的构造函数参数、返回值类型完全一致,方法名称随意。
此时被测类中所有用`new`创建指定类的操作并使用了与Mock方法参数一致的构造函数将被替换为对该自定义方法的调用。
例如,在被测类中有一处`new BlackBox("something")`调用希望在测试时将它换掉通常是换成Mock对象或换成使用测试参数创建的临时对象则只需定义如下Mock方法
```java
// 要覆写的构造函数签名为`BlackBox(String)`
// 无需在Mock方法参数列表增加额外参数由于使用了`targetMethod`参数Mock方法的名称随意起
// 此处的`CONSTRUCTOR`为`TestableTool`辅助类提供的常量,值为"<init>"
@TestableMock(targetMethod = CONSTRUCTOR)
private BlackBox createBlackBox(String text) {
return new BlackBox("mock_" + text);
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_new_object()`测试用例。
#### 5. 识别当前测试用例和调用来源
在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)

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

@@ -0,0 +1,11 @@
- 使用指南
- [使用TestableMock](zh-cn/doc/usage.md)
- [直接访问私有成员](zh-cn/doc/private-accessor.md)
- [快速Mock任意方法](zh-cn/doc/use-mock.md)
- [校验Mock调用](zh-cn/doc/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,12 +4,13 @@
<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>
<modules>
<module>testable-core</module>
<module>testable-processor</module>
<module>testable-agent</module>
<module>testable-maven-plugin</module>
<module>demo</module>

View File

@@ -1,95 +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.1</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.0-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.1</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>
<testable.version>0.2.0-SNAPSHOT</testable.version>
<plugin.compiler.version>3.8.1</plugin.compiler.version>
<plugin.jar.version>3.2.0</plugin.jar.version>
<plugin.shade.version>3.2.4</plugin.shade.version>
</properties>
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-tree</artifactId>
<version>${asm.lib.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-core</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-tree</artifactId>
<version>${asm.lib.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-core</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>testable-agent</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${plugin.compiler.version}</version>
<configuration>
<source>${project.compiler.level}</source>
<target>${project.compiler.level}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${plugin.jar.version}</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.alibaba.testable.agent.PreMain</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
<build>
<finalName>testable-agent</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${plugin.compiler.version}</version>
<configuration>
<source>${project.compiler.level}</source>
<target>${project.compiler.level}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${plugin.jar.version}</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.alibaba.testable.agent.PreMain</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${plugin.shade.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.objectweb.asm</pattern>
<shadedPattern>agent.org.objectweb.asm</shadedPattern>
</relocation>
</relocations>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${plugin.shade.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.objectweb.asm</pattern>
<shadedPattern>agent.org.objectweb.asm</shadedPattern>
</relocation>
</relocations>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${plugin.surefire.version}</version>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${plugin.source.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${plugin.javadoc.version}</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>${plugin.gpg.version}</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>${plugin.staging.version}</version>
<extensions>true</extensions>
<configuration>
<serverId>oss</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<snapshotRepository>
<id>oss</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
</snapshotRepository>
<repository>
<id>oss</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
</profile>
</profiles>
</project>

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

@@ -12,5 +12,8 @@ public class ConstPool {
public static final String TEST_POSTFIX = "Test";
public static final String TESTABLE_INJECT_REF = "_testableInternalRef";
public static final String FIELD_TARGET_METHOD = "targetMethod";
public static final String MOCK_WITH = "com.alibaba.testable.core.annotation.MockWith";
public static final String 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.*;
@@ -19,8 +20,10 @@ public class SourceClassHandler extends BaseClassHandler {
private final List<MethodInfo> injectMethods;
private final Set<Integer> invokeOps = new HashSet<Integer>() {{
add(Opcodes.INVOKESPECIAL);
add(Opcodes.INVOKEVIRTUAL);
add(Opcodes.INVOKESPECIAL);
add(Opcodes.INVOKESTATIC);
add(Opcodes.INVOKEINTERFACE);
}};
public SourceClassHandler(List<MethodInfo> injectMethods) {
@@ -49,6 +52,7 @@ public class SourceClassHandler extends BaseClassHandler {
private void transformMethod(ClassNode cn, MethodNode mn, Set<MethodInfo> memberInjectMethods,
Set<MethodInfo> newOperatorInjectMethods) {
LogUtil.debug(" Handling method %s", mn.name);
AbstractInsnNode[] instructions = mn.instructions.toArray();
List<MethodInfo> memberInjectMethodList = new ArrayList<MethodInfo>(memberInjectMethods);
int i = 0;
@@ -57,10 +61,11 @@ public class SourceClassHandler extends BaseClassHandler {
MethodInsnNode node = (MethodInsnNode)instructions[i];
String memberInjectMethodName = getMemberInjectMethodName(memberInjectMethodList, node);
if (memberInjectMethodName != null) {
// it's a member method and an inject method for it exist
// it's a member or static method and an inject method for it exist
int rangeStart = getMemberMethodStart(instructions, i);
if (rangeStart >= 0) {
instructions = replaceMemberCallOps(cn, mn, instructions, node.owner, memberInjectMethodName, rangeStart, i);
instructions = replaceMemberCallOps(cn, mn, memberInjectMethodName, instructions,
node.owner, node.getOpcode(), rangeStart, i);
i = rangeStart;
}
} else if (ConstPool.CONSTRUCTOR.equals(node.name)) {
@@ -82,7 +87,8 @@ public class SourceClassHandler extends BaseClassHandler {
private String getMemberInjectMethodName(List<MethodInfo> memberInjectMethodList, MethodInsnNode node) {
for (MethodInfo m : memberInjectMethodList) {
if (m.getClazz().equals(node.owner) && m.getName().equals(node.name) && m.getDesc().equals(node.desc)) {
String nodeOwner = ClassUtil.fitCompanionClassName(node.owner);
if (m.getClazz().equals(nodeOwner) && m.getName().equals(node.name) && m.getDesc().equals(node.desc)) {
return m.getMockName();
}
}
@@ -116,13 +122,22 @@ 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.INVOKEINTERFACE:
case Opcodes.INVOKEVIRTUAL:
case Opcodes.INVOKESPECIAL:
case Opcodes.INVOKEDYNAMIC:
case Opcodes.INVOKESTATIC:
stackLevel += ClassUtil.getParameterTypes(((MethodInsnNode)instructions[i]).desc).size();
case Opcodes.INVOKEVIRTUAL:
case Opcodes.INVOKEINTERFACE:
stackLevel += stackEffectOfInvocation(instructions[i]) + 1;
if (((MethodInsnNode)instructions[i]).name.equals(ConstPool.CONSTRUCTOR)) {
// constructor must be INVOKESPECIAL and implicitly pop 1 more stack
stackLevel++;
}
break;
case Opcodes.INVOKESTATIC:
case Opcodes.INVOKEDYNAMIC:
stackLevel += stackEffectOfInvocation(instructions[i]);
break;
case -1:
// reach LineNumberNode or LabelNode
return i + 1;
default:
stackLevel -= BytecodeUtil.stackEffect(instructions[i].getOpcode());
}
@@ -133,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);
@@ -148,24 +170,48 @@ public class SourceClassHandler extends BaseClassHandler {
return mn.instructions.toArray();
}
private int getLineNum(AbstractInsnNode[] instructions, int start) {
for (int i = start - 1; i >= 0; i--) {
if (instructions[i] instanceof LineNumberNode) {
return ((LineNumberNode)instructions[i]).line;
}
}
return 0;
}
private String getConstructorInjectDesc(String constructorDesc, String classType) {
return constructorDesc.substring(0, constructorDesc.length() - 1) +
ClassUtil.toByteCodeClassName(classType);
}
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions,
String ownerClass, String substitutionMethod, int start, int end) {
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, String substitutionMethod,
AbstractInsnNode[] instructions, String ownerClass,
int opcode, int start, int end) {
LogUtil.debug(" Line %d, mock method %s used", getLineNum(instructions, start), substitutionMethod);
mn.maxStack++;
MethodInsnNode method = (MethodInsnNode)instructions[end];
String testClassName = ClassUtil.getTestClassName(cn.name);
mn.instructions.insertBefore(instructions[start], new FieldInsnNode(GETSTATIC, testClassName,
ConstPool.TESTABLE_INJECT_REF, ClassUtil.toByteCodeClassName(testClassName)));
if (Opcodes.INVOKESTATIC == opcode || isCompanionMethod(ownerClass, opcode)) {
// append a null value if it was a static invoke or in kotlin companion class
mn.instructions.insertBefore(instructions[start], new InsnNode(ACONST_NULL));
if (ClassUtil.isCompanionClassName(ownerClass)) {
// for kotlin companion class, remove the byte code of reference to "companion" static field
mn.instructions.remove(instructions[end - 1]);
}
}
// method with @TestableMock will be modified as public access, so INVOKEVIRTUAL is used
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,
substitutionMethod, addFirstParameter(method.desc, ownerClass), false));
substitutionMethod, addFirstParameter(method.desc, ClassUtil.fitCompanionClassName(ownerClass)), false));
mn.instructions.remove(instructions[end]);
return mn.instructions.toArray();
}
private boolean isCompanionMethod(String ownerClass, int opcode) {
return Opcodes.INVOKEVIRTUAL == opcode && ClassUtil.isCompanionClassName(ownerClass);
}
private String addFirstParameter(String desc, String ownerClass) {
return "(" + ClassUtil.toByteCodeClassName(ownerClass) + desc.substring(1);
}

View File

@@ -1,6 +1,8 @@
package com.alibaba.testable.agent.handler;
import com.alibaba.testable.agent.constant.ConstPool;
import com.alibaba.testable.agent.tool.ImmutablePair;
import com.alibaba.testable.agent.util.AnnotationUtil;
import com.alibaba.testable.agent.util.ClassUtil;
import org.objectweb.asm.tree.*;
@@ -16,13 +18,14 @@ public class TestClassHandler extends BaseClassHandler {
private static final String CLASS_TESTABLE_TOOL = "com/alibaba/testable/core/tool/TestableTool";
private static final String CLASS_TESTABLE_UTIL = "com/alibaba/testable/core/util/TestableUtil";
private static final String CLASS_INVOKE_RECORD_UTIL = "com/alibaba/testable/core/util/InvokeRecordUtil";
private static final String FIELD_TEST_CASE = "TEST_CASE";
private static final String FIELD_SOURCE_METHOD = "SOURCE_METHOD";
private static final String METHOD_CURRENT_TEST_CASE_NAME = "currentTestCaseName";
private static final String METHOD_CURRENT_SOURCE_METHOD_NAME = "currentSourceMethodName";
private static final String METHOD_COUNT_MOCK_INVOKE = "countMockInvoke";
private static final String METHOD_RECORD_MOCK_INVOKE = "recordMockInvoke";
private static final String SIGNATURE_TESTABLE_UTIL_METHOD = "(Ljava/lang/Object;)Ljava/lang/String;";
private static final String SIGNATURE_INVOKE_COUNTER_METHOD = "()V";
private static final String SIGNATURE_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;Z)V";
private static final Map<String, String> FIELD_TO_METHOD_MAPPING = new HashMap<String, String>() {{
put(FIELD_TEST_CASE, METHOD_CURRENT_TEST_CASE_NAME);
put(FIELD_SOURCE_METHOD, METHOD_CURRENT_SOURCE_METHOD_NAME);
@@ -49,6 +52,7 @@ public class TestClassHandler extends BaseClassHandler {
private void handleAnnotation(ClassNode cn, MethodNode mn) {
List<String> visibleAnnotationNames = new ArrayList<String>();
if (mn.visibleAnnotations == null) {
// let's assume test case should has a annotation, e.g. @Test or whatever
return;
}
for (AnnotationNode n : mn.visibleAnnotations) {
@@ -58,7 +62,7 @@ public class TestClassHandler extends BaseClassHandler {
mn.access &= ~ACC_PRIVATE;
mn.access &= ~ACC_PROTECTED;
mn.access |= ACC_PUBLIC;
injectInvokeCounter(mn);
injectInvokeRecorder(mn);
} else if (couldBeTestMethod(mn)) {
injectTestableRef(cn, mn);
}
@@ -85,20 +89,93 @@ public class TestClassHandler extends BaseClassHandler {
private AbstractInsnNode[] replaceTestableUtilField(MethodNode mn, AbstractInsnNode[] instructions,
String fieldName, int pos) {
InsnList insnNodes = new InsnList();
// NOTE: will insert in reversed order
insnNodes.insert(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, FIELD_TO_METHOD_MAPPING.get(fieldName),
InsnList il = new InsnList();
il.add(new VarInsnNode(ALOAD, 0));
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, FIELD_TO_METHOD_MAPPING.get(fieldName),
SIGNATURE_TESTABLE_UTIL_METHOD, false));
insnNodes.insert(new VarInsnNode(ALOAD, 0));
mn.instructions.insert(instructions[pos], insnNodes);
mn.instructions.insert(instructions[pos], il);
mn.instructions.remove(instructions[pos]);
return mn.instructions.toArray();
}
private void injectInvokeCounter(MethodNode mn) {
MethodInsnNode node = new MethodInsnNode(INVOKESTATIC, CLASS_TESTABLE_UTIL, METHOD_COUNT_MOCK_INVOKE,
SIGNATURE_INVOKE_COUNTER_METHOD, false);
mn.instructions.insertBefore(mn.instructions.get(0), node);
private void injectInvokeRecorder(MethodNode mn) {
InsnList il = new InsnList();
List<Byte> types = ClassUtil.getParameterTypes(mn.desc);
int size = types.size();
int parameterOffset = 1;
mn.maxStack += 1;
il.add(getIntInsn(size));
il.add(new TypeInsnNode(ANEWARRAY, ClassUtil.CLASS_OBJECT));
for (int i = 0; i < size; i++) {
mn.maxStack += 3;
il.add(new InsnNode(DUP));
il.add(getIntInsn(i));
ImmutablePair<Integer, Integer> code = getLoadParameterByteCode(types.get(i));
il.add(new VarInsnNode(code.left, parameterOffset));
parameterOffset += code.right;
MethodInsnNode typeConvertMethodNode = ClassUtil.getPrimaryTypeConvertMethod(types.get(i));
if (typeConvertMethodNode != null) {
il.add(typeConvertMethodNode);
}
il.add(new InsnNode(AASTORE));
}
if (isMockForConstructor(mn)) {
il.add(new InsnNode(ICONST_1));
} else {
il.add(new InsnNode(ICONST_0));
}
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_INVOKE_RECORD_UTIL, METHOD_RECORD_MOCK_INVOKE,
SIGNATURE_INVOKE_RECORDER_METHOD, false));
mn.instructions.insertBefore(mn.instructions.get(0), il);
}
private boolean isMockForConstructor(MethodNode mn) {
for (AnnotationNode an : mn.visibleAnnotations) {
String method = AnnotationUtil.getAnnotationParameter
(an, ConstPool.FIELD_TARGET_METHOD, null, String.class);
if (ConstPool.CONSTRUCTOR.equals(method)) {
return true;
}
}
return false;
}
private static ImmutablePair<Integer, Integer> getLoadParameterByteCode(Byte type) {
switch (type) {
case ClassUtil.TYPE_BYTE:
case ClassUtil.TYPE_CHAR:
case ClassUtil.TYPE_SHORT:
case ClassUtil.TYPE_INT:
case ClassUtil.TYPE_BOOL:
return ImmutablePair.of(ILOAD, 1);
case ClassUtil.TYPE_DOUBLE:
return ImmutablePair.of(DLOAD, 2);
case ClassUtil.TYPE_FLOAT:
return ImmutablePair.of(FLOAD, 1);
case ClassUtil.TYPE_LONG:
return ImmutablePair.of(LLOAD, 2);
default:
return ImmutablePair.of(ALOAD, 1);
}
}
private AbstractInsnNode getIntInsn(int num) {
switch (num) {
case 0:
return new InsnNode(ICONST_0);
case 1:
return new InsnNode(ICONST_1);
case 2:
return new InsnNode(ICONST_2);
case 3:
return new InsnNode(ICONST_3);
case 4:
return new InsnNode(ICONST_4);
case 5:
return new InsnNode(ICONST_5);
default:
return new IntInsnNode(BIPUSH, num);
}
}
private void injectTestableRef(ClassNode cn, MethodNode mn) {

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,10 +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;
@@ -16,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;
@@ -27,27 +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 TARGET_METHOD = "targetMethod";
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,15 +59,15 @@ 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) {
// className can be null for Java 8 lambdas
return !(loader instanceof URLClassLoader) || null == className || className.startsWith("jdk/");
}
@@ -75,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>();
@@ -89,7 +94,8 @@ public class TestableClassTransformer implements ClassFileTransformer {
for (AnnotationNode an : mn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_MOCK)) {
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
String targetMethod = getAnnotationParameter(an, TARGET_METHOD, mn.name);
String targetMethod = AnnotationUtil.getAnnotationParameter(
an, ConstPool.FIELD_TARGET_METHOD, mn.name, String.class);
if (targetMethod.equals(ConstPool.CONSTRUCTOR)) {
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
methodInfos.add(new MethodInfo(sourceClassName, targetMethod, mn.name, mn.desc));
@@ -101,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
@@ -111,18 +171,4 @@ public class TestableClassTransformer implements ClassFileTransformer {
return pos < 0 ? null : ImmutablePair.of(desc.substring(1, pos + 1), "(" + desc.substring(pos + 1));
}
/**
* Read value of annotation parameter
*/
private <T> T getAnnotationParameter(AnnotationNode an, String key, T defaultValue) {
if (an.values != null) {
for (int i = 0; i < an.values.size(); i += 2) {
if (an.values.get(i).equals(key)) {
return (T)(an.values.get(i + 1));
}
}
}
return defaultValue;
}
}

View File

@@ -0,0 +1,39 @@
package com.alibaba.testable.agent.util;
import org.objectweb.asm.tree.AnnotationNode;
/**
* @author flin
*/
public class AnnotationUtil {
/**
* Read value of annotation parameter
* @param <T> template of target parameter type
* @param an annotation node
* @param key name of parameter to look for
* @param defaultValue value if parameter not exist
* @param clazz type of target parameter
* @return value of parameter
*/
public static <T> T getAnnotationParameter(AnnotationNode an, String key, T defaultValue, Class<T> clazz) {
if (an != null && an.values != null) {
for (int i = 0; i < an.values.size(); i += 2) {
if (an.values.get(i).equals(key)) {
if (clazz.isEnum()) {
// Enum type are stored as String[] in annotation parameter
String[] values = (String[])an.values.get(i + 1);
if (values == null || values.length != 2) {
return defaultValue;
}
Class<? extends Enum> enumClazz = (Class<? extends Enum>)clazz;
return (T)Enum.valueOf(enumClazz, values[1]);
}
return clazz.cast(an.values.get(i + 1));
}
}
}
return defaultValue;
}
}

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

@@ -5,76 +5,80 @@ import com.alibaba.testable.agent.tool.ComparableWeakRef;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.util.*;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
/**
* @author flin
*/
public class ClassUtil {
private static final char TYPE_BYTE = 'B';
private static final char TYPE_CHAR = 'C';
private static final char TYPE_DOUBLE = 'D';
private static final char TYPE_FLOAT = 'F';
private static final char TYPE_INT = 'I';
private static final char TYPE_LONG = 'J';
private static final char TYPE_CLASS = 'L';
private static final char TYPE_SHORT = 'S';
private static final char TYPE_BOOL = 'Z';
private static final char PARAM_END = ')';
private static final char CLASS_END = ';';
private static final char TYPE_ARRAY = '[';
public static final byte TYPE_BYTE = 'B';
public static final byte TYPE_CHAR = 'C';
public static final byte TYPE_DOUBLE = 'D';
public static final byte TYPE_FLOAT = 'F';
public static final byte TYPE_INT = 'I';
public static final byte TYPE_LONG = 'J';
public static final byte TYPE_CLASS = 'L';
public static final byte TYPE_SHORT = 'S';
public static final byte TYPE_BOOL = 'Z';
public static final byte TYPE_VOID = 'V';
private static final byte PARAM_END = ')';
private static final byte CLASS_END = ';';
private static final byte TYPE_ARRAY = '[';
private static final Map<Character, String> TYPE_MAPPING = new HashMap<Character, String>();
private static final Map<ComparableWeakRef<String>, Boolean> loadedClass =
new WeakHashMap<ComparableWeakRef<String>, Boolean>();
public static final String CLASS_OBJECT = "java/lang/Object";
private static final String CLASS_BYTE = "java/lang/Byte";
private static final String CLASS_CHARACTER = "java/lang/Character";
private static final String CLASS_DOUBLE = "java/lang/Double";
private static final String CLASS_FLOAT = "java/lang/Float";
private static final String CLASS_INTEGER = "java/lang/Integer";
private static final String CLASS_LONG = "java/lang/Long";
private static final String CLASS_SHORT = "java/lang/Short";
private static final String CLASS_BOOLEAN = "java/lang/Boolean";
private static final String EMPTY = "";
private static final String METHOD_VALUE_OF = "valueOf";
private static final Map<Byte, String> TYPE_MAPPING = new HashMap<Byte, String>();
static {
TYPE_MAPPING.put(TYPE_BYTE, "java/lang/Byte");
TYPE_MAPPING.put(TYPE_CHAR, "java/lang/Character");
TYPE_MAPPING.put(TYPE_DOUBLE, "java/lang/Double");
TYPE_MAPPING.put(TYPE_FLOAT, "java/lang/Float");
TYPE_MAPPING.put(TYPE_INT, "java/lang/Integer");
TYPE_MAPPING.put(TYPE_LONG, "java/lang/Long");
TYPE_MAPPING.put(TYPE_SHORT, "java/lang/Short");
TYPE_MAPPING.put(TYPE_BOOL, "java/lang/Boolean");
TYPE_MAPPING.put(TYPE_BYTE, CLASS_BYTE);
TYPE_MAPPING.put(TYPE_CHAR, CLASS_CHARACTER);
TYPE_MAPPING.put(TYPE_DOUBLE, CLASS_DOUBLE);
TYPE_MAPPING.put(TYPE_FLOAT, CLASS_FLOAT);
TYPE_MAPPING.put(TYPE_INT, CLASS_INTEGER);
TYPE_MAPPING.put(TYPE_LONG, CLASS_LONG);
TYPE_MAPPING.put(TYPE_SHORT, CLASS_SHORT);
TYPE_MAPPING.put(TYPE_BOOL, CLASS_BOOLEAN);
TYPE_MAPPING.put(TYPE_VOID, EMPTY);
}
/**
* Check whether any method in specified class has specified annotation
* @param className class that need to explore
* @param annotationName annotation to look for
* fit kotlin companion class name to original name
* @param name a class name (which could be a companion class)
* @return is companion class or not
*/
public static boolean anyMethodHasAnnotation(String className, String annotationName) {
Boolean found = loadedClass.get(new ComparableWeakRef<String>(className));
if (found != null) {
return found;
}
try {
ClassNode cn = new ClassNode();
new ClassReader(className).accept(cn, 0);
for (MethodNode mn : cn.methods) {
if (mn.visibleAnnotations != null) {
for (AnnotationNode an : mn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(annotationName)) {
loadedClass.put(new ComparableWeakRef<String>(className), true);
return true;
}
}
}
}
} catch (Exception e) {
// ignore
}
loadedClass.put(new ComparableWeakRef<String>(className), false);
return false;
public static boolean isCompanionClassName(String name) {
return name.endsWith("$Companion");
}
/**
* fit kotlin companion class name to original name
* @param name a class name (which could be a companion class)
* @return original name
*/
public static String fitCompanionClassName(String name) {
return name.replaceAll("\\$Companion$", "");
}
/**
* get test class name from source class name
* @param sourceClassName source class name
* @return test class name
*/
public static String getTestClassName(String sourceClassName) {
return sourceClassName + ConstPool.TEST_POSTFIX;
@@ -83,6 +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());
@@ -90,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>();
@@ -115,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);
@@ -123,15 +132,28 @@ public class ClassUtil {
return desc.substring(returnTypeEdge + 1);
} else if (typeChar == TYPE_CLASS) {
return desc.substring(returnTypeEdge + 2, desc.length() - 1);
} else if (TYPE_MAPPING.containsKey(typeChar)) {
return TYPE_MAPPING.get(typeChar);
} else if (TYPE_MAPPING.containsKey((byte)typeChar)) {
return TYPE_MAPPING.get((byte)typeChar);
} else {
return "";
return EMPTY;
}
}
/**
* Get method node to convert primary type to object type
* @param type primary type to convert
* @return converter method node
*/
public static MethodInsnNode getPrimaryTypeConvertMethod(Byte type) {
String objectType = TYPE_MAPPING.get(type);
return (objectType == null) ? null :
new MethodInsnNode(INVOKESTATIC, objectType, METHOD_VALUE_OF, toDescriptor(type, objectType), false);
}
/**
* convert slash separated name to dot separated name
* @param name original name
* @return converted name
*/
public static String toDotSeparatedName(String name) {
return name.replace(ConstPool.SLASH, ConstPool.DOT);
@@ -139,6 +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);
@@ -146,13 +170,17 @@ public class ClassUtil {
/**
* convert dot separated name to byte code class name
* @param className original name
* @return converted name
*/
public static String toByteCodeClassName(String className) {
return TYPE_CLASS + toSlashSeparatedName(className) + CLASS_END;
return (char)TYPE_CLASS + toSlashSeparatedName(className) + (char)CLASS_END;
}
/**
* convert byte code class name to dot separated human readable name
* @param className original name
* @return converted name
*/
public static String toDotSeparateFullClassName(String className) {
return toDotSeparatedName(className).substring(1, className.length() - 1);
@@ -160,14 +188,19 @@ public class ClassUtil {
/**
* convert byte code class name to slash separated human readable name
* @param className original name
* @return converted name
*/
public static String toSlashSeparateFullClassName(String className) {
return toSlashSeparatedName(className).substring(1, className.length() - 1);
}
private static String toDescriptor(Byte type, String objectType) {
return "(" + (char)type.byteValue() + ")L" + objectType + ";";
}
private static boolean isPrimaryType(byte b) {
return b == TYPE_BYTE || b == TYPE_CHAR || b == TYPE_DOUBLE || b == TYPE_FLOAT
|| b == TYPE_INT || b == TYPE_LONG || b == TYPE_SHORT || b == TYPE_BOOL;
}
}

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

@@ -1,23 +0,0 @@
package com.alibaba.testable.agent.transformer;
import com.alibaba.testable.core.accessor.PrivateAccessor;
import org.junit.jupiter.api.Test;
import org.objectweb.asm.tree.AnnotationNode;
import static com.alibaba.testable.agent.util.CollectionUtil.listOf;
import static org.junit.jupiter.api.Assertions.*;
class TestableClassTransformerTest {
private TestableClassTransformer transformer = new TestableClassTransformer();
@Test
void should_get_annotation_parameter() {
AnnotationNode an = new AnnotationNode("");
an.values = listOf((Object)"testKey", "testValue", "demoKey", "demoValue");
assertEquals("testValue", PrivateAccessor.invoke(transformer, "getAnnotationParameter", an, "testKey", "none"));
assertEquals("demoValue", PrivateAccessor.invoke(transformer, "getAnnotationParameter", an, "demoKey", "none"));
assertEquals("none", PrivateAccessor.invoke(transformer, "getAnnotationParameter", an, "testValue", "none"));
}
}

View File

@@ -0,0 +1,20 @@
package com.alibaba.testable.agent.util;
import org.junit.jupiter.api.Test;
import org.objectweb.asm.tree.AnnotationNode;
import static com.alibaba.testable.agent.util.CollectionUtil.listOf;
import static org.junit.jupiter.api.Assertions.*;
class AnnotationUtilTest {
@Test
void should_get_annotation_parameter() {
AnnotationNode an = new AnnotationNode("");
an.values = listOf((Object)"testKey", "testValue", "demoKey", "demoValue");
assertEquals("testValue", AnnotationUtil.getAnnotationParameter(an, "testKey", "none", String.class));
assertEquals("demoValue", AnnotationUtil.getAnnotationParameter(an, "demoKey", "none", String.class));
assertEquals("none", AnnotationUtil.getAnnotationParameter(an, "testValue", "none", String.class));
}
}

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() {
assertEquals(false, ClassUtil.anyMethodHasAnnotation("class.not.exist", ""));
assertEquals(false, ClassUtil.anyMethodHasAnnotation("org.junit.jupiter.api.Assertions", "annotation.not.exist"));
assertEquals(true, ClassUtil.anyMethodHasAnnotation("org.junit.jupiter.api.Assertions", "org.apiguardian.api.API"));
}
@Test
void should_able_to_get_parameter_count() {
assertEquals(0, ClassUtil.getParameterTypes("()V").size());
@@ -40,5 +29,17 @@ class ClassUtilTest {
assertEquals("Ljava/lang/String;", ClassUtil.toByteCodeClassName("java.lang.String"));
}
@Test
void should_able_to_fit_companion_class_name() {
assertEquals("com/intellij/rt/debugger/agent/CaptureAgent$ParamKeyProvider",
ClassUtil.fitCompanionClassName("com/intellij/rt/debugger/agent/CaptureAgent$ParamKeyProvider"));
assertEquals("com/alibaba/testable/demo/BlackBox",
ClassUtil.fitCompanionClassName("com/alibaba/testable/demo/BlackBox"));
assertEquals("com/alibaba/testable/demo/BlackBox$Companion",
ClassUtil.fitCompanionClassName("com/alibaba/testable/demo/BlackBox$Companion$Companion"));
assertEquals("com/alibaba/testable/demo/BlackBox",
ClassUtil.fitCompanionClassName("com/alibaba/testable/demo/BlackBox$Companion"));
}
}

View File

@@ -3,32 +3,50 @@
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.0-SNAPSHOT</version>
<version>0.3.1</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>
<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>sun.jdk</groupId>
<artifactId>tools</artifactId>
<version>${java.version}</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.2</version>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
@@ -43,10 +61,84 @@
<source>${project.compiler.level}</source>
<target>${project.compiler.level}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${plugin.surefire.version}</version>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${plugin.source.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${plugin.javadoc.version}</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>${plugin.gpg.version}</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>${plugin.staging.version}</version>
<extensions>true</extensions>
<configuration>
<serverId>oss</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<snapshotRepository>
<id>oss</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
</snapshotRepository>
<repository>
<id>oss</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
</profile>
</profiles>
</project>

View File

@@ -46,4 +46,18 @@ public class PrivateAccessor {
return null;
}
public static <T> T invokeStatic(Class<?> clazz, String method, Object... args) {
try {
Class<?>[] cls = TypeUtil.getClassesFromObjects(args);
Method declaredMethod = TypeUtil.getMethodByNameAndParameterTypes(clazz.getDeclaredMethods(), method, cls);
if (declaredMethod != null) {
declaredMethod.setAccessible(true);
return (T)declaredMethod.invoke(null, args);
}
} catch (Exception e) {
e.printStackTrace();
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

@@ -14,6 +14,7 @@ public @interface TestableMock {
/**
* mock specified method instead of method with same name
* @return target method name
*/
String targetMethod() default "";

View File

@@ -5,7 +5,6 @@ package com.alibaba.testable.core.constant;
*/
public final class ConstPool {
public static final String TESTABLE_PRIVATE_ACCESSOR = "com.alibaba.testable.core.accessor.PrivateAccessor";
public static final String TEST_POSTFIX = "Test";
}

View File

@@ -5,12 +5,24 @@ package com.alibaba.testable.core.error;
*/
public class VerifyFailedError extends AssertionError {
public VerifyFailedError(int actualCount, int expectedCount) {
super(getErrorMessage(actualCount, expectedCount));
public VerifyFailedError(String message) {
super(getErrorMessage(message));
}
private static String getErrorMessage(int actualCount, int expectedCount) {
return "\nExpected times : " + expectedCount + "\nActual times : " + actualCount;
public VerifyFailedError(String expected, String actual) {
super(getErrorMessage(expected, actual));
}
public VerifyFailedError(String message, String expected, String actual) {
super(getErrorMessage(message) + getErrorMessage(expected, actual));
}
private static String getErrorMessage(String message) {
return "\n" + message.substring(0, 1).toUpperCase() + message.substring(1);
}
private static String getErrorMessage(String expected, String actual) {
return "\nExpected " + expected + "\n Actual " + actual;
}
}

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

@@ -0,0 +1,166 @@
package com.alibaba.testable.core.matcher;
import com.alibaba.testable.core.error.VerifyFailedError;
import com.alibaba.testable.core.model.Verification;
import com.alibaba.testable.core.util.InvokeRecordUtil;
import com.alibaba.testable.core.util.TestableUtil;
import java.security.InvalidParameterException;
import java.util.List;
/**
* @author flin
*/
public class InvokeVerifier {
private final List<Object[]> records;
private Verification lastVerification = null;
private InvokeVerifier(List<Object[]> records) {
this.records = records;
}
/**
* Get counter to check whether specified mock method invoked
* @param mockMethodName name of a mock method
* @return the verifier object
*/
public static InvokeVerifier verify(String mockMethodName) {
String testClass = Thread.currentThread().getStackTrace()[InvokeRecordUtil.INDEX_OF_TEST_CLASS].getClassName();
String testCaseName = TestableUtil.currentTestCaseName(testClass);
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) {
boolean found = false;
for (int i = 0; i < records.size(); i++) {
try {
withInternal(args, i);
found = true;
break;
} catch (AssertionError e) {
// continue
}
}
if (!found) {
throw new VerifyFailedError("has not invoke with " + desc(args));
}
lastVerification = new Verification(args, false);
return this;
}
/**
* Expect next mock method call was invoked with specified parameters
* @param args parameters to compare
* @return the verifier object
*/
public InvokeVerifier withInOrder(Object... args) {
withInternal(args, 0);
lastVerification = new Verification(args, true);
return this;
}
/**
* Expect mock method had never invoked with specified parameters
* @param args parameters to compare
* @return the verifier object
*/
public InvokeVerifier without(Object... args) {
for (Object[] r : records) {
if (r.length == args.length) {
for (int i = 0; i < r.length; i++) {
if (!matches(args[i], r[i])) {
break;
}
if (i == r.length - 1) {
// all arguments are equal
throw new VerifyFailedError("was invoked with " + desc(args));
}
}
}
}
return this;
}
/**
* Expect mock method have been invoked specified times
* @param expectedCount times to compare
* @return the verifier object
*/
public InvokeVerifier withTimes(int expectedCount) {
if (expectedCount != records.size()) {
throw new VerifyFailedError("times: " + expectedCount, "times: " + records.size());
}
lastVerification = null;
return this;
}
/**
* Expect several consecutive invocations with the same parameters
* @param count number of invocations
* @return the verifier object
*/
public InvokeVerifier times(int count) {
if (lastVerification == null) {
// when used independently, equals to `withTimes()`
System.out.println("Warning: [" + TestableUtil.getPreviousStackLocation() + "] using \"times()\" method "
+ "without \"with()\" or \"withInOrder()\" is not recommended, please use \"withTimes()\" instead.");
return withTimes(count);
}
if (count < 2) {
throw new InvalidParameterException("should only use times() method with count equal or larger than 2.");
}
for (int i = 0; i < count - 1; i++) {
if (lastVerification.inOrder) {
withInOrder(lastVerification.parameters);
} else {
with(lastVerification.parameters);
}
}
lastVerification = null;
return this;
}
private void withInternal(Object[] args, int order) {
if (records.isEmpty()) {
throw new VerifyFailedError("has not more invoke");
}
Object[] record = records.get(order);
if (record.length != args.length) {
throw new VerifyFailedError(desc(args), desc(record));
}
for (int i = 0; i < args.length; i++) {
if (!(args[i] instanceof InvokeMatcher || args[i].getClass().equals(record[i].getClass()))) {
throw new VerifyFailedError("parameter " + (i + 1) + " type mismatch",
": " + args[i].getClass(), ": " + record[i].getClass());
}
if (!matches(args[i], record[i])) {
throw new VerifyFailedError("parameter " + (i + 1) + " mismatched", desc(args), desc(record));
}
}
records.remove(order);
}
private boolean matches(Object expectValue, Object realValue) {
return expectValue instanceof InvokeMatcher ?
((InvokeMatcher) expectValue).matchFunction.check(realValue) :
expectValue.equals(realValue);
}
private String desc(Object[] args) {
StringBuilder sb = new StringBuilder(": ");
for (int i = 0; i < args.length; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(args[i]);
}
return sb.toString();
}
}

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

@@ -0,0 +1,16 @@
package com.alibaba.testable.core.model;
/**
* @author flin
*/
public class Verification {
public Object[] parameters;
public boolean inOrder;
public Verification(Object[] parameters, boolean inOrder) {
this.parameters = parameters;
this.inOrder = inOrder;
}
}

View File

@@ -1,22 +0,0 @@
package com.alibaba.testable.core.tool;
import com.alibaba.testable.core.error.VerifyFailedError;
/**
* @author flin
*/
public class InvokeCounter {
private final int actualCount;
public InvokeCounter(int actualCount) {
this.actualCount = actualCount;
}
public void times(int expectedCount) {
if (expectedCount != actualCount) {
throw new VerifyFailedError(actualCount, expectedCount);
}
}
}

View File

@@ -1,7 +1,5 @@
package com.alibaba.testable.core.tool;
import com.alibaba.testable.core.util.TestableUtil;
/**
* @author flin
*/
@@ -22,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 InvokeCounter verify(String mockMethodName) {
String testClass = Thread.currentThread().getStackTrace()[TestableUtil.INDEX_OF_TEST_CLASS].getClassName();
String testCaseName = TestableUtil.currentTestCaseName(testClass);
return new InvokeCounter(TestableUtil.getInvokeCount(mockMethodName, testCaseName));
}
}

View File

@@ -0,0 +1,66 @@
package com.alibaba.testable.core.util;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author flin
*/
public class InvokeRecordUtil {
/**
* Mock method name → List of invoke parameters
*/
private static final Map<String, List<Object[]>> INVOKE_RECORDS = new HashMap<String, List<Object[]>>();
private final static String JOINER = "::";
/**
* [0]Thread → [1]TestableUtil/TestableTool → [2]TestClass
*/
public static final int INDEX_OF_TEST_CLASS = 2;
/**
* Record mock method invoke event
* @param args invocation parameters
* @param isConstructor whether mocked method is constructor
*/
public static void recordMockInvoke(Object[] args, boolean isConstructor) {
StackTraceElement mockMethodTraceElement = Thread.currentThread().getStackTrace()[INDEX_OF_TEST_CLASS];
String mockMethodName = mockMethodTraceElement.getMethodName();
String testClass = mockMethodTraceElement.getClassName();
String testCaseName = TestableUtil.currentTestCaseName(testClass);
String key = testCaseName + JOINER + mockMethodName;
List<Object[]> records = getInvokeRecord(mockMethodName, testCaseName);
if (isConstructor) {
records.add(args);
} else {
records.add(slice(args, 1));
}
INVOKE_RECORDS.put(key, records);
}
/**
* 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;
List<Object[]> records = INVOKE_RECORDS.get(key);
return (records == null) ? new LinkedList<Object[]>() : records;
}
private static Object[] slice(Object[] args, int firstIndex) {
int size = args.length - firstIndex;
if (size <= 0) {
return new Object[0];
}
Object[] slicedArgs = new Object[size];
System.arraycopy(args, firstIndex, slicedArgs, 0, size);
return slicedArgs;
}
}

View File

@@ -2,34 +2,12 @@ package com.alibaba.testable.core.util;
import com.alibaba.testable.core.constant.ConstPool;
import java.util.HashMap;
import java.util.Map;
/**
* @author flin
*/
public class TestableUtil {
private static final Map<String, Integer> INVOKE_RECORDS = new HashMap<String, Integer>();
private final static String JOINER = "->";
/**
* [0]Thread -> [1]TestableUtil/TestableTool -> [2]TestClass
*/
public static final int INDEX_OF_TEST_CLASS = 2;
/**
* Record mock method invoke event
*/
public static void countMockInvoke() {
StackTraceElement mockMethodTraceElement = Thread.currentThread().getStackTrace()[INDEX_OF_TEST_CLASS];
String mockMethodName = mockMethodTraceElement.getMethodName();
String testClass = mockMethodTraceElement.getClassName();
String testCaseName = TestableUtil.currentTestCaseName(testClass);
String key = testCaseName + JOINER + mockMethodName;
int count = getInvokeCount(mockMethodName, testCaseName);
INVOKE_RECORDS.put(key, count + 1);
}
/**
* Get the last visit method in source file
* @param testClassRef usually `this` variable of the test class
@@ -72,13 +50,14 @@ public class TestableUtil {
return "";
}
public static int getInvokeCount(String mockMethodName, String testCaseName) {
String key = testCaseName + JOINER + mockMethodName;
Integer count = INVOKE_RECORDS.get(key);
if (count == null) {
count = 0;
}
return count;
/**
* Get file name and line number of where current method was called
* @return in "filename:linenumber" format
*/
public static String getPreviousStackLocation() {
// 0 - Thread.getStackTrace(), 1 - this method, 2 - code call this method, 3 - code call the caller method
StackTraceElement stack = getMainThread().getStackTrace()[3];
return stack.getFileName() + ":" + stack.getLineNumber();
}
private static String findLastMethodFromSourceClass(String sourceClassName, StackTraceElement[] stack) {

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,8 +40,11 @@ public class TypeUtil {
/**
* type equals
* @param classesLeft class to be compared
* @param classesRight class to compare
* @return whether all class equals
*/
public static boolean typeEquals(Class<?>[] classesLeft, Class<?>[] classesRight) {
private static boolean typeEquals(Class<?>[] classesLeft, Class<?>[] classesRight) {
if (classesLeft.length != classesRight.length) {
return false;
}
@@ -53,6 +61,7 @@ public class TypeUtil {
* fuzzy equal
* @param factTypes fact types (can be primary type)
* @param userTypes user types
* @return whether all class equals
*/
private static boolean fuzzyEqual(Class<?> factTypes, Class<?> userTypes) {
return (factTypes.equals(int.class) && userTypes.equals(Integer.class)) ||

View File

@@ -1 +0,0 @@
com.alibaba.testable.core.processor.EnablePrivateAccessProcessor

View File

@@ -0,0 +1,18 @@
package com.alibaba.testable.core.util;
import com.alibaba.testable.core.accessor.PrivateAccessor;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class InvokeRecordUtilTest {
@Test
void should_slice_array() {
Object[] args = new Object[]{"1", "2", "3"};
Object[] slicedArgs = PrivateAccessor.invokeStatic(InvokeRecordUtil.class, "slice", args, 1);
assertEquals(2, slicedArgs.length);
assertEquals("2", slicedArgs[0]);
assertEquals("3", slicedArgs[1]);
}
}

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.0-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.1</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.0-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.1</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>

169
testable-processor/pom.xml Normal file
View File

@@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-processor</artifactId>
<version>0.3.1</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.1</testable.version>
<plugin.compiler.version>3.8.1</plugin.compiler.version>
<plugin.surefire.version>3.0.0-M5</plugin.surefire.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>com.alibaba.testable</groupId>
<artifactId>testable-core</artifactId>
<version>${testable.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</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>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${plugin.surefire.version}</version>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>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

@@ -1,10 +1,10 @@
package com.alibaba.testable.core.processor;
package com.alibaba.testable.processor;
import com.alibaba.testable.core.annotation.EnablePrivateAccess;
import com.alibaba.testable.core.constant.ConstPool;
import com.alibaba.testable.core.model.TestableContext;
import com.alibaba.testable.core.translator.EnablePrivateAccessTranslator;
import com.alibaba.testable.core.util.TestableLogger;
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
import com.alibaba.testable.processor.constant.ConstPool;
import com.alibaba.testable.processor.model.TestableContext;
import com.alibaba.testable.processor.translator.EnablePrivateAccessTranslator;
import com.alibaba.testable.processor.util.TestableLogger;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
@@ -26,7 +26,7 @@ import java.util.Set;
/**
* @author flin
*/
@SupportedAnnotationTypes("com.alibaba.testable.core.annotation.EnablePrivateAccess")
@SupportedAnnotationTypes("com.alibaba.testable.processor.annotation.EnablePrivateAccess")
public class EnablePrivateAccessProcessor extends AbstractProcessor {
private TestableContext cx;

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.core.annotation;
package com.alibaba.testable.processor.annotation;
import java.lang.annotation.*;

View File

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

View File

@@ -1,6 +1,6 @@
package com.alibaba.testable.core.generator;
package com.alibaba.testable.processor.generator;
import com.alibaba.testable.core.model.TestableContext;
import com.alibaba.testable.processor.model.TestableContext;
import com.sun.tools.javac.tree.JCTree.*;
/**

View File

@@ -1,7 +1,7 @@
package com.alibaba.testable.core.generator;
package com.alibaba.testable.processor.generator;
import com.alibaba.testable.core.model.TestableContext;
import com.alibaba.testable.core.constant.ConstPool;
import com.alibaba.testable.processor.model.TestableContext;
import com.alibaba.testable.processor.constant.ConstPool;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
@@ -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

@@ -1,6 +1,6 @@
package com.alibaba.testable.core.model;
package com.alibaba.testable.processor.model;
import com.alibaba.testable.core.util.TestableLogger;
import com.alibaba.testable.processor.util.TestableLogger;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Names;

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.core.translator;
package com.alibaba.testable.processor.translator;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.tree.TreeTranslator;

View File

@@ -1,8 +1,8 @@
package com.alibaba.testable.core.translator;
package com.alibaba.testable.processor.translator;
import com.alibaba.testable.core.constant.ConstPool;
import com.alibaba.testable.core.generator.PrivateAccessStatementGenerator;
import com.alibaba.testable.core.model.TestableContext;
import com.alibaba.testable.processor.constant.ConstPool;
import com.alibaba.testable.processor.generator.PrivateAccessStatementGenerator;
import com.alibaba.testable.processor.model.TestableContext;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Name;
@@ -18,10 +18,23 @@ 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) {
@@ -56,25 +69,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 +114,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 +127,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

@@ -1,4 +1,4 @@
package com.alibaba.testable.core.util;
package com.alibaba.testable.processor.util;
import java.util.List;
@@ -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)
{

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.core.util;
package com.alibaba.testable.processor.util;
import javax.annotation.processing.Messager;
import javax.tools.Diagnostic;

View File

@@ -0,0 +1 @@
com.alibaba.testable.processor.EnablePrivateAccessProcessor

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