mirror of
https://github.com/alibaba/testable-mock.git
synced 2026-08-30 03:08:21 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6ac715f57 | ||
|
|
6b126223e8 | ||
|
|
508610b0c8 | ||
|
|
459fea37c3 | ||
|
|
703aa319ba | ||
|
|
df7deedcce | ||
|
|
4701b3b42b | ||
|
|
085c6784d4 | ||
|
|
a1437e7291 | ||
|
|
d17bdb258b | ||
|
|
03253e529d | ||
|
|
6b4467a03b | ||
|
|
8cfaa6d36e | ||
|
|
78a19b6876 | ||
|
|
5484b216f1 | ||
|
|
d1007f6166 | ||
|
|
9b82fcdc24 | ||
|
|
bf4c263d93 | ||
|
|
28bde0b85a | ||
|
|
179e71c7c2 | ||
|
|
f624c9131e | ||
|
|
83974cfb2a | ||
|
|
abb6d04c16 | ||
|
|
16ec3b9444 | ||
|
|
180dfe9da8 | ||
|
|
92a2c69e05 | ||
|
|
f40834eb94 | ||
|
|
2dea6f9480 | ||
|
|
2bdad3c1f6 | ||
|
|
3f672e45da | ||
|
|
328c8540a8 |
@@ -2,9 +2,12 @@
|
||||
|
||||
换种思路写Mock,让单元测试更简单。
|
||||
|
||||
无需初始化,不挑测试框架,甭管要换的方法是被测类的私有方法、静态方法还是其他任何类的成员方法,也甭管要换的对象是怎么创建的。写好Mock方法,加个`@TestableMock`注解,一切统统搞定。
|
||||
无需初始化,不挑测试框架,甭管要换的是私有方法、静态方法、构造方法还是其他任何类的任何方法,也甭管要换的对象是怎么创建的。写好Mock定义,加个`@MockMethod`注解,一切统统搞定。
|
||||
|
||||
文档:https://alibaba.github.io/testable-mock/
|
||||
- 文档:https://alibaba.github.io/testable-mock/
|
||||
- 国内文档镜像:http://freyrlin.gitee.io/testable-mock/ (速度快,内容稍有延迟)
|
||||
|
||||
阅读[这里](https://mp.weixin.qq.com/s/KyU6Eu7mDkZU8FspfSqfMw)了解更多故事。
|
||||
|
||||
## 目录结构
|
||||
|
||||
@@ -35,5 +38,5 @@ mvn clean install
|
||||
docsify serve docs
|
||||
```
|
||||
|
||||
> Testable文档使用`docsify`工具生成,构建前请安装[nodejs](https://nodejs.org/en/download/)运行时,并使用`npm install -g docsify`命令安装文档生成工具。
|
||||
> TestableMock文档使用`docsify`工具生成,构建前请安装[nodejs](https://nodejs.org/en/download/)运行时,并使用`npm install -g docsify`命令安装文档生成工具。
|
||||
|
||||
|
||||
39
README_EN.md
Normal file
39
README_EN.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# TestableMock
|
||||
|
||||
Write mock faster, make unit testing easier.
|
||||
|
||||
Any test framework, no initialization, no matter private method, static method, construction method, or any other method of any class, and no matter how the object created.
|
||||
Write a mock method, add an `@MockMethod` annotation, everything is done.
|
||||
|
||||
Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```bash
|
||||
|-- testable-parent ➜ Provide parent pom file shared by other modules
|
||||
|-- testable-all ➜ Dependence aggration, for easily import all modules at once
|
||||
|-- testable-processor ➜ Compile-time code preprocessing module, provides test assist functions
|
||||
|-- testable-agent ➜ JavaAgent module, provides test mocking related functions
|
||||
|-- testable-core ➜ Basic function module, provides Mock related class and annotation
|
||||
|-- testable-maven-plugin ➜ Maven plugin module, for simplify JavaAgent injection
|
||||
|-- demo
|
||||
| |-- java-demo ➜ Java code example
|
||||
| `-- kotlin-demo ➜ Kotlin code example
|
||||
`-- docs ➜ Source code of usage document
|
||||
```
|
||||
|
||||
## Build project
|
||||
|
||||
The project is built using JDK 1.6+ and Maven 3+, except for the `demo` sub-project requires JDK 1.8+.
|
||||
|
||||
```bash
|
||||
mvn clean install
|
||||
```
|
||||
|
||||
## Generate document
|
||||
|
||||
```bash
|
||||
docsify serve docs
|
||||
```
|
||||
|
||||
> `TestableMock` document is generated by `docsify` tool, please install [nodejs](https://nodejs.org/en/download/) before execution, and run `npm install -g docsify` command to install the tool required.
|
||||
@@ -12,8 +12,8 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.0')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.4')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.4')
|
||||
}
|
||||
|
||||
test {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.4.0</testable.version>
|
||||
<testable.version>0.4.4</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -20,7 +20,7 @@
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
@@ -32,14 +32,11 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<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>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<configuration>
|
||||
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
</configuration>
|
||||
|
||||
@@ -5,7 +5,8 @@ import com.alibaba.testable.demo.model.Box;
|
||||
import com.alibaba.testable.demo.model.Color;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
* Demonstrate scenario of mocking method from sub-type object referred by parent-type variable
|
||||
*/
|
||||
public class DemoInherit {
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
* 演示Mock方法调用校验器
|
||||
* Demonstrate mock method invocation verifier
|
||||
*/
|
||||
public class DemoMatcher {
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 演示基本的Mock功能
|
||||
* Demonstrate basic mock functionality
|
||||
*/
|
||||
public class DemoMock {
|
||||
|
||||
/**
|
||||
@@ -20,7 +24,7 @@ public class DemoMock {
|
||||
* method with member method invoke
|
||||
*/
|
||||
public String outerFunc(String s) throws Exception {
|
||||
return "{ \"res\": \"" + innerFunc(s) + "\"}";
|
||||
return "{ \"res\": \"" + innerFunc(s) + staticFunc() + "\"}";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +52,10 @@ public class DemoMock {
|
||||
return callFromDifferentMethod();
|
||||
}
|
||||
|
||||
private static String staticFunc() {
|
||||
return "_STATIC_TAIL";
|
||||
}
|
||||
|
||||
private String innerFunc(String s) throws Exception {
|
||||
return Files.readAllLines(Paths.get("/a-not-exist-file")).stream().collect(Collectors.joining());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
* 演示私有成员访问功能
|
||||
* Demonstrate private member access functionality
|
||||
*/
|
||||
public class DemoPrivateAccess {
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@ package com.alibaba.testable.demo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 演示模板方法的Mock场景
|
||||
* Demonstrate scenario of mocking template method
|
||||
*/
|
||||
public class DemoTemplate {
|
||||
|
||||
private <T> List<T> getList(T value) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import com.alibaba.testable.demo.model.Box;
|
||||
import com.alibaba.testable.demo.model.Color;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示父类变量引用子类对象时的Mock场景
|
||||
@@ -17,32 +17,32 @@ class DemoInheritTest {
|
||||
|
||||
private DemoInherit demoInherit = new DemoInherit();
|
||||
|
||||
@TestableMock(targetMethod = "put")
|
||||
@MockMethod(targetMethod = "put")
|
||||
private void put_into_box(Box self, String something) {
|
||||
self.put("put_" + something + "_into_box");
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "put")
|
||||
@MockMethod(targetMethod = "put")
|
||||
private void put_into_blackbox(BlackBox self, String something) {
|
||||
self.put("put_" + something + "_into_blackbox");
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "get")
|
||||
@MockMethod(targetMethod = "get")
|
||||
private String get_from_box(Box self) {
|
||||
return "get_from_box";
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "get")
|
||||
@MockMethod(targetMethod = "get")
|
||||
private String get_from_blackbox(BlackBox self) {
|
||||
return "get_from_blackbox";
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "getColor")
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private String get_color_from_color(Color self) {
|
||||
return "color_from_color";
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "getColor")
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private String get_color_from_blackbox(BlackBox self) {
|
||||
return "color_from_blackbox";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.core.error.VerifyFailedError;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -17,15 +17,16 @@ class DemoMatcherTest {
|
||||
|
||||
private DemoMatcher demoMatcher = new DemoMatcher();
|
||||
|
||||
@TestableMock(targetMethod = "methodToBeMocked")
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithoutArgument(DemoMatcher self) {}
|
||||
|
||||
@TestableMock(targetMethod = "methodToBeMocked")
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArguments(DemoMatcher self, Object a1, Object a2) {}
|
||||
|
||||
@TestableMock(targetMethod = "methodToBeMocked")
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private void methodWithArrayArgument(DemoMatcher self, Object[] a) {}
|
||||
|
||||
|
||||
@Test
|
||||
void should_match_no_argument() {
|
||||
demoMatcher.callMethodWithoutArgument();
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock;
|
||||
import com.alibaba.testable.core.tool.TestableConst;
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import com.alibaba.testable.demo.model.BlackBox;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.*;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.TEST_CASE;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
@@ -19,37 +20,42 @@ class DemoMockTest {
|
||||
|
||||
private DemoMock demoMock = new DemoMock();
|
||||
|
||||
@TestableMock(targetMethod = TestableConst.CONSTRUCTOR)
|
||||
@MockConstructor
|
||||
private BlackBox createBlackBox(String text) {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private String innerFunc(DemoMock self, String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private String staticFunc(DemoMock self) {
|
||||
return "_MOCK_TAIL";
|
||||
}
|
||||
|
||||
@MockMethod
|
||||
private String trim(String self) {
|
||||
return "trim_string";
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "substring")
|
||||
@MockMethod(targetMethod = "substring")
|
||||
private String sub(String self, int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private boolean startsWith(String self, String s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private BlackBox secretBox(BlackBox ignore) {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private String callFromDifferentMethod(DemoMock self) {
|
||||
if (TEST_CASE.equals("should_able_to_get_test_case_name")) {
|
||||
return "mock_special";
|
||||
@@ -69,7 +75,7 @@ class DemoMockTest {
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
assertEquals("{ \"res\": \"mock_hello\"}", demoMock.outerFunc("hello"));
|
||||
assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoMock.outerFunc("hello"));
|
||||
verify("innerFunc").with("hello");
|
||||
}
|
||||
|
||||
|
||||
@@ -32,14 +32,14 @@ class DemoPrivateAccessTest {
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_static_method() {
|
||||
//assertEquals("hello + 1", DemoPrivateAccess.privateStaticFunc("hello", 1));
|
||||
assertEquals("hello + 1", DemoPrivateAccess.privateStaticFunc("hello", 1));
|
||||
assertEquals("hello + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFunc", "hello", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_access_private_static_field() {
|
||||
//DemoPrivateAccess.staticCount = 2;
|
||||
//assertEquals(new Integer(2), DemoPrivateAccess.staticCount);
|
||||
DemoPrivateAccess.staticCount = 2;
|
||||
assertEquals(new Integer(2), DemoPrivateAccess.staticCount);
|
||||
|
||||
PrivateAccessor.setStatic(DemoPrivateAccess.class, "staticCount", 3);
|
||||
assertEquals(new Integer(3), PrivateAccessor.getStatic(DemoPrivateAccess.class, "staticCount"));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.alibaba.testable.demo;
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock;
|
||||
import com.alibaba.testable.core.tool.TestableConst;
|
||||
import com.alibaba.testable.core.annotation.MockConstructor;
|
||||
import com.alibaba.testable.core.annotation.MockMethod;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
@@ -9,8 +9,8 @@ import java.util.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 演示模板方法可以被Mock
|
||||
* Demonstrate template method can be mocked
|
||||
* 演示模板方法的Mock场景
|
||||
* Demonstrate scenario of mocking template method
|
||||
*/
|
||||
class DemoTemplateTest {
|
||||
|
||||
@@ -19,24 +19,24 @@ class DemoTemplateTest {
|
||||
/* 第一种写法:使用泛型定义 */
|
||||
/* First solution: use generics type */
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private <T> List<T> getList(DemoTemplate self, T value) {
|
||||
return new ArrayList<T>() {{ add((T)(value.toString() + "_mock_list")); }};
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private <K, V> Map<K, V> getMap(DemoTemplate self, K key, V value) {
|
||||
return new HashMap<K, V>() {{ put(key, (V)(value.toString() + "_mock_map")); }};
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = TestableConst.CONSTRUCTOR)
|
||||
@MockConstructor
|
||||
private <T> HashSet<T> newHashSet() {
|
||||
HashSet<T> set = new HashSet<>();
|
||||
set.add((T)"insert_mock");
|
||||
return set;
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private <E> boolean add(Set s, E e) {
|
||||
s.add(e.toString() + "_mocked");
|
||||
return true;
|
||||
@@ -45,24 +45,24 @@ class DemoTemplateTest {
|
||||
/* 第二种写法:使用Object类型 */
|
||||
/* Second solution: use object type */
|
||||
|
||||
//@TestableMock
|
||||
//@MockMethod
|
||||
//private List<Object> getList(DemoTemplate self, Object value) {
|
||||
// return new ArrayList<Object>() {{ add(value.toString() + "_mock_list"); }};
|
||||
//}
|
||||
//
|
||||
//@TestableMock
|
||||
//@MockMethod
|
||||
//private Map<Object, Object> getMap(DemoTemplate self, Object key, Object value) {
|
||||
// return new HashMap<Object, Object>() {{ put(key, value.toString() + "_mock_map"); }};
|
||||
//}
|
||||
//
|
||||
//@TestableMock(targetMethod = TestableConst.CONSTRUCTOR)
|
||||
//@MockConstructor
|
||||
//private HashSet newHashSet() {
|
||||
// HashSet<Object> set = new HashSet<>();
|
||||
// set.add("insert_mock");
|
||||
// return set;
|
||||
//}
|
||||
//
|
||||
//@TestableMock
|
||||
//@MockMethod
|
||||
//private boolean add(Set s, Object e) {
|
||||
// s.add(e.toString() + "_mocked");
|
||||
// return true;
|
||||
|
||||
@@ -16,8 +16,8 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
|
||||
testImplementation("com.alibaba.testable:testable-all:0.4.0")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.0")
|
||||
testImplementation("com.alibaba.testable:testable-all:0.4.4")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.4")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>5.6.2</junit.version>
|
||||
<testable.version>0.4.0</testable.version>
|
||||
<testable.version>0.4.4</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -33,7 +33,7 @@
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
@@ -51,14 +51,10 @@
|
||||
<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>-->
|
||||
<!-- <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>
|
||||
<plugin>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
|
||||
@@ -41,8 +41,24 @@ class DemoMatcher {
|
||||
val longArray = arrayOf(1L, 2L)
|
||||
methodToBeMocked(1, 2)
|
||||
methodToBeMocked(1L, 2.0)
|
||||
|
||||
// below two invocations are equivalent
|
||||
methodToBeMocked(listOf(1), setOf(1.0f))
|
||||
// multiple lines method invocation
|
||||
methodToBeMocked(object : ArrayList<Int?>() {
|
||||
init {
|
||||
add(1)
|
||||
}
|
||||
}, object : HashSet<Float?>() {
|
||||
init {
|
||||
add(1.0f)
|
||||
}
|
||||
})
|
||||
|
||||
// below two invocations are equivalent
|
||||
methodToBeMocked(1.0, mapOf(1 to 1.0f))
|
||||
methodToBeMocked(1.0, object : HashMap<Int?, Float?>(2) { init { put(1, 1.0f) } })
|
||||
|
||||
methodToBeMocked(floatList, floatList)
|
||||
methodToBeMocked(longArray)
|
||||
methodToBeMocked(arrayOf(1.0, 2.0))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.Box
|
||||
@@ -14,37 +14,38 @@ import org.junit.jupiter.api.Test
|
||||
*/
|
||||
internal class DemoInheritTest {
|
||||
|
||||
@TestableMock(targetMethod = "put")
|
||||
private val demoInherit = DemoInherit()
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_box(self: Box, something: String) {
|
||||
self.put("put_" + something + "_into_box")
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "put")
|
||||
@MockMethod(targetMethod = "put")
|
||||
private fun put_into_blackbox(self: BlackBox, something: String) {
|
||||
self.put("put_" + something + "_into_blackbox")
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "get")
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_box(self: Box): String {
|
||||
return "get_from_box"
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "get")
|
||||
@MockMethod(targetMethod = "get")
|
||||
private fun get_from_blackbox(self: BlackBox): String {
|
||||
return "get_from_blackbox"
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "getColor")
|
||||
@MockMethod(targetMethod = "getColor")
|
||||
private fun get_color_from_color(self: Color): String {
|
||||
return "color_from_color"
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "getColor")
|
||||
@MockMethod(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() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.error.VerifyFailedError
|
||||
import com.alibaba.testable.core.matcher.InvokeMatcher
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier
|
||||
@@ -14,19 +14,20 @@ import org.junit.jupiter.api.Test
|
||||
*/
|
||||
internal class DemoMatcherTest {
|
||||
|
||||
@TestableMock(targetMethod = "methodToBeMocked")
|
||||
private val demoMatcher = DemoMatcher()
|
||||
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithoutArgument(self: DemoMatcher) {
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "methodToBeMocked")
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArguments(self: DemoMatcher, a1: Any, a2: Any) {
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = "methodToBeMocked")
|
||||
@MockMethod(targetMethod = "methodToBeMocked")
|
||||
private fun methodWithArrayArgument(self: DemoMatcher, a: Array<Any>) {
|
||||
}
|
||||
|
||||
private val demoMatcher = DemoMatcher()
|
||||
|
||||
@Test
|
||||
fun should_match_no_argument() {
|
||||
@@ -43,8 +44,8 @@ internal class DemoMatcherTest {
|
||||
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(1.0, InvokeMatcher.anyMapOf(Int::class.javaObjectType, Float::class.javaObjectType)).times(2)
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyList(), InvokeMatcher.anySetOf(Float::class.javaObjectType)).times(2)
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyList(), InvokeMatcher.anyListOf(Float::class.javaObjectType))
|
||||
InvokeVerifier.verify("methodWithArrayArgument").with(InvokeMatcher.anyArrayOf(Long::class.javaObjectType))
|
||||
InvokeVerifier.verify("methodWithArrayArgument").with(InvokeMatcher.anyArray())
|
||||
@@ -69,12 +70,12 @@ internal class DemoMatcherTest {
|
||||
@Test
|
||||
fun should_match_with_times() {
|
||||
demoMatcher.callMethodWithNumberArguments()
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(3)
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(4)
|
||||
|
||||
demoMatcher.callMethodWithNumberArguments()
|
||||
var gotError = false
|
||||
try {
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(4)
|
||||
InvokeVerifier.verify("methodWithArguments").with(InvokeMatcher.anyNumber(), InvokeMatcher.any()).times(5)
|
||||
} catch (e: VerifyFailedError) {
|
||||
gotError = true
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock
|
||||
import com.alibaba.testable.core.annotation.MockConstructor
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import com.alibaba.testable.core.tool.TestableConst
|
||||
import com.alibaba.testable.core.tool.TestableTool.*
|
||||
import com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD
|
||||
import com.alibaba.testable.core.tool.TestableTool.TEST_CASE
|
||||
import com.alibaba.testable.demo.model.BlackBox
|
||||
import com.alibaba.testable.demo.model.ColorBox
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -16,32 +17,34 @@ import java.util.concurrent.Executors
|
||||
*/
|
||||
internal class DemoMockTest {
|
||||
|
||||
@TestableMock(targetMethod = TestableConst.CONSTRUCTOR)
|
||||
private val demoMock = DemoMock()
|
||||
|
||||
@MockConstructor
|
||||
private fun createBlackBox(text: String) = BlackBox("mock_$text")
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private fun innerFunc(self: DemoMock, text: String) = "mock_$text"
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private fun trim(self: BlackBox) = "trim_string"
|
||||
|
||||
@TestableMock(targetMethod = "substring")
|
||||
@MockMethod(targetMethod = "substring")
|
||||
private fun sub(self: BlackBox, i: Int, j: Int) = "sub_string"
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private fun startsWith(self: BlackBox, s: String) = false
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private fun secretBox(ignore: BlackBox): BlackBox {
|
||||
return BlackBox("not_secret_box")
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private fun createBox(ignore: ColorBox, color: String, box: BlackBox): BlackBox {
|
||||
return BlackBox("White_${box.get()}")
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private fun callFromDifferentMethod(self: DemoMock): String {
|
||||
return if (TEST_CASE == "should_able_to_get_test_case_name") {
|
||||
"mock_special"
|
||||
@@ -53,7 +56,6 @@ internal class DemoMockTest {
|
||||
}
|
||||
}
|
||||
|
||||
private val demoMock = DemoMock()
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_new_object() {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.alibaba.testable.demo
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockWith
|
||||
import com.alibaba.testable.core.annotation.TestableMock
|
||||
import com.alibaba.testable.core.model.MockDiagnose
|
||||
import com.alibaba.testable.core.tool.TestableConst
|
||||
import com.alibaba.testable.core.annotation.MockConstructor
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.*
|
||||
@@ -12,34 +10,34 @@ import java.util.*
|
||||
* 演示模板方法可以被Mock
|
||||
* Demonstrate template method can be mocked
|
||||
*/
|
||||
@MockWith(diagnose = MockDiagnose.ENABLE)
|
||||
internal class DemoTemplateTest {
|
||||
|
||||
private val demoTemplate = DemoTemplate()
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private fun <T> getList(self: DemoTemplate, value: T): List<T> {
|
||||
return mutableListOf((value.toString() + "_mock_list") as T)
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private fun <K, V> getMap(self: DemoTemplate, key: K, value: V): Map<K, V> {
|
||||
return mutableMapOf(key to (value.toString() + "_mock_map") as V)
|
||||
}
|
||||
|
||||
@TestableMock(targetMethod = TestableConst.CONSTRUCTOR)
|
||||
@MockConstructor
|
||||
private fun newHashSet(): HashSet<*> {
|
||||
val set = HashSet<Any>()
|
||||
set.add("insert_mock")
|
||||
return set
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private fun <E> add(s: MutableSet<E>, e: E): Boolean {
|
||||
s.add((e.toString() + "_mocked") as E)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_single_template_method() {
|
||||
val res = demoTemplate.singleTemplateMethod()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.alibaba.testable.demo.util
|
||||
|
||||
import com.alibaba.testable.core.annotation.TestableMock
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
|
||||
class PathUtilTest {
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
fun exists(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> true
|
||||
@@ -16,7 +16,7 @@ class PathUtilTest {
|
||||
}
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
fun isDirectory(f: File): Boolean {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b/c" -> true
|
||||
@@ -24,12 +24,12 @@ class PathUtilTest {
|
||||
}
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
fun delete(f: File): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
fun listFiles(f: File): Array<File>? {
|
||||
return when (f.absolutePath) {
|
||||
"/a/b" -> arrayOf(File("/a/b/c"), File("/a/b/d"))
|
||||
|
||||
13
docs/en-us/README.md
Normal file
13
docs/en-us/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
TestableMock Introduction
|
||||
---
|
||||
|
||||
The Mock method in unit testing is usually to bypass method calls that rely on external resources or irrelevant functions, so that the focus of the test can be keep on the code logic that needs to be verified and guaranteed.
|
||||
|
||||
When defining the Mock method, the developer really cares about only one thing: "<u>This call should be replaced with the fake **mock method** during testing</u>".
|
||||
|
||||
However, when the current mainstream Mock framework implements the Mock function, developers have to worry about too many things: how the Mock framework is initialized, whether it is compatible with the unit testing framework used, whether the method to be mocked is private or static, whether the Mock object is created by `new` operator or injected, how to send the mock object back to the class under test... These non-critical additional tasks greatly distract the fun of using the mock tool.
|
||||
|
||||
Therefore, we developed `TestableMock`, **a maverick and lightweight mock tool**.
|
||||
|
||||
|
||||

|
||||
26
docs/en-us/doc/comparation.md
Normal file
26
docs/en-us/doc/comparation.md
Normal file
@@ -0,0 +1,26 @@
|
||||
Mock Tools Comparison
|
||||
---
|
||||
|
||||
Besides `TestableMock`, there are also several other community Mock tools, such as `Mockito`, `Spock`, `PowerMock` and `JMockit`. Comparison as follows:
|
||||
|
||||
| Tool | Mechanism | Minimal Mock Unit | Limitation of method be mocked | Ease of use | IDE support |
|
||||
| ---- | ---- | ---- | ---- | ---- | ---- |
|
||||
| Mockito | Dynamic proxy | Class | Except private/static method and constructor | **Easy** | **Very well** |
|
||||
| Spock | Dynamic proxy | Class | Except private/static method and constructor | Complicate | Just so so |
|
||||
| PowerMock | Custom class loader | Class | **No limitation, any method works** | Complicate | **Good** |
|
||||
| JMockit | Runtime bytecode modification | Class | Except constructor (i.e. new operator) | Complicate | Just so so |
|
||||
| TestableMock | Runtime bytecode modification | Method | **No limitation, any method works** | **Very easy** | Just so so |
|
||||
|
||||
`Mockito` is Java's the most classical mock tool, good stability and easy to use, with both IntelliJ and Eclipse have plugin. The shortcoming part is that its mock functionality is sometimes not enough and have to be used in conjunction with other mock tools when necessary.
|
||||
|
||||
`Spock` is a highly readable unit testing framework with built-in mock support, it has a good overall consistency. Since it is also based on dynamic proxy implementation, its shortcomings are similar to `Mockito`.
|
||||
|
||||
`PowerMock` is a very powerful mock tool. Its basic syntax is compatible with `Mockito`, and extends many missing features of `Mockito`, including support for mocking of private, static and construction methods. However, due to the use of custom class loader, the test coverage of Jacoco will drop to zero in the default `on-the-fly` mode.
|
||||
|
||||
`JMockit` is a mock tool whose functionality and convenience are between `Mockito` and `PowerMock`, and it makes up for their respective shortcomings. The project tried to launch a rewritten version of JMockit2 in 2017 but failed to complete, and is currently in an inactive maintenance state.
|
||||
|
||||
The functionality of `TestabledMock` is basically the same as that of `PowerMock`, and it is extremely easy to use. You can complete most tasks only by mastering the annotations of `@MockMethod`.
|
||||
|
||||
The main disadvantage of the current `TestableMock` is that the IDE cannot promptly prompt whether the method parameters are matched correctly when writing the mock method. If the mocking effect does not meet expectation, it has to be verified during runtime through the method provided in the [self-help troubleshooting](en-us/doc/troubleshooting.md) document. This feature needs to be provided by extending IDE plugins in the future.
|
||||
|
||||
In addition, because `TestableMock` uses the mock mechanism based on a single method in a unique way, it decouples the mock method definition from the unit test cases. Thus, the mock methods are by default reusable, and the unit test cases become cleaner and purer. On the other hand, the mock methods become fragmented, and life cycle management is relatively difficult. Therefore, it may take some time for developers get used to it.
|
||||
8
docs/en-us/doc/feedback.md
Normal file
8
docs/en-us/doc/feedback.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Feedback Channel
|
||||
---
|
||||
|
||||
If you have any questions or suggestions during the use of `TestableMock`, please go ahead raise them in [Project Issue](https://github.com/alibaba/testable-mock/issues) and we will usually reply within 24 hours.
|
||||
|
||||
Please describe the situation in detail. If you think the problem may be related to the potential BUG of `TestableMock`, please try to provide reproducible source code or examples for shorten the troubleshooting time.
|
||||
|
||||
`TestableMock`, with no more untestable code in Java : )
|
||||
48
docs/en-us/doc/frequently-asked-questions.md
Normal file
48
docs/en-us/doc/frequently-asked-questions.md
Normal file
@@ -0,0 +1,48 @@
|
||||
Frequently Asked Questions
|
||||
---
|
||||
|
||||
#### 1. How to mock the fields initialized by `@Autowired` in the class under test?
|
||||
|
||||
Create the class under test object directly, and then use the ability of `TestableMock` to access private members to directly assign values to these fields.
|
||||
|
||||
#### 2. Can `TestableMock` be used with other Mock tools?
|
||||
|
||||
`TestableMock` can be safely used together with other Mock tools based on dynamic proxy mechanism, such as `Mockito`, `EasyMock`, `Spock`, etc., all belong to this category.
|
||||
|
||||
For Mock tools that modify the class loader or the bytecode of the class under test, such as `PowerMock` and `JMockit`, there is no yet case to prove that they will conflict with `TestableMock`, but in principle, there may be a risk of incompatibility between the two, Please use with caution.
|
||||
|
||||
#### 3. How to implement the mock method when the parent class variable points to the child class object?
|
||||
|
||||
In the code, there are often cases of using <u>interface variables or parent class variables</u> to point to an instance of subclasses and calling methods provided by the parent class or subclass.
|
||||
|
||||
At this time, follow a principle that the type of the first parameter of the mock method is always the same as the type of the variable that initiated the call.
|
||||
|
||||
Therefore, regardless of whether the actually called method comes from the parent class or the subclass, and whether the subclass overrides the method. If the calling variable is of the parent type (or interface type), the first parameter type of the Mock method should use the corresponding parent type (or interface) type.
|
||||
|
||||
See the use case of the `DemoInheritTest` test class in the Java and Kotlin examples.
|
||||
|
||||
#### 4. How to mock generic methods (template methods)?
|
||||
|
||||
Same as the Mock method of the ordinary method, just use the same generic parameters directly on the Mock method.
|
||||
|
||||
See the use case of the `DemoTemplateTest` test class in the Java and Kotlin examples.
|
||||
|
||||
> Because JVM has a generic erasure mechanism, you can also directly use the `Object` type to replace generic parameters for Java projects, see the commented out "Second solution" example in the Java version of the `DemoTemplateTest` test class.
|
||||
|
||||
#### 5. Why mocking methods in the `String` class in the Kotlin project does not work?
|
||||
|
||||
The `String` type in Kotlin language is actually `kotlin.String` instead of `java.lang.String`. However, when this type is built from bytecode, it will be replaced with Java's `java.lang.String` class, so no matter if the Mock target is written as `kotlin.String` or `java.lang.String`, it cannot match the original called method.
|
||||
|
||||
In actual scenarios, there are very few scenarios where methods in the `String` class need to be mocked, so `TestableMock` has not dealt with this situation specifically.
|
||||
|
||||
#### 6. When trigger a single test case in IntelliJ IDE, why class with `@EnablePrivateAccess` annotation report still private member access errors?
|
||||
|
||||
The default compiler provided by IntelliJ handle the annotation processor of the `JSR-269` specification in an incompatible way of maven. You can turn on the "Delegate IDE build/run actions to maven" option in "Build Tools > Maven > Runner" of IntelliJ system configuration:
|
||||
|
||||

|
||||
|
||||
#### 7. Can `TestableMock` be used for testing Android projects?
|
||||
|
||||
It can be used in combination with [Roboelectric](https://github.com/robolectric/robolectric) testing framework.
|
||||
|
||||
The `Dalvik` and `ART` virtual machines of the Android system use a bytecode system different from the standard JVM, which will affect the normal functionality of `TestableMock`. The `Roboelectric` framework can run Android unit tests on a standard JVM virtual machine, which is much faster than running unit tests through the Android virtual machine. Recently, most Android App unit tests are written with the `Roboelectric` framework.
|
||||
75
docs/en-us/doc/invoke-matcher.md
Normal file
75
docs/en-us/doc/invoke-matcher.md
Normal file
@@ -0,0 +1,75 @@
|
||||
Verify Mock Invocation
|
||||
---
|
||||
|
||||
In the test, in addition to replacing **methods contain external dependencies** with mocks, it is often necessary to verify whether the actual parameters of mock invocation are in accordance with expectations.
|
||||
|
||||
The **verifiers** and **matchers** are provided in `TestableMock` to achieve this function. for example:
|
||||
|
||||
```java
|
||||
@Test
|
||||
public test_case() {
|
||||
int res = insToTest.methodToTest();
|
||||
verify("mockMethod").with(123, "abc");
|
||||
}
|
||||
```
|
||||
|
||||
This use case will check whether the mock method named `mockMethod` has been called when the method under test `methodToTest()` is executed, and whether the parameter values received during the call are `123` and `"abc"` (Assuming that the `mockMethod` method has two parameters).
|
||||
|
||||
In addition to this simple verification, `TestableMock` currently supports a variety of **validators**, as well as **matchers** that can fuzzy match parameter characteristics.
|
||||
|
||||
The `DemoMatcherTest` test classes in the sample projects `java-demo` and `kotlin-demo` show the usage of these validators and matchers in detail.
|
||||
|
||||
## Basic validator
|
||||
|
||||
- `with(Object...args)` → verify whether the method has been called by the specified parameters
|
||||
- `withInOrder(Object... args)` → verify the specified method is called with specified parameters according to the actual calling order
|
||||
- `withTimes(int expectedCount)` → verify whether the method has been called the specified number of times, ignoring the check of the calling parameters
|
||||
- `without(Object...args)` → verification method has never been called with specified parameters
|
||||
- `times(int count)` → use after the `with()` or `withInOrder()` method to verify that the method has been called the specified number of times with the same conditions
|
||||
|
||||
## Basic matcher
|
||||
|
||||
- `any()` → matches any value, including `null`
|
||||
- `any(Class<?> clazz)` → match any value of the specified type or subtype
|
||||
- `anyTypeOf(Class<?>... classes)` → match any type of value in the list
|
||||
- `anyString()` → matches any string
|
||||
- `anyNumber()` → matches any number (integer, long, float, double, etc)
|
||||
- `anyBoolean()` → matches any Boolean value
|
||||
- `anyByte()` → match any single byte type value
|
||||
- `anyChar()` → matches any single character type value
|
||||
- `anyInt()` → matches any integer value
|
||||
- `anyLong()` → matches any value of long integer type
|
||||
- `anyFloat()` → match any value of float type
|
||||
- `anyDouble()` → match any double-precision float number type value
|
||||
- `anyShort()` → matches any value of short integer type
|
||||
- `anyArray()` → matches any array
|
||||
- `anyArrayOf(Class<?> clazz)` → matches any array of the specified type
|
||||
- `anyList()` → matches any list
|
||||
- `anyListOf(Class<?> clazz)` → matches any list of the specified type
|
||||
- `anySet()` → matches any set
|
||||
- `anySetOf(Class<?> clazz)` → matches any set of the specified type
|
||||
- `anyMap()` → match any map
|
||||
- `anyMapOf(Class<?> keyClass, Class<?> valueClass)` → match any map of the specified type
|
||||
- `anyCollection()` → match any container
|
||||
- `anyCollectionOf(Class<?> clazz)` → matches any specified type of container
|
||||
- `anyIterable()` → matches any iterator
|
||||
- `anyIterableOf(Class<?> clazz)` → matches any iterator of the specified type
|
||||
- `eq(Object obj)` → match objects equal to the specified value
|
||||
- `refEq(Object obj)` → match the specified object (not equal to the value, but the same object)
|
||||
|
||||
## Null value matcher
|
||||
|
||||
- `isNull()` → match `null`
|
||||
- `notNull()` → matches any value except `null`
|
||||
- `nullable(Class<?> clazz)` → matches any value of `null` or specified type
|
||||
|
||||
## String matcher
|
||||
|
||||
- `contains(String substring)` → match a string containing a specific substring
|
||||
- `matches(String regex)` → match strings that match the specified regular expression
|
||||
- `endsWith(String suffix)` → match the string ending with the specified substring
|
||||
- `startsWith(String prefix)` → match the string starting with the specified substring
|
||||
|
||||
## Universal matcher
|
||||
|
||||
- `any(MatchFunction matcher)` → match the value that matches the specified expression
|
||||
34
docs/en-us/doc/private-accessor.md
Normal file
34
docs/en-us/doc/private-accessor.md
Normal file
@@ -0,0 +1,34 @@
|
||||
Private Accessor
|
||||
---
|
||||
|
||||
Nowadays, the debate about whether private methods should be unit tested is gradually disappearing, and the common practice of developers has given factual answers. Indirect testing of private methods through public methods is difficult in many cases. Developers are more willing to modify method visibility to make original private methods testable in test cases.
|
||||
|
||||
In addition, before unit testing begin, it is often necessary to initialize specific member fields of the object under test, but sometimes it could be impossible to easily assign values to these private fields. So, is it possible let the code in the unit test case directly access the private methods and member fields of the class under test without breaking the encapsulation of the type under test? `TestableMock` provides two simple solutions.
|
||||
|
||||
### Solution 1: Use `@EnablePrivateAccess` annotation
|
||||
|
||||
Just add `@EnablePrivateAccess` annotation to the test class, then you have got the following enhancements in the test case:
|
||||
|
||||
- Invoke private methods (including static methods) of the class under test
|
||||
- Read private fields (including static fields) of the class under test
|
||||
- Modify private fields (including static fields) of the class under test
|
||||
- Modify the constant fields of the class under test (fields modified with final, including static fields)
|
||||
|
||||
When accessing and modifying private and constant members, the IDE may prompt some syntax errors, but the compiler will be able to run the test normally.
|
||||
|
||||
For the effect, see the use case in the test class of the `java-demo` sample project `DemoPrivateAccessTest`. (Using compile-time code enhancement, currently only the adaptation of the Java language is implemented)
|
||||
|
||||
### Solution 2: Use the `PrivateAccessor` tool class
|
||||
|
||||
If you don't want to see the IDE's syntax error reminder, or in a non-Java language JVM project (such as Kotlin language), you can also use the `PrivateAccessor` tool class to directly access private members.
|
||||
|
||||
This class provides 6 static methods:
|
||||
|
||||
- `PrivateAccessor.get(<ObjectUnderTest>, "<private-field-name>")` ➜ read the private field of the class under test
|
||||
- `PrivateAccessor.set(<ObjectUnderTest>, "<private-field-name>", <new-value>)` ➜ modify the private field (or constant field) of the class under test
|
||||
- `PrivateAccessor.invoke(<ObjectUnderTest>, "<private-method-name>", <call-parameters>..)` ➜ call the private method of the class under test
|
||||
- `PrivateAccessor.getStatic(<ClassUnderTest>, "<private-static-field-name>")` ➜ read the **static** private field of the class under test
|
||||
- `PrivateAccessor.setStatic(<ClassUnderTest>, "<private-static-field-name>", <new-value>)` ➜ modify the **static** private field (or **static** constant field) of the class under test
|
||||
- `PrivateAccessor.invokeStatic(<ClassUnderTest>, "<private-static-method-name>", <call-parameters>..)` ➜ call the **static** private method of the class under test
|
||||
|
||||
For details, see the use cases in the test classes of the `java-demo` and `kotlin-demo` sample projects `DemoPrivateAccessTest`.
|
||||
73
docs/en-us/doc/release-note.md
Normal file
73
docs/en-us/doc/release-note.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Release Note
|
||||
|
||||
## 0.4.4
|
||||
- fix an issue of accessing private method with interface type parameter fail
|
||||
- fix an issue of mocking static method without parameter fail
|
||||
|
||||
## 0.4.3
|
||||
- support static private member access
|
||||
|
||||
## 0.4.2
|
||||
- support change javaagent global log level via maven plugin
|
||||
- fix an issue of duplicate test class injection
|
||||
- fix an issue cause multiple-line method invocation mock fail
|
||||
|
||||
## 0.4.1
|
||||
- deprecate @TestableMock annotation, use @MockMethod and @MockConstructor instead
|
||||
|
||||
## 0.4.0
|
||||
- fix a jvm 9+ compatibility issue cause by default classloader change
|
||||
- fix a conflict issue when testcase name duplicated between different class
|
||||
- refactor code structure, module `testable-all` added
|
||||
- separate constant definition from `TestableTool` to `TestableConst`
|
||||
|
||||
## 0.3.2
|
||||
- support grable project for both private member access and quick mock
|
||||
- support access private static field and methods via PrivateAccessor
|
||||
|
||||
## 0.3.1
|
||||
- support detail log of mocking process for diagnosis
|
||||
|
||||
## v0.3.0
|
||||
- add `without()` checker to verify mock method never invoked with specified parameters
|
||||
- support fuzz matcher when verifying mock invocation
|
||||
|
||||
## v0.2.2
|
||||
- support mock method parameters check
|
||||
- fix a compatibility issue with jvm 9+
|
||||
|
||||
## v0.2.1
|
||||
- support mock static method
|
||||
- support mock kotlin companion object method
|
||||
- support mock invoke by interface / base class object
|
||||
|
||||
## v0.2.0
|
||||
- use `TestableTool` class to expose test context and verify mock invoke
|
||||
- add `testable-maven-plugin` module to simplify javaagent configuration
|
||||
- remove dependence on EnableTestable annotation in `testable-agent`
|
||||
- rename annotations to reflect the actual use
|
||||
|
||||
## v0.1.0
|
||||
- move generated agent jar to class folder
|
||||
- support mock method of any object
|
||||
|
||||
## v0.0.5
|
||||
- use dynamically runtime modification to replace static `e.java` file
|
||||
- get rid of unit test framework dependence
|
||||
- add testable ref field in test class at runtime instead of compile time
|
||||
|
||||
## v0.0.4
|
||||
- use runtime byte code rewrite to invoke testable setup method
|
||||
- add `TestableUtil` class to fetch current test case and invocation source
|
||||
|
||||
## v0.0.3
|
||||
- use global method invoke to access private members instead of modification in place
|
||||
- use `e.java` replace `testable` class make code more readable
|
||||
- introduce `agent` module, use runtime byte code modification to support new operation and member method mocking
|
||||
|
||||
## v0.0.2
|
||||
- add support of member method mocking by compile time code modification
|
||||
|
||||
## v0.0.1
|
||||
- PoC version
|
||||
- use compile time code modification to support new operation mocking and private field & method access
|
||||
78
docs/en-us/doc/setup.md
Normal file
78
docs/en-us/doc/setup.md
Normal file
@@ -0,0 +1,78 @@
|
||||
Use TestableMock
|
||||
---
|
||||
|
||||
`TestableMock` is an assist tool for Java unit testing based on source code and bytecode enhancement, including the following functions:
|
||||
|
||||
- [Access private members of the class under test](en-us/doc/private-accessor.md): enable unit tests directly invoke or access private members of the class under test, solve the problems of private member initialization and private method testing
|
||||
- [Quick Mock arbitrary call](en-us/doc/use-mock.md): quickly replace any method invocation in the class under test with a mock method, solve the cumbersome use of traditional mock tools problem
|
||||
- [Auxiliary test void method](en-us/doc/test-void-method.md): use the Mock validator to check the internal logic of method, solve the problem that unit testing is difficult to implement to the method with no return value
|
||||
|
||||
## Use in Maven project
|
||||
|
||||
In the project `pom.xml` file, add `testable-all` dependency and `maven-surefire-plugin` configuration, the specific method is as follows.
|
||||
|
||||
It is recommended to add a `property` field that identifies the TestableMock version, in order to simplify version management:
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.4.4</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
Add dependence of `TestableMock` inside `dependencies` field:
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
Finally, add the `maven-surefire-plugin` plugin to the `plugins` list in the `build` area (if this plugin is already included, just add the `<argLine>` part of the configuration):
|
||||
|
||||
```xml
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
```
|
||||
|
||||
If the project also uses the `on-the-fly` mode of `Jacoco` (default mode) to calculate the unit test coverage, you need to add a `@{argLine}` parameter in the `<argLine>` configuration, after adding it The configuration is as follows:
|
||||
|
||||
```xml
|
||||
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
```
|
||||
|
||||
See the [pom.xml](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/pom.xml) file of project `java-demo` and the [pom.xml](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/pom.xml) file of project `kotlin-demo`.
|
||||
|
||||
## Use in Gradle project
|
||||
|
||||
Add dependence of `TestableMock` in `build.gradle` file:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.4')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.4')
|
||||
}
|
||||
```
|
||||
|
||||
Then add javaagent to `test` configuration:
|
||||
|
||||
```groovy
|
||||
test {
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
}
|
||||
```
|
||||
|
||||
See the [build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/build.gradle) file of project `java-demo` and the [build.gradle.kts](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/build.gradle.kts) file of project `kotlin-demo`.
|
||||
94
docs/en-us/doc/test-void-method.md
Normal file
94
docs/en-us/doc/test-void-method.md
Normal file
@@ -0,0 +1,94 @@
|
||||
Test Void Method
|
||||
---
|
||||
|
||||
"How to test void type methods" has always been a topic that many unit testing frameworks are quietly avoiding. Since the existing unit testing methods are mainly to verify the returned results of the tested unit, when the method has no return value, there is no way to test.
|
||||
|
||||
From a functional point of view, although the void method does not return any value, its execution will definitely have some potential impact on the outside world. We call it the "side effect" of the method, such as:
|
||||
|
||||
1. Initialize external variables (private member variables or global static variables)
|
||||
2. Assign value to external object
|
||||
3. Print logs
|
||||
4. Invoke other external methods
|
||||
5. ... ...
|
||||
|
||||
> A method that does not return any value and does not produce any "side effects" has no meaning to exist.
|
||||
|
||||
The essence of these "side effects" can be summarized into two categories: **modify external variables** and **invoke external methods**.
|
||||
|
||||
Through the private field accessor and the mock validator of `TestableMock`, the "side effects" can be easily checked.
|
||||
|
||||
### 1. Void type method which modify external variables
|
||||
|
||||
For example, the following method will modify the private member variable `hashCache` based on the input:
|
||||
|
||||
```java
|
||||
class Demo {
|
||||
private Map<String, Integer> hashCache = mapOf();
|
||||
|
||||
public void updateCache(String domain, String key) {
|
||||
String cacheKey = domain + "::" + key;
|
||||
Integer num = hashCache.get(cacheKey);
|
||||
hashCache.put(cacheKey, count == null ? initHash(key) : nextHash(num, key));
|
||||
}
|
||||
|
||||
... // Other methods omitted
|
||||
}
|
||||
```
|
||||
|
||||
To test this method, you can use `TestableMock` directly read the value of the private member variable and verify the result:
|
||||
|
||||
```java
|
||||
@EnablePrivateAccess // Enable private member access functionality of TestableMock
|
||||
class DemoTest {
|
||||
private Demo demo = new Demo();
|
||||
|
||||
@Test
|
||||
public void testSaveToCache() {
|
||||
Integer firstVal = demo.initHash("hello"); // Invoke private method
|
||||
Integer nextVal = demo.nextHash(firstVal, "hello"); // Invoke private method
|
||||
demo.saveToCache("demo", "hello");
|
||||
assertEquals(firstVal, demo.hashCache.get("demo::hello")); // Access private variable
|
||||
demo.saveToCache("demo", "hello");
|
||||
assertEquals(nextVal, demo.hashCache.get("demo::hello")); // Access private variable
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Void type method which invoke external method
|
||||
|
||||
For example, the following method will print information to the console based on input:
|
||||
|
||||
```java
|
||||
class Demo {
|
||||
public void recordAction(Action action) {
|
||||
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss ");
|
||||
String timeStamp = df.format(new Date());
|
||||
System.out.println(timeStamp + "[" + action.getType() + "] " + action.getTarget());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To test this method, you can use `TestableMock` to quickly mock out the `System.out.println` method. In the Mock method body, you can simply call the original method (equivalent to not affecting the original method function, only used for call recording), or leave it blank (equivalent to removing the side effects of the original method).
|
||||
|
||||
After executing the void type method under test, use `InvokeVerifier.verify()` to verify whether the incoming print content meets expectations:
|
||||
|
||||
```java
|
||||
class DemoTest {
|
||||
private Demo demo = new Demo();
|
||||
|
||||
// Intercept `System.out.println` invocation
|
||||
@MockMethod
|
||||
public void println(PrintStream ps, String msg) {
|
||||
// Execute the original call
|
||||
ps.println(msg);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecordAction() {
|
||||
Action action = new Action("click", ":download");
|
||||
demo.recordAction();
|
||||
// Verify mock method `println` is invoked, and passing parameters in line with expectations
|
||||
verify("println").with(matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download"));
|
||||
}
|
||||
}
|
||||
```
|
||||
40
docs/en-us/doc/troubleshooting.md
Normal file
40
docs/en-us/doc/troubleshooting.md
Normal file
@@ -0,0 +1,40 @@
|
||||
Self-Help Troubleshooting
|
||||
---
|
||||
|
||||
Compared with `Mockito` and other mock tools where developers have to manually inject mock classes, `TestableMock` uses method name and parameter type matching to automatically find invocations that require mock. While this mechanism brings convenience, it may also cause unexpected mock replacement.
|
||||
|
||||
To troubleshoot mock-related issues, just add the `@MockWith` annotation to the test class, and configure the parameter `diagnose` to `MockDiagnose.ENABLE`, so the detailed Mock method replacement process will be printed when the test is run.
|
||||
|
||||
```java
|
||||
@MockWith(diagnose = MockDiagnose.ENABLE)
|
||||
class DemoTest {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The output log example is as follows:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
|
||||
[DIAGNOSE] 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
|
||||
```
|
||||
|
||||
The log shows all the mocked invocation and corresponding code line numbers in the class under test.
|
||||
42
docs/en-us/doc/use-maven-plugin.md
Normal file
42
docs/en-us/doc/use-maven-plugin.md
Normal file
@@ -0,0 +1,42 @@
|
||||
Testable Maven Plugin
|
||||
---
|
||||
|
||||
For projects built with Maven, in addition to directly modifying the argument of the `maven-surefire-plugin` plugin, the same effect can also be obtained through the `testable-maven-plugin` plugin:
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare</id>
|
||||
<goals>
|
||||
<goal>prepare</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
> When using `testable-maven-plugin`, the `TestableMock` related configuration on `maven-surefire-plugin` should be removed.
|
||||
|
||||
The `testable-maven-plugin` can be used with the Jacoco plugin without additional adaptation, so it can make the writing of the `pom.xml` file easier and more readable.
|
||||
|
||||
However, it should be noted that when running a single test case through the IDE, those mock function may fail to work.
|
||||
|
||||
This is because the IDE usually only runs the `maven-surefire-plugin` plugin when running a single test case, skipping the execution of the `testable-maven-plugin` plugin, resulting in the JavaAgent required for the mock function not being injected into the context.
|
||||
|
||||
This problem can be bypassed by configuring the test parameters of the IDE additionally. Take IntelliJ as an example, open the "Edit Configuration..." option of the run menu, as shown in the position ①
|
||||
|
||||

|
||||
|
||||
Add JavaAgent startup parameters at the end of the "virtual machine parameters" attribute value: `-javaagent:${HOME}/.m2/repository/com/alibaba/testable/testable-agent/xyz/testable-agent-xyzjar`, as shown in the figure position ②
|
||||
|
||||
> PS: Please replace `x.y.z` in the path with the actual version number
|
||||
|
||||

|
||||
|
||||
Finally, click to run the unit test, as shown in the position ③
|
||||
|
||||
In general, the additional complexity of using `testable-maven-plugin` is still higher than its simplified configuration. At present, modify the configuration of the `maven-surefire-plugin` directly in the `pom.xml` file is still the preferred solution.
|
||||
5
docs/en-us/navbar.md
Normal file
5
docs/en-us/navbar.md
Normal file
@@ -0,0 +1,5 @@
|
||||
* [Home](/en-us/)
|
||||
* [Feedback And Issue](https://github.com/alibaba/testable-mock/issues)
|
||||
* Languages
|
||||
* English
|
||||
* [中文](/zh-cn/)
|
||||
16
docs/en-us/sidebar.md
Normal file
16
docs/en-us/sidebar.md
Normal file
@@ -0,0 +1,16 @@
|
||||
- Quick Start
|
||||
- [Use TestableMock](en-us/doc/setup.md)
|
||||
- [Private Accessor](en-us/doc/private-accessor.md)
|
||||
- [Fast Mocking](en-us/doc/use-mock.md)
|
||||
- [Test Void Method](en-us/doc/test-void-method.md)
|
||||
|
||||
- Usage Reference
|
||||
- [Verify Mock Invocation](en-us/doc/invoke-matcher.md)
|
||||
- [Frequently Asked Questions](en-us/doc/frequently-asked-questions.md)
|
||||
- [Self-Help Troubleshooting](en-us/doc/troubleshooting.md)
|
||||
- [Testable Maven Plugin](en-us/doc/use-maven-plugin.md)
|
||||
|
||||
- Technical Reference
|
||||
- [Mock Tools Comparison](en-us/doc/comparation.md)
|
||||
- [Release Note](en-us/doc/release-note.md)
|
||||
- [Feedback Channel](en-us/doc/feedback.md)
|
||||
@@ -14,7 +14,7 @@
|
||||
<div id="app">Loading...</div>
|
||||
<script>
|
||||
window.$docsify = {
|
||||
name: 'Testable',
|
||||
name: 'TestableMock',
|
||||
repo: 'https://github.com/alibaba/testable-mock',
|
||||
loadSidebar: "sidebar.md",
|
||||
loadNavbar: "navbar.md",
|
||||
|
||||
@@ -9,4 +9,4 @@ TestableMock简介
|
||||
|
||||
于是,我们开发了`TestableMock`,**一款特立独行的轻量Mock工具**。
|
||||
|
||||

|
||||

|
||||
|
||||
26
docs/zh-cn/doc/comparation.md
Normal file
26
docs/zh-cn/doc/comparation.md
Normal file
@@ -0,0 +1,26 @@
|
||||
主流Mock工具对比
|
||||
---
|
||||
|
||||
除`TestableMock`外,目前主要的Mock工具主要有`Mockito`、`Spock`、`PowerMock`和`JMockit`,基本差异如下:
|
||||
|
||||
| 工具 | 原理 | 最小Mock单元 | 对被Mock方法的限制 | 上手难度 | IDE支持 |
|
||||
| ---- | ---- | ---- | ---- | ---- | ---- |
|
||||
| Mockito | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | **较容易** | **很好** |
|
||||
| Spock | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | 较复杂 | 一般 |
|
||||
| PowerMock | 自定义类加载器 | 类 | **任何方法皆可** | 较复杂 | **较好** |
|
||||
| JMockit | 运行时字节码修改 | 类 | 不能Mock构造方法(new操作符) | 较复杂 | 一般 |
|
||||
| TestableMock | 运行时字节码修改 | 方法 | **任何方法皆可** | **很容易** | 一般 |
|
||||
|
||||
`Mockito`是Java最老牌的Mock工具,稳定性和易用性较好,IntelliJ和Eclipse都有专用插件支持。相对不足之处在于Mock功能稍弱,在必要情况下需与其他Mock工具配合使用。
|
||||
|
||||
`Spock`是一款代码可读性非常高的单元测试框架,内置Mock支持,具有很好的整体感。由于同样基于动态代理实现,其不足点与`Mockito`类似。
|
||||
|
||||
`PowerMock`是一款功能十分强大的Mock工具,其基本语法与`Mockito`兼容,同时扩展了许多`Mockito`缺失的功能,包括对支持对私有、静态和构造方法实施Mock。但由于使用了自定义类加载器,会导致Jacoco在默认的`on-the-fly`模式下覆盖率跌零。
|
||||
|
||||
`JMockit`是一款功能性与易用性均居于`Mockito`与`PowerMock`之间的Mock工具,较好的弥补了两者各自的不足。该项目在2017年尝试推出JMockit2重写版本但未能完成,目前处于不活跃的维护状态。
|
||||
|
||||
相比之下,`TestabledMock`的功能与`PowerMock`基本平齐,且极易上手,只需掌握`@MockMethod`注解就可以完成绝大多数任务。
|
||||
|
||||
当前`TestableMock`的主要不足在于,编写Mock方法时IDE尚无法即时提示方法参数是否正确匹配。若发现匹配效果不符合预期,需要通过[自助问题排查](zh-cn/doc/troubleshooting.md)文档提供的方法在运行期进行校验。这个功能未来需要通过扩展主流IDE插件来提供。
|
||||
|
||||
此外,由于`TestableMock`独辟蹊径的采用基于单个方法的Mock机制,将Mock方法定义与单元测试用例解耦,一方面使得Mock方法具有默认可复用性,单元测试用例也因此变得更干净纯粹,另一方面也导致Mock方法定义变得零散,生命周期管理起来相对困难,对现有开发者的Mock编写习惯会带来一定改变。
|
||||
8
docs/zh-cn/doc/feedback.md
Normal file
8
docs/zh-cn/doc/feedback.md
Normal file
@@ -0,0 +1,8 @@
|
||||
问题反馈
|
||||
---
|
||||
|
||||
在使用`TestableMock`过程中如有任何问题或建议,请直接在[项目Issue](https://github.com/alibaba/testable-mock/issues)中提出,我们将在24小时内回复。
|
||||
|
||||
请详细描述遇到的情况,若您觉得问题可能与`TestableMock`的潜在BUG有关,请尽量提供可复现的相关源码或示例,以便进行针对性定位和排查。
|
||||
|
||||
`TestableMock`,让Java没有难测的代码 : )
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
`TestableMock`可与其他基于动态代理机制的Mock工具安全的共同使用,譬如`Mockito`、`EasyMock`、`MockRunner`等皆属此范畴。
|
||||
|
||||
对于其他会修改类加载器或被测类字节码的Mock工具,譬如`PowerMock`和`JMockit`,尚无案例证明会与`TestableMock`发生冲突,但从原理来说二者可能存在不兼容风险,请谨慎使用。
|
||||
对于会修改类加载器或被测类字节码的Mock工具,譬如`PowerMock`和`JMockit`,尚无案例证明会与`TestableMock`发生冲突,但从原理来说二者可能存在不兼容风险,请谨慎使用。
|
||||
|
||||
#### 3. 父类变量指向子类对象时,如何实现Mock方法?
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
这时候遵循一个原则,Mock方法的首个参数类型**始终与发起调用的变量类型一致**。
|
||||
|
||||
因此,不论实际被调用方法来自父类还是子类,也不论子类是否覆写该方法。若变量为父类型(或接口类型),则Mock方法的首个参数类型都应该使用相同的父类(或接口)类型。
|
||||
因此,不论实际被调用方法来自父类还是子类,也不论子类是否覆写该方法。若发起调用的变量为父类型(或接口类型),则Mock方法的首个参数类型都应该使用相应的父类(或接口)类型。
|
||||
|
||||
参见Java和Kotlin示例中`DemoInheritTest`测试类的用例。
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
参见Java和Kotlin示例中`DemoTemplateTest`测试类的用例。
|
||||
|
||||
不过,由于JVM存在泛型擦除机制,对于Java项目也可以直接使用`Object`类型替代泛型参数,见Java版`DemoTemplateTest`测试类中被注释掉的"第二种写法"示例。
|
||||
> 由于JVM存在泛型擦除机制,对于Java项目也可以直接使用`Object`类型替代泛型参数,见Java版`DemoTemplateTest`测试类中被注释掉的"第二种写法"示例。
|
||||
|
||||
#### 5. 在Kotlin项目对`String`类中的方法进行Mock不生效?
|
||||
|
||||
@@ -35,7 +35,13 @@ Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.S
|
||||
|
||||
实际场景中需要对`String`类中的方法进行Mock的场景很少,`TestableMock`暂未对这种情况做特别处理。
|
||||
|
||||
#### 6. `TestableMock`能否用于Android项目的测试?
|
||||
#### 6. 在IntelliJ IDE中运行单个测试用例时,用了`@EnablePrivateAccess`注解还是报私有成员访问错误?
|
||||
|
||||
IntelliJ默认编译方法对`JSR-269`规范注解处理器的处理机制与Maven标准不完全兼容,在IntelliJ系统配置的"Build Tools > Maven > Runner"中开启"Delegate IDE build/run actions to maven"选项即可:
|
||||
|
||||

|
||||
|
||||
#### 7. `TestableMock`能否用于Android项目的测试?
|
||||
|
||||
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
在测试中,除了需要将某些含有外部依赖的方法替换为Mock,经常还会需要验证该方法被调用时的参数是否符合预期。
|
||||
|
||||
在TestableMock中提供了校验器(verifier)和匹配器(matcher)来实现这一功能。譬如:
|
||||
在`TestableMock`中提供了校验器(verifier)和匹配器(matcher)来实现这一功能。譬如:
|
||||
|
||||
```java
|
||||
@Test
|
||||
@@ -13,9 +13,9 @@ public test_case() {
|
||||
}
|
||||
```
|
||||
|
||||
这个用例会检查在执行被测方法`methodToTest()`时,名称是`mockMethod`的Mock方法应当被调用过,且调用时收到的参数值为123和"abc"(假设被Mock的`mockMethod`方法有两个参数)。
|
||||
这个用例会检查在执行被测方法`methodToTest()`时,名称是`mockMethod`的Mock方法是否有被调用过,且调用时收到的参数值是否为`123`和`"abc"`(假设被Mock的`mockMethod`方法有两个参数)。
|
||||
|
||||
除了这种简单校验以外,TestableMock当前已经支持了多种**校验器**,以及能够模糊匹配参数特征的**匹配器**。
|
||||
除了这种简单校验以外,`TestableMock`当前已经支持了多种**校验器**,以及能够模糊匹配参数特征的**匹配器**。
|
||||
|
||||
在示例项目`java-demo`和`kotlin-demo`中的`DemoMatcherTest`测试类详细展示了这些校验器和匹配器的用法。
|
||||
|
||||
|
||||
@@ -3,17 +3,32 @@
|
||||
|
||||
如今关于私有方法是否应该做单元测试的争论正逐渐消停,开发者的普遍实践已经给出事实答案。通过公有方法间接测私有方法在很多情况下难以进行,开发者们更愿意通过修改方法可见性的办法来让原本私有的方法在测试用例中变得可测。
|
||||
|
||||
此外,在单元测试中时常会需要对被测对象进行特定的成员字段初始化,但有时由于被测类的构造方法限制,使得无法便捷的对这些字段进行赋值。那么,能否在不破坏被测类型封装的情况下,允许单元测试用例内的代码直接访问被测类的私有方法和成员变量呢?TestableMock提供了一种简单的解决方案。
|
||||
此外,在单元测试中时常会需要对被测对象进行特定的成员字段初始化,但有时由于被测类的构造方法限制,使得无法便捷的对这些字段进行赋值。那么,能否在不破坏被测类型封装的情况下,允许单元测试用例内的代码直接访问被测类的私有方法和成员字段呢?`TestableMock`提供了两种简单的解决方案。
|
||||
|
||||
### 方法一:使用`@EnablePrivateAccess`注解
|
||||
|
||||
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
|
||||
|
||||
- 调用被测类的私有方法
|
||||
- 读取被测类的私有成员
|
||||
- 修改被测类的私有成员
|
||||
- 修改被测类的常量成员(使用final修饰的成员)
|
||||
- 调用被测类的私有方法(包括静态方法)
|
||||
- 读取被测类的私有字段(包括静态字段)
|
||||
- 修改被测类的私有字段(包括静态字段)
|
||||
- 修改被测类的常量字段(使用final修饰的字段,包括静态字段)
|
||||
|
||||
访问和修改私有、常量成员时,IDE可能会提示语法有误,但编译器将能够正常运行测试。(使用编译期代码增强,目前仅实现了Java语言的适配)
|
||||
|
||||
若不希望看到IDE的语法错误提醒,或是在非Java语言的JVM工程(譬如Kotlin语言)里,也可以借助`PrivateAccessor`工具类来实现私有成员的访问。
|
||||
效果见`java-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
|
||||
|
||||
效果见`java-demo`和`kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
|
||||
### 方法二:使用`PrivateAccessor`工具类
|
||||
|
||||
若不希望看到IDE的语法错误提醒,或是在非Java语言的JVM工程(譬如Kotlin语言)里,也可以借助`PrivateAccessor`工具类来直接访问私有成员。
|
||||
|
||||
这个类提供了6个静态方法:
|
||||
|
||||
- `PrivateAccessor.get(被测对象, "私有字段名")` ➜ 读取被测类的私有字段
|
||||
- `PrivateAccessor.set(被测对象, "私有字段名", 新的值)` ➜ 修改被测类的私有字段(或常量字段)
|
||||
- `PrivateAccessor.invoke(被测对象, "私有方法名", 调用参数..)` ➜ 调用被测类的私有方法
|
||||
- `PrivateAccessor.getStatic(被测类型, "私有静态字段名")` ➜ 读取被测类的**静态**私有字段
|
||||
- `PrivateAccessor.setStatic(被测类型, "私有静态字段名", 新的值)` ➜ 修改被测类的**静态**私有字段(或**静态**常量字段)
|
||||
- `PrivateAccessor.invokeStatic(被测类型, "私有静态方法名", 调用参数..)` ➜ 调用被测类的**静态**私有方法
|
||||
|
||||
详见`java-demo`和`kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# Release Note
|
||||
|
||||
## 0.4.4
|
||||
- 修复无法访问参数类型包含接口的私有方法的BUG (issue-15)
|
||||
- 修复Mock无参数的静态方法会出错的BUG (issue-16)
|
||||
|
||||
## 0.4.3
|
||||
- 完善了对私有静态成员的直接访问能力
|
||||
|
||||
## 0.4.2
|
||||
- support change javaagent global log level via maven plugin
|
||||
- fix an issue of duplicate test class injection
|
||||
- fix an issue cause multiple-line method invocation mock fail
|
||||
|
||||
## 0.4.1
|
||||
- deprecate @TestableMock annotation, use @MockMethod and @MockConstructor instead
|
||||
|
||||
## 0.4.0
|
||||
- fix a jvm 9+ compatibility issue cause by default classloader change
|
||||
- fix a conflict issue when testcase name duplicated between different class
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
|
||||
## 在Maven项目中使用
|
||||
|
||||
在项目`pom.xml`文件中,增加`testable-processor`依赖和`maven-surefire-plugin`配置,具体方法如下。
|
||||
在项目`pom.xml`文件中,增加`testable-all`依赖和`maven-surefire-plugin`配置,具体方法如下。
|
||||
|
||||
建议先添加一个标识TestableMock版本的`property`,便于统一管理:
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.4.0</testable.version>
|
||||
<testable.version>0.4.4</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -25,20 +25,14 @@
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-processor</artifactId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>provided</scope>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
最后在`build`区域的`plugins`列表里添加`maven-surefire-plugin`插件(如果已有此插件则只需添加`<argLine>`部分配置):
|
||||
最后在`build`区域的`plugins`列表里添加`maven-surefire-plugin`插件(如果已包含此插件则只需添加`<argLine>`部分配置):
|
||||
|
||||
```xml
|
||||
<build>
|
||||
@@ -68,8 +62,8 @@
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.0')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.4')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.4')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
5. ... ...
|
||||
|
||||
|
||||
> 不返回任何值也不产生任何"副作用"的方法没有存在意义。
|
||||
> 不返回任何值也不产生任何"副作用"的方法没有存在的意义。
|
||||
|
||||
这些"副作用"归纳来说可分为两类:**修改外部变量**和**调用外部方法**。
|
||||
这些"副作用"的本质归纳来说可分为两类:**修改外部变量**和**调用外部方法**。
|
||||
|
||||
通过TestableMock的私有字段访问和Mock校验器可以很方便的实现对"副作用"的结果检查。
|
||||
通过`TestableMock`的私有字段访问和Mock校验器可以很方便的实现对"副作用"的结果检查。
|
||||
|
||||
#### 修改外部变量的void方法
|
||||
### 1. 修改外部变量的void方法
|
||||
|
||||
例如,下面这个方法会根据输入修改私有成员变量`hashCache`:
|
||||
|
||||
@@ -36,7 +36,7 @@ class Demo {
|
||||
}
|
||||
```
|
||||
|
||||
若要测试此方法,可以利用TestableMock直接读取私有成员变量的值,对结果进行校验:
|
||||
若要测试此方法,可以利用`TestableMock`直接读取私有成员变量的值,对结果进行校验:
|
||||
|
||||
```java
|
||||
@EnablePrivateAccess // 启用TestableMock的私有成员访问功能
|
||||
@@ -55,7 +55,7 @@ class DemoTest {
|
||||
}
|
||||
```
|
||||
|
||||
#### 调用外部方法的void方法
|
||||
### 2. 调用外部方法的void方法
|
||||
|
||||
例如,下面这个方法会根据输入打印信息到控制台:
|
||||
|
||||
@@ -69,7 +69,8 @@ class Demo {
|
||||
}
|
||||
```
|
||||
|
||||
若要测试此方法,可以利用TestableMock快速Mock掉`System.out.println`方法。在Mock方法体里可以继续执行原调用(相当于并不影响本来方法功能,仅用于做调用记录),也可以直接留空(相当于去除了原方法的副作用)。
|
||||
若要测试此方法,可以利用`TestableMock`快速Mock掉`System.out.println`方法。在Mock方法体里可以继续执行原调用(相当于并不影响本来方法功能,仅用于做调用记录),也可以直接留空(相当于去除了原方法的副作用)。
|
||||
|
||||
在执行完被测的void类型方法以后,用`InvokeVerifier.verify()`校验传入的打印内容是否符合预期:
|
||||
|
||||
```java
|
||||
@@ -77,7 +78,7 @@ class DemoTest {
|
||||
private Demo demo = new Demo();
|
||||
|
||||
// 拦截`System.out.println`调用
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
public void println(PrintStream ps, String msg) {
|
||||
// 执行原调用
|
||||
ps.println(msg);
|
||||
@@ -88,7 +89,7 @@ class DemoTest {
|
||||
Action action = new Action("click", ":download");
|
||||
demo.recordAction();
|
||||
// 验证Mock方法`println`被调用,且传入参数符合预期
|
||||
verify("println").matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download");
|
||||
verify("println").with(matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
自助问题排查
|
||||
---
|
||||
|
||||
相比Mockito等由开发者手工放置Mock类的做法,TestableMock使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也有可能发生预料之外的Mock替换。
|
||||
相比`Mockito`等由开发者手工放置Mock类的做法,`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也有可能发生预料之外的Mock替换。
|
||||
|
||||
若要排查Mock相关的问题,只需在测试类上添加`@MockWith`注解,并配置参数`diagnose`值为`MockDiagnose.ENABLE`,在运行测试时就会打印出详细的Mock方法替换过程。
|
||||
|
||||
|
||||
@@ -19,18 +19,24 @@
|
||||
</plugin>
|
||||
```
|
||||
|
||||
> 当使用`testable-maven-plugin`插件时,应该移除`maven-surefire-plugin`插件上的TestableMock相关配置,同时可以从项目的`pom.xml`文件中移除对`com.alibaba.testable:testable-agent`包的显示依赖
|
||||
> 当使用`testable-maven-plugin`插件时,应该移除`maven-surefire-plugin`插件上的TestableMock相关配置
|
||||
|
||||
`testable-maven-plugin`插件能够与Jacoco插件直接同时使用,无需额外适配,因此能使`pom.xml`文件编写起来更简单且美观。
|
||||
|
||||
但是,当通过IDE运行单个测试用例时,Mock功能会失效。
|
||||
但需要注意的是,当通过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`
|
||||
该问题可以通过额外配置IDE的测试参数绕过。以IntelliJ为例,打开运行菜单的"编辑配置..."选型,如图中位置①
|
||||
|
||||

|
||||
|
||||
在"虚拟机参数"属性值末尾添加JavaAgent启动参数:`-javaagent:${HOME}/.m2/repository/com/alibaba/testable/testable-agent/x.y.z/testable-agent-x.y.z.jar`,如图中位置②
|
||||
|
||||
> PS:请将路径中的`x.y.z`替换成实际使用的版本号
|
||||
|
||||

|
||||

|
||||
|
||||
这样实际上还是该了`maven-surefire-plugin`插件的配置,因此目前除了需要考虑美观因素的场景以外,直接在`pom.xml`文件中修改`maven-surefire-plugin`插件配置依然是更加实用的方案。
|
||||
最后点击运行单元测试,如图中位置③
|
||||
|
||||
总体来说,使用`testable-maven-plugin`带来的额外复杂性依然高于其对配置的简化作用。目前而言,直接在`pom.xml`文件中修改`maven-surefire-plugin`插件配置还是相对推荐的实用方案。
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
快速Mock被测类的任意方法调用
|
||||
---
|
||||
|
||||
相比以往Mock工具以类为粒度的Mock方式,TestableMock允许用户直接定义需要Mock的单个方法,并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测方法中的指定方法调用。
|
||||
相比以往Mock工具以类为粒度的Mock方式,`TestableMock`允许用户直接定义需要Mock的单个方法,并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测方法中的指定方法调用。
|
||||
|
||||
> 归纳起来就两条:
|
||||
> - Mock非构造方法,拷贝原方法定义到测试类,增加一个与调用者类型相同的参数,加`@MockMethod`注解
|
||||
> - Mock构造方法,拷贝原方法定义到测试类,返回值换成构造的类型,方法名随意,加`@MockContructor`注解
|
||||
|
||||
> **注意**:当前版本还有一项约定是,测试类名称应该为`被测类名+Test`,通常Java单元测试均符合这种惯例。此约定在未来的`TestableMock`版本中可能会被放宽或去除。
|
||||
|
||||
具体的Mock方法定义约定如下:
|
||||
|
||||
#### 1. 覆写任意类的方法调用
|
||||
|
||||
在测试类里定义一个有`@TestableMock`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
|
||||
在测试类里定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
|
||||
|
||||
此时被测类中所有对该需覆写方法的调用,将在单元测试运行时,将自动被替换为对上述自定义Mock方法的调用。
|
||||
|
||||
**注意**:当遇到待覆写方法有重名时,可以将需覆写的方法名写到`@TestableMock`注解的`targetMethod`参数里,这样Mock方法自身就可以随意命名了。
|
||||
**注意**:当遇到待覆写方法有重名时,可以将需覆写的方法名写到`@MockMethod`注解的`targetMethod`参数里,这样Mock方法自身就可以随意命名了。
|
||||
|
||||
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在测试类定义如下方法:
|
||||
|
||||
@@ -20,7 +26,7 @@
|
||||
// 调用此方法的对象`"anything"`类型为`String`
|
||||
// 则Mock方法签名在其参数列表首位增加一个类型为`String`的参数(名字随意)
|
||||
// 此参数可用于获得当时的实际调用者的值和上下文
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private String substring(String self, int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
@@ -31,7 +37,7 @@ private String substring(String self, int i, int j) {
|
||||
```java
|
||||
// 使用`targetMethod`指定需Mock的方法名
|
||||
// 此方法本身现在可以随意命名,但方法参数依然需要遵循相同的匹配规则
|
||||
@TestableMock(targetMethod = "substring")
|
||||
@MockMethod(targetMethod = "substring")
|
||||
private String use_any_mock_method_name(String self, int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
@@ -50,7 +56,7 @@ private String use_any_mock_method_name(String self, int i, int j) {
|
||||
```java
|
||||
// 被测类型是`DemoMock`
|
||||
// 因此在定义Mock方法时,在目标方法参数首位加一个类型为`DemoMock`的参数(名字随意)
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private String innerFunc(DemoMock self, String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
@@ -60,7 +66,7 @@ private String innerFunc(DemoMock self, String text) {
|
||||
|
||||
#### 3. 覆写任意类的静态方法
|
||||
|
||||
对于静态方法的Mock与普通方法相同。但需要注意的是,对于静态方法,传入Mock方法的第一个参数实际值始终是`null`。
|
||||
对于静态方法的Mock与普通方法相同。但需要注意的是,静态方法的Mock方法被调用时,传入的第一个参数实际值始终是`null`。
|
||||
|
||||
例如,在被测类中调用了`BlackBox`类型中的静态方法`secretBox()`,改方法签名为`BlackBox secretBox()`,则Mock方法如下:
|
||||
|
||||
@@ -68,7 +74,7 @@ private String innerFunc(DemoMock self, String text) {
|
||||
// 目标静态方法定义在`BlackBox`类型中
|
||||
// 在定义Mock方法时,在目标方法参数首位加一个类型为`BlackBox`的参数(名字随意)
|
||||
// 此参数仅用于标识目标类型,实际传入值将始终为`null`
|
||||
@TestableMock
|
||||
@MockMethod
|
||||
private BlackBox secretBox(BlackBox ignore) {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
@@ -78,7 +84,7 @@ private BlackBox secretBox(BlackBox ignore) {
|
||||
|
||||
#### 4. 覆写任意类的new操作
|
||||
|
||||
在测试类里定义一个有`@TestableMock`注解的普通方法,将注解的`targetMethod`参数写为"<init>",然后使该方法与要被创建类型的构造函数参数、返回值类型完全一致,方法名称随意。
|
||||
在测试类里定义一个有`@MockContructor`注解的普通方法,使该方法返回值类型为要被创建的对象类型,且方法参数与要Mock的构造函数参数完全一致,方法名称随意。
|
||||
|
||||
此时被测类中所有用`new`创建指定类的操作(并使用了与Mock方法参数一致的构造函数)将被替换为对该自定义方法的调用。
|
||||
|
||||
@@ -86,20 +92,23 @@ private BlackBox secretBox(BlackBox ignore) {
|
||||
|
||||
```java
|
||||
// 要覆写的构造函数签名为`BlackBox(String)`
|
||||
// 无需在Mock方法参数列表增加额外参数,由于使用了`targetMethod`参数,Mock方法的名称随意起
|
||||
// 此处的`TestableConst.CONSTRUCTOR`为`TestableMock`提供的辅助常量,值为"<init>"
|
||||
@TestableMock(targetMethod = TestableConst.CONSTRUCTOR)
|
||||
// 无需在Mock方法参数列表增加额外参数,Mock方法的名称随意起
|
||||
@MockContructor
|
||||
private BlackBox createBlackBox(String text) {
|
||||
return new BlackBox("mock_" + text);
|
||||
}
|
||||
```
|
||||
|
||||
> 也可以依然使用`@MockMethod`注解,并配置`targetMethod`参数值为`"<init>"`,其余同上。效果与使用`@MockContructor`注解相同
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_new_object()`测试用例。
|
||||
|
||||
#### 5. 识别当前测试用例和调用来源
|
||||
|
||||
在Mock方法中可以通过`TestableTool.TEST_CASE`和`TestableTool.SOURCE_METHOD`来识别**当前运行的测试用例名称**和**进入该Mock方法前的被测类方法名称**,从而区分处理不同的调用场景。
|
||||
|
||||
> 这两个字段的实现机制基于调用堆栈分析,尽管已经做了各种特殊情况的处理,但在涉及多线程的复杂场景下,依然存在误判的可能。若您发现了相关的可复现BUG,请在Github提交Issue。
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_get_source_method_name()`和`should_able_to_get_test_case_name()`测试用例。
|
||||
|
||||
#### 6. 验证Mock方法被调用的顺序和参数
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
|
||||
- 使用参考
|
||||
- [校验Mock调用](zh-cn/doc/invoke-matcher.md)
|
||||
- [常见使用问题](zh-cn/doc/frequency-asked-questions.md)
|
||||
- [常见使用问题](zh-cn/doc/frequently-asked-questions.md)
|
||||
- [自助问题排查](zh-cn/doc/troubleshooting.md)
|
||||
- [Testable Maven插件](zh-cn/doc/use-maven-plugin.md)
|
||||
|
||||
- 其他文档
|
||||
- 技术参考
|
||||
- [主流Mock工具对比](zh-cn/doc/comparation.md)
|
||||
- [Release Note](zh-cn/doc/release-note.md)
|
||||
- [问题反馈](zh-cn/doc/feedback.md)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<version>0.4.4</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.alibaba.testable.agent;
|
||||
|
||||
import com.alibaba.testable.agent.transformer.TestableClassTransformer;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import com.alibaba.testable.core.model.MockDiagnose;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
|
||||
@@ -13,8 +12,11 @@ import java.lang.instrument.Instrumentation;
|
||||
public class PreMain {
|
||||
|
||||
private static final String AND = "&";
|
||||
private static final String MUTE = "mute";
|
||||
private static final String DEBUG = "debug";
|
||||
private static final String VERBOSE = "verbose";
|
||||
private static final String LOG_LEVEL = "logLevel";
|
||||
private static final String EQUAL = "=";
|
||||
|
||||
public static void premain(String agentArgs, Instrumentation inst) {
|
||||
parseArgs(agentArgs);
|
||||
@@ -26,12 +28,31 @@ public class PreMain {
|
||||
return;
|
||||
}
|
||||
for (String a : args.split(AND)) {
|
||||
if (a.equals(DEBUG)) {
|
||||
LogUtil.setDefaultLevel(LogUtil.LEVEL_DIAGNOSE);
|
||||
} else if (a.equals(VERBOSE)) {
|
||||
LogUtil.setDefaultLevel(LogUtil.LEVEL_VERBOSE);
|
||||
int i = a.indexOf(EQUAL);
|
||||
if (i > 0) {
|
||||
String k = a.substring(0, i);
|
||||
String v = a.substring(i + 1);
|
||||
if (k.equals(LOG_LEVEL)) {
|
||||
setLogLevel(v);
|
||||
}
|
||||
} else {
|
||||
setLogLevel(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean setLogLevel(String level) {
|
||||
if (level.equals(MUTE)) {
|
||||
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_MUTE);
|
||||
return true;
|
||||
} else if (level.equals(DEBUG)) {
|
||||
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_DIAGNOSE);
|
||||
return true;
|
||||
} else if (level.equals(VERBOSE)) {
|
||||
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_VERBOSE);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,5 +14,13 @@ public class ConstPool {
|
||||
public static final String FIELD_TARGET_METHOD = "targetMethod";
|
||||
|
||||
public static final String MOCK_WITH = "com.alibaba.testable.core.annotation.MockWith";
|
||||
public static final String MOCK_METHOD = "com.alibaba.testable.core.annotation.MockMethod";
|
||||
public static final String MOCK_CONSTRUCTOR = "com.alibaba.testable.core.annotation.MockConstructor";
|
||||
public static final String TESTABLE_MOCK = "com.alibaba.testable.core.annotation.TestableMock";
|
||||
|
||||
/**
|
||||
* Name of the constructor method
|
||||
*/
|
||||
public static final String CONSTRUCTOR = "<init>";
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.model.MethodInfo;
|
||||
import com.alibaba.testable.agent.util.BytecodeUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.core.tool.TestableConst;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.*;
|
||||
@@ -40,7 +39,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
Set<MethodInfo> memberInjectMethods = new HashSet<MethodInfo>();
|
||||
Set<MethodInfo> newOperatorInjectMethods = new HashSet<MethodInfo>();
|
||||
for (MethodInfo mi : injectMethods) {
|
||||
if (mi.getName().equals(TestableConst.CONSTRUCTOR)) {
|
||||
if (mi.getName().equals(ConstPool.CONSTRUCTOR)) {
|
||||
newOperatorInjectMethods.add(mi);
|
||||
} else {
|
||||
memberInjectMethods.add(mi);
|
||||
@@ -68,8 +67,10 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
instructions = replaceMemberCallOps(cn, mn, memberInjectMethodName, instructions,
|
||||
node.owner, node.getOpcode(), rangeStart, i);
|
||||
i = rangeStart;
|
||||
} else {
|
||||
LogUtil.warn("Potential missed mocking at %s:%s", mn.name, getLineNum(instructions, i));
|
||||
}
|
||||
} else if (TestableConst.CONSTRUCTOR.equals(node.name)) {
|
||||
} else if (ConstPool.CONSTRUCTOR.equals(node.name)) {
|
||||
// it's a new operation
|
||||
String newOperatorInjectMethodName = getNewOperatorInjectMethodName(newOperatorInjectMethods, node);
|
||||
if (newOperatorInjectMethodName != null) {
|
||||
@@ -120,28 +121,12 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
}
|
||||
|
||||
private int getMemberMethodStart(AbstractInsnNode[] instructions, int rangeEnd) {
|
||||
int stackLevel = ClassUtil.getParameterTypes(((MethodInsnNode)instructions[rangeEnd]).desc).size();
|
||||
int stackLevel = getInitialStackLevel((MethodInsnNode)instructions[rangeEnd]);
|
||||
if (stackLevel < 0) {
|
||||
return rangeEnd;
|
||||
}
|
||||
for (int i = rangeEnd - 1; i >= 0; i--) {
|
||||
switch (instructions[i].getOpcode()) {
|
||||
case Opcodes.INVOKESPECIAL:
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
case Opcodes.INVOKEINTERFACE:
|
||||
stackLevel += stackEffectOfInvocation(instructions[i]) + 1;
|
||||
if (((MethodInsnNode)instructions[i]).name.equals(TestableConst.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());
|
||||
}
|
||||
stackLevel += getStackLevelChange(instructions[i]);
|
||||
if (stackLevel < 0) {
|
||||
return i;
|
||||
}
|
||||
@@ -149,6 +134,38 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int getInitialStackLevel(MethodInsnNode instruction) {
|
||||
int stackLevel = ClassUtil.getParameterTypes((instruction).desc).size();
|
||||
switch (instruction.getOpcode()) {
|
||||
case Opcodes.INVOKESPECIAL:
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
case Opcodes.INVOKEINTERFACE:
|
||||
return stackLevel;
|
||||
case Opcodes.INVOKESTATIC:
|
||||
case Opcodes.INVOKEDYNAMIC:
|
||||
return stackLevel - 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private int getStackLevelChange(AbstractInsnNode instruction) {
|
||||
switch (instruction.getOpcode()) {
|
||||
case Opcodes.INVOKESPECIAL:
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
case Opcodes.INVOKEINTERFACE:
|
||||
return stackEffectOfInvocation(instruction) + 1;
|
||||
case Opcodes.INVOKESTATIC:
|
||||
case Opcodes.INVOKEDYNAMIC:
|
||||
return stackEffectOfInvocation(instruction);
|
||||
case -1:
|
||||
// either LabelNode or LineNumberNode
|
||||
return 0;
|
||||
default:
|
||||
return -BytecodeUtil.stackEffect(instruction.getOpcode());
|
||||
}
|
||||
}
|
||||
|
||||
private int stackEffectOfInvocation(AbstractInsnNode instruction) {
|
||||
String desc = ((MethodInsnNode)instruction).desc;
|
||||
return ClassUtil.getParameterTypes(desc).size() - (ClassUtil.getReturnType(desc).isEmpty() ? 0 : 1);
|
||||
@@ -202,7 +219,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
mn.instructions.remove(instructions[end - 1]);
|
||||
}
|
||||
}
|
||||
// method with @TestableMock will be modified as public access, so INVOKEVIRTUAL is used
|
||||
// method with @MockMethod will be modified as public access, so INVOKEVIRTUAL is used
|
||||
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,
|
||||
substitutionMethod, addFirstParameter(method.desc, ClassUtil.fitCompanionClassName(ownerClass)), false));
|
||||
mn.instructions.remove(instructions[end]);
|
||||
|
||||
@@ -4,12 +4,15 @@ import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.util.AnnotationUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.core.tool.TestableConst;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
@@ -35,11 +38,19 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
*/
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
for (MethodNode m : cn.methods) {
|
||||
transformMethod(cn, m);
|
||||
Iterator<FieldNode> iterator = cn.fields.iterator();
|
||||
if (iterator.hasNext()) {
|
||||
if (ConstPool.TESTABLE_INJECT_REF.equals(iterator.next().name)) {
|
||||
// avoid duplicate injection
|
||||
LogUtil.verbose("Duplicate injection found, ignore " + cn.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
cn.fields.add(new FieldNode(ACC_PUBLIC | ACC_STATIC, ConstPool.TESTABLE_INJECT_REF,
|
||||
ClassUtil.toByteCodeClassName(cn.name), null, null));
|
||||
for (MethodNode m : cn.methods) {
|
||||
transformMethod(cn, m);
|
||||
}
|
||||
}
|
||||
|
||||
private void transformMethod(ClassNode cn, MethodNode mn) {
|
||||
@@ -56,7 +67,9 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
for (AnnotationNode n : mn.visibleAnnotations) {
|
||||
visibleAnnotationNames.add(n.desc);
|
||||
}
|
||||
if (visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.TESTABLE_MOCK))) {
|
||||
if (visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD)) ||
|
||||
visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.TESTABLE_MOCK)) ||
|
||||
visibleAnnotationNames.contains(ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR))) {
|
||||
mn.access &= ~ACC_PRIVATE;
|
||||
mn.access &= ~ACC_PROTECTED;
|
||||
mn.access |= ACC_PUBLIC;
|
||||
@@ -136,9 +149,12 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
|
||||
private boolean isMockForConstructor(MethodNode mn) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.MOCK_CONSTRUCTOR)) {
|
||||
return true;
|
||||
}
|
||||
String method = AnnotationUtil.getAnnotationParameter
|
||||
(an, ConstPool.FIELD_TARGET_METHOD, null, String.class);
|
||||
if (TestableConst.CONSTRUCTOR.equals(method)) {
|
||||
if (ConstPool.CONSTRUCTOR.equals(method)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.alibaba.testable.agent.model;
|
||||
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
|
||||
/**
|
||||
* Record parameter fetch from @MockWith annotation
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
public class CachedMockParameter {
|
||||
|
||||
private final boolean classExist;
|
||||
private final AnnotationNode mockWith;
|
||||
|
||||
private CachedMockParameter(boolean classExist, AnnotationNode mockWith) {
|
||||
this.classExist = classExist;
|
||||
this.mockWith = mockWith;
|
||||
}
|
||||
|
||||
public static CachedMockParameter notExist() {
|
||||
return new CachedMockParameter(false, null);
|
||||
}
|
||||
|
||||
public static CachedMockParameter exist() {
|
||||
return new CachedMockParameter(true, null);
|
||||
}
|
||||
|
||||
public static CachedMockParameter exist(AnnotationNode mockWith) {
|
||||
return new CachedMockParameter(true, mockWith);
|
||||
}
|
||||
|
||||
public boolean isClassExist() {
|
||||
return classExist;
|
||||
}
|
||||
|
||||
public AnnotationNode getMockWith() {
|
||||
return mockWith;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,10 @@ 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.core.tool.TestableConst;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import com.alibaba.testable.core.model.MockDiagnose;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
@@ -30,8 +27,6 @@ import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassNa
|
||||
public class TestableClassTransformer implements ClassFileTransformer {
|
||||
|
||||
private static final String FIELD_DIAGNOSE = "diagnose";
|
||||
private final Map<ComparableWeakRef<String>, CachedMockParameter> loadedClass =
|
||||
new WeakHashMap<ComparableWeakRef<String>, CachedMockParameter>();
|
||||
|
||||
/**
|
||||
* Just avoid spend time to scan those surely non-user classes
|
||||
@@ -44,7 +39,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
|
||||
ProtectionDomain protectionDomain, byte[] classFileBuffer) {
|
||||
if (isSystemClass(className) || loadedClass.containsKey(new ComparableWeakRef<String>(className))) {
|
||||
if (isSystemClass(className)) {
|
||||
// Ignore system class and reloaded class
|
||||
LogUtil.verbose("Ignore class: " + (className == null ? "<lambda>" : className));
|
||||
return null;
|
||||
@@ -117,32 +112,40 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
return;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_MOCK)) {
|
||||
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
|
||||
String fullClassName = toDotSeparateFullClassName(an.desc);
|
||||
if (fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
|
||||
addMockConstructor(cn, methodInfos, mn);
|
||||
} else if (fullClassName.equals(ConstPool.MOCK_METHOD) ||
|
||||
fullClassName.equals(ConstPool.TESTABLE_MOCK)) {
|
||||
String targetMethod = AnnotationUtil.getAnnotationParameter(
|
||||
an, ConstPool.FIELD_TARGET_METHOD, mn.name, String.class);
|
||||
if (targetMethod.equals(TestableConst.CONSTRUCTOR)) {
|
||||
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
|
||||
methodInfos.add(new MethodInfo(sourceClassName, targetMethod, mn.name, mn.desc));
|
||||
if (targetMethod.equals(ConstPool.CONSTRUCTOR)) {
|
||||
addMockConstructor(cn, methodInfos, mn);
|
||||
} else {
|
||||
methodInfos.add(new MethodInfo(targetClass, targetMethod, mn.name, methodDescPair.right));
|
||||
addMockMethod(methodInfos, mn, methodDescPair, targetMethod);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addMockMethod(List<MethodInfo> methodInfos, MethodNode mn,
|
||||
ImmutablePair<String, String> methodDescPair, String targetMethod) {
|
||||
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
|
||||
methodInfos.add(new MethodInfo(targetClass, targetMethod, mn.name, methodDescPair.right));
|
||||
}
|
||||
|
||||
private void addMockConstructor(ClassNode cn, List<MethodInfo> methodInfos, MethodNode mn) {
|
||||
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
|
||||
methodInfos.add(new MethodInfo(sourceClassName, ConstPool.CONSTRUCTOR, mn.name, mn.desc));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether any method in specified class has specified annotation
|
||||
* @param className class that need to explore
|
||||
* @return found annotation or not
|
||||
*/
|
||||
private boolean hasMockAnnotation(String className) {
|
||||
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);
|
||||
@@ -150,7 +153,6 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -158,8 +160,10 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
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());
|
||||
String fullClassName = toDotSeparateFullClassName(an.desc);
|
||||
if (fullClassName.equals(ConstPool.MOCK_METHOD) ||
|
||||
fullClassName.equals(ConstPool.TESTABLE_MOCK) ||
|
||||
fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -169,7 +173,6 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
// Usually class not found, return without record
|
||||
return false;
|
||||
}
|
||||
loadedClass.put(new ComparableWeakRef<String>(className), CachedMockParameter.notExist());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.tool.ComparableWeakRef;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<version>0.4.4</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-all</artifactId>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<version>0.4.4</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-core</artifactId>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.alibaba.testable.core.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Mark method as mock constructor
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
public @interface MockConstructor {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.alibaba.testable.core.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Mark method as mock method
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
public @interface MockMethod {
|
||||
|
||||
/**
|
||||
* mock specified method instead of method with same name
|
||||
* @return target method name
|
||||
*/
|
||||
String targetMethod() default "";
|
||||
|
||||
}
|
||||
@@ -3,13 +3,15 @@ package com.alibaba.testable.core.annotation;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Use marked method to replace the ones in source class
|
||||
* Mark method as mock method
|
||||
* @deprecated will be remove in v0.5.0, use @MockMethod or @MockConstructor instead
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
@Deprecated
|
||||
public @interface TestableMock {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.alibaba.testable.core.tool;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class TestableConst {
|
||||
|
||||
/**
|
||||
* Name of the constructor method
|
||||
*/
|
||||
public static final String CONSTRUCTOR = "<init>";
|
||||
|
||||
}
|
||||
@@ -5,43 +5,62 @@ package com.alibaba.testable.core.util;
|
||||
*/
|
||||
public class LogUtil {
|
||||
|
||||
public static final int LEVEL_ERROR = 0;
|
||||
public static final int LEVEL_WARN = 1;
|
||||
public static final int LEVEL_DIAGNOSE = 2;
|
||||
public static final int LEVEL_VERBOSE = 3;
|
||||
public enum LogLevel {
|
||||
/**
|
||||
* Mute
|
||||
*/
|
||||
LEVEL_MUTE(0),
|
||||
/**
|
||||
* Warn only
|
||||
*/
|
||||
LEVEL_WARN(1),
|
||||
/**
|
||||
* Show diagnose messages
|
||||
*/
|
||||
LEVEL_DIAGNOSE(2),
|
||||
/**
|
||||
* Show detail progress logs
|
||||
*/
|
||||
LEVEL_VERBOSE(3);
|
||||
|
||||
private static int defaultLogLevel = LEVEL_WARN;
|
||||
private static int level;
|
||||
int level;
|
||||
LogLevel(int l) {
|
||||
level = l;
|
||||
}
|
||||
}
|
||||
|
||||
private static LogLevel defaultLogLevel = LogLevel.LEVEL_WARN;
|
||||
private static LogLevel currentLogLevel = LogLevel.LEVEL_WARN;
|
||||
|
||||
public static void verbose(String msg, Object... args) {
|
||||
if (level >= LEVEL_VERBOSE) {
|
||||
if (currentLogLevel.level >= LogLevel.LEVEL_VERBOSE.level) {
|
||||
System.out.println(String.format("[VERBOSE] " + msg, args));
|
||||
}
|
||||
}
|
||||
|
||||
public static void diagnose(String msg, Object... args) {
|
||||
if (level >= LEVEL_DIAGNOSE) {
|
||||
if (currentLogLevel.level >= LogLevel.LEVEL_DIAGNOSE.level) {
|
||||
System.out.println(String.format("[DIAGNOSE] " + msg, args));
|
||||
}
|
||||
}
|
||||
|
||||
public static void warn(String msg, Object... args) {
|
||||
if (level >= LEVEL_WARN) {
|
||||
if (currentLogLevel.level >= LogLevel.LEVEL_WARN.level) {
|
||||
System.out.println(String.format("[WARN] " + msg, args));
|
||||
}
|
||||
}
|
||||
|
||||
public static void enableDiagnose(boolean enable) {
|
||||
level = enable ? LEVEL_DIAGNOSE : LEVEL_ERROR;
|
||||
currentLogLevel = enable ? LogLevel.LEVEL_DIAGNOSE : LogLevel.LEVEL_MUTE;
|
||||
}
|
||||
|
||||
public static void setDefaultLevel(int level) {
|
||||
public static void setDefaultLevel(LogLevel level) {
|
||||
defaultLogLevel = level;
|
||||
resetLogLevel();
|
||||
}
|
||||
|
||||
public static void resetLogLevel() {
|
||||
level = defaultLogLevel;
|
||||
currentLogLevel = defaultLogLevel;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -49,8 +49,7 @@ public class TypeUtil {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < classesLeft.length; i++) {
|
||||
if (!classesLeft[i].equals(classesRight[i]) &&
|
||||
!fuzzyEqual(classesLeft[i], classesRight[i])) {
|
||||
if (!classesLeft[i].isAssignableFrom(classesRight[i]) && !fuzzyEqual(classesLeft[i], classesRight[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<version>0.4.4</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
|
||||
@@ -24,13 +24,21 @@ public class TestableMojo extends AbstractMojo
|
||||
/**
|
||||
* Maven project.
|
||||
*/
|
||||
@Parameter(property = "project", readonly = true)
|
||||
@Parameter(property = "project", required = true, readonly = true)
|
||||
private MavenProject project;
|
||||
|
||||
/**
|
||||
* Map of plugin artifacts.
|
||||
*/
|
||||
@Parameter(property = "plugin.artifactMap", required = true, readonly = true)
|
||||
private Map<String, Artifact> pluginArtifactMap;
|
||||
|
||||
/**
|
||||
* JavaAgent log level (mute/debug/verbose)
|
||||
*/
|
||||
@Parameter
|
||||
private String logLevel;
|
||||
|
||||
/**
|
||||
* Name of the Testable Agent artifact.
|
||||
*/
|
||||
@@ -57,9 +65,16 @@ public class TestableMojo extends AbstractMojo
|
||||
getLog().error("failed to fetch project properties");
|
||||
return;
|
||||
}
|
||||
String extraArgs = "";
|
||||
if (logLevel != null && !logLevel.isEmpty()) {
|
||||
extraArgs += logLevel;
|
||||
}
|
||||
final String oldArgs = projectProperties.getProperty(testArgsPropertyKey);
|
||||
final String newArgs = (oldArgs == null) ? getAgentJarArgs().trim() : (oldArgs + getAgentJarArgs());
|
||||
String newArgs = (oldArgs == null) ? getAgentJarArgs().trim() : (oldArgs + getAgentJarArgs());
|
||||
getLog().info(testArgsPropertyKey + " set to " + newArgs);
|
||||
if (!extraArgs.isEmpty()) {
|
||||
newArgs += ("=" + extraArgs);
|
||||
}
|
||||
projectProperties.setProperty(testArgsPropertyKey, newArgs);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<version>0.4.4</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>testable-parent</name>
|
||||
<description>Unit test enhancement toolkit</description>
|
||||
@@ -42,7 +42,7 @@
|
||||
<plugin.gpg.version>1.6</plugin.gpg.version>
|
||||
<plugin.staging.version>1.6.8</plugin.staging.version>
|
||||
<plugin.maven.version>3.6.0</plugin.maven.version>
|
||||
<testable.version>0.4.0</testable.version>
|
||||
<testable.version>0.4.4</testable.version>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<version>0.4.4</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-processor</artifactId>
|
||||
|
||||
@@ -37,4 +37,32 @@ public class PrivateAccessStatementGenerator extends BaseGenerator {
|
||||
params.addAll(expr.args);
|
||||
return cx.treeMaker.Apply(List.<JCExpression>nil(), invoker, params.toList());
|
||||
}
|
||||
|
||||
public JCExpression fetchStaticGetterStatement(JCFieldAccess access) {
|
||||
JCFieldAccess getter = cx.treeMaker.Select(nameToExpression(ConstPool.TESTABLE_PRIVATE_ACCESSOR),
|
||||
cx.names.fromString("getStatic"));
|
||||
JCExpression classField = cx.treeMaker.Select(access.selected, cx.names.fromString("class"));
|
||||
return cx.treeMaker.Apply(List.<JCExpression>nil(), getter, List.of(classField,
|
||||
cx.treeMaker.Literal(access.name.toString())));
|
||||
}
|
||||
|
||||
public JCExpression fetchStaticSetterStatement(JCAssign assign) {
|
||||
JCFieldAccess setter = cx.treeMaker.Select(nameToExpression(ConstPool.TESTABLE_PRIVATE_ACCESSOR),
|
||||
cx.names.fromString("setStatic"));
|
||||
JCExpression selected = ((JCFieldAccess)assign.lhs).selected;
|
||||
JCExpression classField = cx.treeMaker.Select(selected, cx.names.fromString("class"));
|
||||
return cx.treeMaker.Apply(List.<JCExpression>nil(), setter, List.of(classField,
|
||||
cx.treeMaker.Literal(((JCFieldAccess)assign.lhs).name.toString()), assign.rhs));
|
||||
}
|
||||
|
||||
public JCExpression fetchStaticInvokeStatement(JCMethodInvocation expr) {
|
||||
JCFieldAccess invoker = cx.treeMaker.Select(nameToExpression(ConstPool.TESTABLE_PRIVATE_ACCESSOR),
|
||||
cx.names.fromString("invokeStatic"));
|
||||
JCExpression selected = ((JCFieldAccess)expr.meth).selected;
|
||||
JCExpression classField = cx.treeMaker.Select(selected, cx.names.fromString("class"));
|
||||
ListBuffer<JCExpression> params = ListBuffer.of(classField);
|
||||
params.add(cx.treeMaker.Literal(((JCFieldAccess)expr.meth).name.toString()));
|
||||
params.addAll(expr.args);
|
||||
return cx.treeMaker.Apply(List.<JCExpression>nil(), invoker, params.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.alibaba.testable.processor.model;
|
||||
|
||||
public enum MemberType {
|
||||
|
||||
/**
|
||||
* Private member or final member
|
||||
*/
|
||||
PRIVATE_OR_FINAL,
|
||||
|
||||
/**
|
||||
* Static private member or Static final member
|
||||
*/
|
||||
STATIC_PRIVATE,
|
||||
|
||||
/**
|
||||
* None private member
|
||||
*/
|
||||
NONE_PRIVATE
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.alibaba.testable.processor.translator;
|
||||
|
||||
import com.alibaba.testable.processor.constant.ConstPool;
|
||||
import com.alibaba.testable.processor.generator.PrivateAccessStatementGenerator;
|
||||
import com.alibaba.testable.processor.model.MemberType;
|
||||
import com.alibaba.testable.processor.model.TestableContext;
|
||||
import com.sun.tools.javac.tree.JCTree.*;
|
||||
import com.sun.tools.javac.util.ListBuffer;
|
||||
@@ -23,7 +24,7 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
/**
|
||||
* Name of source class
|
||||
*/
|
||||
private final String sourceClassName;
|
||||
private final Name sourceClassName;
|
||||
/**
|
||||
* Fields of source class instance in the test class
|
||||
*/
|
||||
@@ -40,11 +41,12 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
private final PrivateAccessStatementGenerator privateAccessStatementGenerator;
|
||||
|
||||
public EnablePrivateAccessTranslator(String pkgName, String testClassName, TestableContext cx) {
|
||||
this.sourceClassName = testClassName.substring(0, testClassName.length() - ConstPool.TEST_POSTFIX.length());
|
||||
String sourceClass = testClassName.substring(0, testClassName.length() - ConstPool.TEST_POSTFIX.length());
|
||||
this.privateAccessStatementGenerator = new PrivateAccessStatementGenerator(cx);
|
||||
this.sourceClassName = cx.names.fromString(sourceClass);
|
||||
try {
|
||||
Class<?> cls = null;
|
||||
String sourceClassFullName = pkgName + "." + sourceClassName;
|
||||
String sourceClassFullName = pkgName + "." + sourceClass;
|
||||
try {
|
||||
cls = Class.forName(sourceClassFullName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
@@ -77,7 +79,7 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
public void visitVarDef(JCVariableDecl jcVariableDecl) {
|
||||
super.visitVarDef(jcVariableDecl);
|
||||
if (jcVariableDecl.vartype.getClass().equals(JCIdent.class) &&
|
||||
((JCIdent)jcVariableDecl.vartype).name.toString().equals(sourceClassName)) {
|
||||
((JCIdent)jcVariableDecl.vartype).name.equals(sourceClassName)) {
|
||||
sourceClassIns.add(jcVariableDecl.name);
|
||||
}
|
||||
}
|
||||
@@ -89,10 +91,15 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
@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);
|
||||
if (jcExpressionStatement.expr.getClass().equals(JCAssign.class)) {
|
||||
MemberType memberType = checkSetterType((JCAssign)jcExpressionStatement.expr);
|
||||
if (memberType.equals(MemberType.PRIVATE_OR_FINAL)) {
|
||||
jcExpressionStatement.expr = privateAccessStatementGenerator.fetchSetterStatement(
|
||||
(JCAssign)jcExpressionStatement.expr);
|
||||
} else if (memberType.equals(MemberType.STATIC_PRIVATE)) {
|
||||
jcExpressionStatement.expr = privateAccessStatementGenerator.fetchStaticSetterStatement(
|
||||
(JCAssign)jcExpressionStatement.expr);
|
||||
}
|
||||
}
|
||||
// visitExec could be an invoke
|
||||
jcExpressionStatement.expr = checkAndExchange(jcExpressionStatement.expr);
|
||||
@@ -129,36 +136,59 @@ 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);
|
||||
if (expr.getClass().equals(JCFieldAccess.class)) {
|
||||
MemberType memberType = checkGetterType((JCFieldAccess)expr);
|
||||
if (memberType.equals(MemberType.PRIVATE_OR_FINAL)) {
|
||||
expr = privateAccessStatementGenerator.fetchGetterStatement((JCFieldAccess)expr);
|
||||
} else if (memberType.equals(MemberType.STATIC_PRIVATE)) {
|
||||
expr = privateAccessStatementGenerator.fetchStaticGetterStatement((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);
|
||||
if (expr.getClass().equals(JCMethodInvocation.class)) {
|
||||
MemberType memberType = checkInvokeType((JCMethodInvocation)expr);
|
||||
if (memberType.equals(MemberType.PRIVATE_OR_FINAL)) {
|
||||
expr = privateAccessStatementGenerator.fetchInvokeStatement((JCMethodInvocation)expr);
|
||||
} else if (memberType.equals(MemberType.STATIC_PRIVATE)) {
|
||||
expr = privateAccessStatementGenerator.fetchStaticInvokeStatement((JCMethodInvocation)expr);
|
||||
}
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
private boolean isPrivateField(JCFieldAccess access) {
|
||||
return access.selected.getClass().equals(JCIdent.class) &&
|
||||
sourceClassIns.contains(((JCIdent)access.selected).name) &&
|
||||
privateOrFinalFields.contains(access.name.toString());
|
||||
private MemberType checkGetterType(JCFieldAccess access) {
|
||||
if (access.selected.getClass().equals(JCIdent.class) &&
|
||||
privateOrFinalFields.contains(access.name.toString())) {
|
||||
return checkSourceClassOrIns(((JCIdent)access.selected).name);
|
||||
}
|
||||
return MemberType.NONE_PRIVATE;
|
||||
}
|
||||
|
||||
private boolean isPrivateField(JCAssign assign) {
|
||||
return assign.lhs.getClass().equals(JCFieldAccess.class) &&
|
||||
private MemberType checkSetterType(JCAssign assign) {
|
||||
if (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());
|
||||
privateOrFinalFields.contains(((JCFieldAccess)(assign).lhs).name.toString())) {
|
||||
return checkSourceClassOrIns(((JCIdent)((JCFieldAccess)(assign).lhs).selected).name);
|
||||
}
|
||||
return MemberType.NONE_PRIVATE;
|
||||
}
|
||||
|
||||
private boolean isPrivateMethod(JCMethodInvocation expr) {
|
||||
return expr.meth.getClass().equals(JCFieldAccess.class) &&
|
||||
private MemberType checkInvokeType(JCMethodInvocation expr) {
|
||||
if (expr.meth.getClass().equals(JCFieldAccess.class) &&
|
||||
((JCFieldAccess)(expr).meth).selected.getClass().equals(JCIdent.class) &&
|
||||
sourceClassIns.contains(((JCIdent)((JCFieldAccess)(expr).meth).selected).name) &&
|
||||
privateMethods.contains(((JCFieldAccess)(expr).meth).name.toString());
|
||||
privateMethods.contains(((JCFieldAccess)(expr).meth).name.toString())) {
|
||||
return checkSourceClassOrIns(((JCIdent)((JCFieldAccess)(expr).meth).selected).name);
|
||||
}
|
||||
return MemberType.NONE_PRIVATE;
|
||||
}
|
||||
|
||||
private MemberType checkSourceClassOrIns(Name name) {
|
||||
if (sourceClassName.equals(name)) {
|
||||
return MemberType.STATIC_PRIVATE;
|
||||
} else if (sourceClassIns.contains(name)) {
|
||||
return MemberType.PRIVATE_OR_FINAL;
|
||||
}
|
||||
return MemberType.NONE_PRIVATE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user