mirror of
https://github.com/alibaba/testable-mock.git
synced 2026-08-19 17:53:29 +08:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9924d7d2fc | ||
|
|
9f5867ed76 | ||
|
|
be0a580a04 | ||
|
|
78d7ee21a6 | ||
|
|
af409a65a8 | ||
|
|
a67619d574 | ||
|
|
b89daa22c3 | ||
|
|
e948e625ea | ||
|
|
d463f863db | ||
|
|
31140bef87 | ||
|
|
d2944a88fb | ||
|
|
9d6e4dd1fa | ||
|
|
9c09bf9653 | ||
|
|
6d87653fa2 | ||
|
|
eec88f4bd9 | ||
|
|
5921ccab07 | ||
|
|
8dde402a8c | ||
|
|
d10478f95f | ||
|
|
0c063e9b1f | ||
|
|
b8771f0dac | ||
|
|
42dd58584b | ||
|
|
ce55bb4c9a | ||
|
|
cff2d1ae5a | ||
|
|
5904ad99cb | ||
|
|
74fbc5b53f | ||
|
|
f6b5bab250 | ||
|
|
f2feb3cc68 | ||
|
|
b41923bfa0 | ||
|
|
7c1f18de46 | ||
|
|
c393d40d0d | ||
|
|
d4a69528a7 | ||
|
|
a46e73a421 | ||
|
|
01e9890234 | ||
|
|
93cdef1d6c | ||
|
|
e2e986e50d | ||
|
|
f5475aa18e | ||
|
|
844323af3a | ||
|
|
cc2deeff1c | ||
|
|
14e4d2db5b | ||
|
|
c9c8ba6c24 | ||
|
|
b5c0afb3da | ||
|
|
617d372545 | ||
|
|
58da25de53 | ||
|
|
46706ba56c | ||
|
|
1ced04bf29 | ||
|
|
7b2f967626 | ||
|
|
e0a4a39d55 | ||
|
|
b4da23a90e | ||
|
|
c1659242b6 | ||
|
|
929abffb66 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# maven ignore
|
||||
target/
|
||||
.mvn/
|
||||
|
||||
# gradle ignore
|
||||
build/
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<font size="5">**0.6版本已发布**</font>,从`0.5.x`升级到`0.6.x`版本请参考[版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-guide)
|
||||
|
||||
如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将在24小时内回复并处理
|
||||
如果有遇到其他任何使用问题和建议,请直接在[Issues](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将在24小时内回复并处理
|
||||
|
||||
-----
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|-- testable-agent ➜ JavaAgent模块,提供Mock测试相关功能
|
||||
|-- testable-core ➜ 基础功能模块,提供Mock相关注解和工具类
|
||||
|-- testable-maven-plugin ➜ Maven插件模块,用于简化JavaAgent注入
|
||||
|-- tool ➜ 项目开发过程中的工具脚本
|
||||
|-- demo
|
||||
| |-- java-demo ➜ Java语言的示例代码
|
||||
| `-- kotlin-demo ➜ Kotlin语言的示例代码
|
||||
|
||||
@@ -24,6 +24,7 @@ Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
|
||||
|-- 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
|
||||
|-- tool ➜ Scripts for project maintain
|
||||
|-- demo
|
||||
| |-- java-demo ➜ Java code example
|
||||
| `-- kotlin-demo ➜ Kotlin code example
|
||||
|
||||
@@ -13,8 +13,8 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.4')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.4')
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
|
||||
@@ -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.6.0</testable.version>
|
||||
<testable.version>0.6.4</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
1
demo/java-demo/src/test/resources/testable.properties
Normal file
1
demo/java-demo/src/test/resources/testable.properties
Normal file
@@ -0,0 +1 @@
|
||||
omni.constructor.enhance.enable = 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.6.0")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.6.0")
|
||||
testImplementation("com.alibaba.testable:testable-all:0.6.4")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.6.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.6.0</testable.version>
|
||||
<testable.version>0.6.4</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
rootProject.name = "demo"
|
||||
@@ -3,7 +3,7 @@ package com.alibaba.demo.java2kotlin
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
object PathUtil {
|
||||
object PathDemo {
|
||||
|
||||
fun deleteRecursively(file: File) {
|
||||
if (!file.exists()) {
|
||||
@@ -5,7 +5,7 @@ import com.alibaba.testable.core.matcher.InvokeVerifier.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
|
||||
class PathUtilTest {
|
||||
class PathDemoTest {
|
||||
|
||||
class Mock {
|
||||
@MockMethod
|
||||
@@ -42,7 +42,7 @@ class PathUtilTest {
|
||||
|
||||
@Test
|
||||
fun should_mock_java_method_invoke_in_kotlin() {
|
||||
PathUtil.deleteRecursively(File("/a/b/"))
|
||||
PathDemo.deleteRecursively(File("/a/b/"))
|
||||
verify("listFiles").withTimes(2)
|
||||
verify("delete").withTimes(4)
|
||||
}
|
||||
1
demo/kotlin-demo/src/test/resources/testable.properties
Normal file
1
demo/kotlin-demo/src/test/resources/testable.properties
Normal file
@@ -0,0 +1 @@
|
||||
omni.constructor.enhance.enable = true
|
||||
@@ -12,6 +12,7 @@
|
||||
<modules>
|
||||
<module>java-demo</module>
|
||||
<module>kotlin-demo</module>
|
||||
<module>spock-demo</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
|
||||
5
demo/settings.gradle
Normal file
5
demo/settings.gradle
Normal file
@@ -0,0 +1,5 @@
|
||||
rootProject.name = 'demos'
|
||||
|
||||
include 'java-demo'
|
||||
include 'kotlin-demo'
|
||||
include 'spock-demo'
|
||||
28
demo/spock-demo/build.gradle
Normal file
28
demo/spock-demo/build.gradle
Normal file
@@ -0,0 +1,28 @@
|
||||
plugins {
|
||||
id 'groovy'
|
||||
}
|
||||
|
||||
group = 'com.alibaba.testable'
|
||||
version = '1.0.0-SNAPSHOT'
|
||||
sourceCompatibility = '8'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'org.codehaus.groovy:groovy-all:3.0.7'
|
||||
testImplementation 'org.spockframework:spock-core:2.0-M5-groovy-3.0'
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.4')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.4')
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
test {
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
useJUnitPlatform()
|
||||
}
|
||||
86
demo/spock-demo/pom.xml
Normal file
86
demo/spock-demo/pom.xml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Refer to: https://github.com/spockframework/spock-example/blob/master/pom.xml -->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>spock-demo</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>spock-demo</name>
|
||||
<description>Spock demo for Testable</description>
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<testable.version>0.6.4</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.spockframework</groupId>
|
||||
<artifactId>spock-bom</artifactId>
|
||||
<version>2.0-M5-groovy-3.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.groovy</groupId>
|
||||
<artifactId>groovy</artifactId>
|
||||
<version>3.0.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.spockframework</groupId>
|
||||
<artifactId>spock-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.spockframework</groupId>
|
||||
<artifactId>spock-junit4</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<!-- The gmavenplus plugin is used to compile Groovy code -->
|
||||
<groupId>org.codehaus.gmavenplus</groupId>
|
||||
<artifactId>gmavenplus-plugin</artifactId>
|
||||
<version>1.12.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
<goal>compileTests</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<configuration>
|
||||
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
<useFile>false</useFile>
|
||||
<includes>
|
||||
<include>**/*Test.java</include>
|
||||
<include>**/*Spec.java</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.github.pbetkier.spockdemo;
|
||||
|
||||
import com.github.pbetkier.spockdemo.model.SpockBox;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DemoSpock {
|
||||
|
||||
public SpockBox createBoxOfNum() {
|
||||
SpockBox box = new SpockBox();
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
box.put(String.valueOf(i));
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.github.pbetkier.spockdemo.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SpockBox {
|
||||
|
||||
private List<String> contents = new ArrayList<>();
|
||||
|
||||
public int size() {
|
||||
return contents.size();
|
||||
}
|
||||
|
||||
public void put(String data) {
|
||||
contents.add(data);
|
||||
}
|
||||
|
||||
public String pop() {
|
||||
if (contents.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String data = contents.get(size() - 1);
|
||||
contents.remove(size() - 1);
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.github.pbetkier.spockdemo
|
||||
|
||||
import com.alibaba.testable.core.annotation.MockConstructor
|
||||
import com.alibaba.testable.core.annotation.MockMethod
|
||||
import com.github.pbetkier.spockdemo.model.SpockBox
|
||||
import spock.lang.Shared
|
||||
import spock.lang.Specification
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
|
||||
class DemoSpockTest extends Specification {
|
||||
|
||||
@Shared
|
||||
def demoSpock = new DemoSpock()
|
||||
|
||||
static class Mock {
|
||||
@MockConstructor
|
||||
SpockBox createBox() {
|
||||
SpockBox box = new SpockBox()
|
||||
box.put("mock zero")
|
||||
return box
|
||||
}
|
||||
|
||||
@MockMethod(targetMethod = "put")
|
||||
void putBox(SpockBox self, String data) {
|
||||
self.put("mock " + data)
|
||||
}
|
||||
}
|
||||
|
||||
def "should get a box of numbers"() {
|
||||
given:
|
||||
def box = demoSpock.createBoxOfNum()
|
||||
|
||||
expect:
|
||||
box.size() == 4
|
||||
box.pop() == "mock 3"
|
||||
box.pop() == "mock 2"
|
||||
box.pop() == "mock 1"
|
||||
box.pop() == "mock zero"
|
||||
verify("createBox").withTimes(1)
|
||||
verify("putBox").withInOrder("1").withInOrder("2").withInOrder("3")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,29 @@
|
||||
How Mock Works
|
||||
---
|
||||
|
||||
TO BE TRANSLATED
|
||||
This document mainly introduces the design and implementation principles of the mock function in `TestableMock`.
|
||||
|
||||
Unlike common mock-tools which write mock definitions in each test case, `TestableMock` allows each business class to provide its own set of mock methods, describing what should be mocked during testing, and define the corresponding replacement logic (that is, each business class has its own independent "Test class" and independent "Mock class"). By adopting "convention is better than configuration" principle, `TestableMock` not only reduce redundant codes but also reduce the cost of mock learning.
|
||||
|
||||
This design is based on two basic assumptions:
|
||||
|
||||
1. In any business class, methods that need to be mocked in one test case are usually required to be mocked in other test cases. For these mocked methods usually requiring external dependencies that are not easy to test.
|
||||
2. Each unit test only focuses on the logic inside the unit under test, and irrelevant calls outside the unit should be replaced with mocks. That is, the invocations that need to be mocked should all be in the code of the class under test.
|
||||
|
||||
Accordingly, the unit test scenarios that meet the above assumptions are simplified through conventions, and the remaining more complex use scenarios are supported through configuration.
|
||||
|
||||
The mechanism of `TestableMock` can be summarized in one sentence: <u>Using java agent to dynamically modify the bytecode, before the unit test is about to run, replace all invocations in the business class under test which match the mock method definition with invocations to the mock method itself.</u>.
|
||||
|
||||
The final effect is that no matter what service framework or object-container the code uses, no matter whether the object of mock target is injected by a framework, created by new operation, and whether the target method of mock is private or external, defined as global, local, static, inherited or overloaded, all can be mocked in a same and simple way, which make unit testing much easier.
|
||||
|
||||
> Notice: Mock's goal is the method invocation in the class under test. The code inside the test case will not be mocked, and the method definition itself has not changed, but the invocation code to those methods will be replaced.
|
||||
|
||||
Specifically, when the unit test is started, `TestableMock` will preprocess the classes loaded into memory and establish the association relationship between the "class under test", the "test class", and the "mock container class" respectively (can be one-to-one or many-to-one). On the one hand, this association is to correctly match the mock call and replace it before the test case is executed, and on the other hand, it is used to control the effective scope of the mock method.
|
||||
|
||||
For the class under test, replace the matched call with a call to the mock container method.
|
||||
|
||||
For the test class, insert the mock context initialization code at the beginning of each test case.
|
||||
|
||||
For the mock container class, add the `testableIns()` method to make the class become a singleton class, and insert codes to record the call at the beginning of each Mock method.
|
||||
|
||||
The above is the core logic of the entire mocking logic. For more implementation details, please refer to the source code. If you have any questions, suggestions, or improvement proposals, you are welcome to participate in the discussion and contribute through Github Issue and Pull Request 😃
|
||||
|
||||
@@ -11,7 +11,15 @@ Create the class under test object directly, and then use the ability of `Testab
|
||||
|
||||
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?
|
||||
#### 3. Which test frameworks does `TestableMock` support?
|
||||
|
||||
The `PrivateAccessor` / `OmniConstructor` / `OmniAccessor` and basic functionality of `TestableMock` are theoretically test-frameworks-independent, and should work with any test frameworks out-of-box.
|
||||
|
||||
The only test-framework-related part is the mock invocation verifier, which currently supports `JUnit 4` / `JUnit 5` / `TestNG` and `Spock` appropriately.
|
||||
|
||||
If you encounter compatibility problem under a specific test framework, or want to add mock invocation verifier support for any other frameworks, please raise an [issue](https://github.com/alibaba/testable-mock/issues) to tell us.
|
||||
|
||||
#### 4. 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.
|
||||
|
||||
@@ -21,7 +29,7 @@ Therefore, regardless of whether the actually called method comes from the paren
|
||||
|
||||
See the use case of the `DemoInheritTest` test class in the Java and Kotlin examples.
|
||||
|
||||
#### 4. How to mock generic methods (template methods)?
|
||||
#### 5. How to mock generic methods (template methods)?
|
||||
|
||||
Just use the same generic parameters directly on the mock method.
|
||||
|
||||
@@ -29,25 +37,25 @@ See the use case of the `DemoTemplateTest` test class in the Java and Kotlin exa
|
||||
|
||||
> 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. How to mock invocation inside an inner class?
|
||||
#### 6. How to mock invocation inside an inner class?
|
||||
|
||||
Just put mock methods in the mock container class of its outer class, it works for code in all inner classes.
|
||||
|
||||
See the use case of the `DemoInnerClass` test class in the Java and Kotlin examples.
|
||||
|
||||
#### 6. Why mocking methods in the `String` class in the Kotlin project does not work?
|
||||
#### 7. 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.
|
||||
|
||||
#### 7. Can `TestableMock` be used for testing Android projects?
|
||||
#### 8. 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.
|
||||
|
||||
#### 8. Meet "Command Line is too Long. Shorten command line for ..." error when triggering test in IntelliJ IDE?
|
||||
#### 9. Meet "Command Line is too Long. Shorten command line for ..." error when triggering test in IntelliJ IDE?
|
||||
|
||||
This problem is caused by the system `Class Path` content is too long, and has nothing to do with `TestableMock`. However, it should be noted that IntelliJ provides two auxiliary solutions: `JAR manifest` and `classpath file`. If `TestableMock` is used in the test, please select `JAR manifest`.
|
||||
|
||||
|
||||
@@ -1,6 +1,117 @@
|
||||
Parameter constructor
|
||||
Parameter Constructor
|
||||
---
|
||||
|
||||
No matter how intricate the parameter structure required by the method under test is, even there is no suitable construction method, or even there are private internal class objects... Call `TestableMock`, the parameter object will be handed to you immediately~
|
||||
In unit testing, the preparation and construction of test data is a necessary and tedious task. Object-oriented layer-by-layer encapsulation becomes an obstacle to initializing the state of the object during testing. Especially when the type structure is complicated, there is no suitable construction method, or some fields need to use private inner classes, etc. Using conventional methods to construct those class often appear to be inadequate.
|
||||
|
||||
This feature is planned to be released in the `0.6` version.
|
||||
For this reason, `TestableMock` provides two minimalist tool classes, `OmniConstructor` and `OmniAccessor`, which makes the construction of any object no longer difficult.
|
||||
|
||||
### 1. Construct any object with one line of code
|
||||
|
||||
No matter how special the target type is, `OmniConstructor` will hand it to you immediately~~ The universal object constructor `OmniConstructor` has two static methods:
|
||||
|
||||
- `newInstance(<AnyClass>)` ➜ Specify any type, and return an object of that type
|
||||
- `newArray(<AnyClass>, <ArraySize>)` ➜ Specify any type, and return an array of that type
|
||||
|
||||
Usage example:
|
||||
|
||||
```java
|
||||
// Construct a object of "WhatEverClass" type
|
||||
WhatEverClass obj = OmniConstructor.newInstance(WhatEverClass.class);
|
||||
// Construct a array of "WhatEverClass[]" type with capability of 5
|
||||
WhatEverClass[] arr = OmniConstructor.newArray(WhatEverClass.class, 5);
|
||||
```
|
||||
|
||||
Beside that, object constructed by `OmniConstructor` is not just a simple empty object with all member values of `null`, but a "fullness" object in which all members and all sub-members of all members have been recursively initialized. Compared with using `new` operation, `OmniConstructor` can ensure the integrity of the object structure and avoid the `NullPointerException` problem caused by partial initialization of test data.
|
||||
|
||||
```java
|
||||
// Construct object using new operation
|
||||
Parent parent = new Parent();
|
||||
// Inner member is not initialized, will cause NullPointerException (❌)
|
||||
parent.getChild().getGrandChild();
|
||||
|
||||
// Construct object using OmniConstructor
|
||||
Parent parent = OmniConstructor.newInstance(Parent.class);
|
||||
// No need to worry, visit any child member safely (✅)
|
||||
parent.getChild().getGrandChild().getContent();
|
||||
```
|
||||
|
||||
> **Note 1**: In the current version, the member fields of type interface or abstract class will still be initialized to `null`, this problem will be fixed in the future.
|
||||
>
|
||||
> **Note 2**: Based on the light-weight principle, in the default mode, `OmniConstructor` will only uses the original constructor of the class to create objects. For POJO and most model layer objects, this mode has been able to meet the needs.
|
||||
> But for more complex situations, such as when certain class have constructors throwing exceptions or contain other statements that hinder the normal execution of the construction, the object construction may fail.
|
||||
> In those situations, you can use the [Testable global configuration](en-us/doc/javaagent-args.md) `omni.constructor.enhance.enable = true` to enable bytecode enhancement mode of `OmniConstructor`, in this mode, any Java class can be constructed.
|
||||
|
||||
In addition to use as input parameters of the method under test, `OmniConstructor` can also be used to quickly construct the return value of the mock method. Compared to using `null` as the return value of the mock method, using a fully initialized object can better guarantee the reliability of the test .
|
||||
|
||||
In the `DemoOmniMethodsTest` test class of the `java-demo` and `kotlin-demo` sample projects, it is shown in detail how `OmniConstructor` could be used when the target type has a multi-layered nested structure, the construction method is throwing exception, and even without public construction method available.
|
||||
|
||||
### 2. Access any inner member with one line of code
|
||||
|
||||
For test data, even with complex structure, it is usually only part of its attributes and states that are related to a specific test case. However, it is sometimes not easy to assign values to these fields deep wrapped in the object structure.
|
||||
|
||||
As an enhanced version of the `PrivateAccessor` tool, `OmniAccessor` is inspired by the [XPath node selector](https://www.w3schools.com/xml/xpath_syntax.asp) in the `XML` language, It provides two main static methods of `get` and `set`:
|
||||
|
||||
- `get(arbitrary object, "access path")` ➜ returns all member objects searched for based on path-matching
|
||||
- `set(arbitrary object, "access path", new value)` ➜ Assign a value to any objects based on path-matching
|
||||
|
||||
There is also a `getFirst()` method used to directly obtain the unique target object during exact path matching. Its function is equivalent to `OmniAccessor.get(...).get(0)`:
|
||||
|
||||
- `getFirst(arbitrary object, "access path")` ➜ returns the first member object searched based on path-matching
|
||||
|
||||
You only need to write the access path that meets the rules, no matter what type and depth of members, you can directly reach them with one line of code:
|
||||
|
||||
```java
|
||||
// Get all field of the parent object, which named as content and inside type GrandChild
|
||||
OmniAccessor.get(parent, "{GrandChild}/content");
|
||||
// Assign 100 to any fields which named as value and inside any child member that matches the 3rd item of the array named children
|
||||
OmniAccessor.set(parent, "children[2]/*/value", 100);
|
||||
```
|
||||
|
||||
The path rules are as follows:
|
||||
|
||||
**1. Matching member name**
|
||||
|
||||
The path name without additional decoration will match any member object with the same name
|
||||
|
||||
- `child`: match any descendant member whose name is `child`
|
||||
- `child/grandChild`: matches the child member named `grandChild` among the descendants of the name `child`
|
||||
|
||||
**2. Matching member type**
|
||||
|
||||
Use curly braces to match the type name, usually used to obtain or assign multiple member objects of the same type in batches
|
||||
|
||||
- `{Child}`: match all descendants of `Child`
|
||||
- `{Children[]}`: match all descendants of the `Children` array
|
||||
- `{Child}/{GrandChild}`: match all descendant members of `Child`, all types are children of `GrandChild`
|
||||
|
||||
The member name and type can be mixed on the path (currently it is not supported to specify the member name and type at the same time in the same level path)
|
||||
|
||||
- `child/{GrandChild}`: match all descendant members whose name is `child`, all types are child members of `GrandChild`
|
||||
- `{Child}/grandChild/content`: match all descendant members whose type is `Child`, the child members named `grandChild`, and the child members named `content`
|
||||
|
||||
**3. Use subscripts to access array members**
|
||||
|
||||
Use square brackets with numerical values to indicate that the matching position is an array type, and the object with the specified subscript is taken (without subscript, when the matching object is an array type, all objects in the array are matched)
|
||||
|
||||
- `children[1]/content`: match the descendant members of the array type named `children`, and take the child member named `content` in the `2`th object
|
||||
- `parent/children[1]`: match the child member of the array type named `children` among the descendant members named `parent`, and take the `2`th object among them
|
||||
|
||||
**4. Use wildcards**
|
||||
|
||||
Wildcards can be used to match member names or type names
|
||||
|
||||
- `child*`: match all descendant members whose name starts with `child`
|
||||
- `{*Child}`: match all descendant members whose type ends with `Child`
|
||||
- `c*ld/{Grand*ld}`: match the descendant members whose name starts with `c` and ends with `ld`, and the members whose type starts with `Grand` and ends with `ld`
|
||||
- `child/*/content`: At this time, `*` will match any member, that is, the child member of `content` contained in any child member of the `child` object
|
||||
|
||||
For details, see the use cases in the test classes of the `java-demo` and `kotlin-demo` sample projects `DemoOmniMethodsTest`.
|
||||
|
||||
### 3. Special instructions
|
||||
|
||||
> **Do you really need to use `OmniAccessor`? **
|
||||
>
|
||||
> `OmniAccessor` implement the basic anti-refactoring mechanism based on the Fail-Fast principle. When the access path provided by the user cannot match any member, the `OmniAccessor` will immediately throw a `NoSuchMemberError` error, so that the unit test is terminated early. However, compared to the conventional member access method, the support of `OmniAccessor` in IDE refactoring is still weak.
|
||||
>
|
||||
> For content assignment of complex objects, in most cases, we recommend using [Builder Pattern](https://www.geeksforgeeks.org/builder-pattern-in-java/), or exposing Getter/Setter method implementations. Although these conventional methods are slightly clumsy (especially when you need to assign values to many similar members in batches), they are more friendly to the encapsulation and reconstruction of business logic.
|
||||
> Only when the original type is not suitable for transformation, and there is no other way to access the target member, `OmniAccessor` is the last resort.
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# Release Note
|
||||
|
||||
## 0.6.4
|
||||
- `TestableNull` class removed, let `OmniConstructor` even lighter
|
||||
- support `@Nested` annotation of JUnit 5
|
||||
- fix several `OmniConstructor` compatibility issues(thanks to [@ddatsh](https://github.com/ddatsh))
|
||||
|
||||
## 0.6.3
|
||||
- support custom inner `Mock` class name
|
||||
- support skip specified packages from `OmniConstructor` enhancement
|
||||
- fix 2 circle construction cases in `OmniConstructor`
|
||||
- fix an index out-of-range issue caused by non-static method without `this` reference
|
||||
- fix a null pointer exception caused by resource folder unavailable at runtime
|
||||
|
||||
## 0.6.2
|
||||
- support setup global testable agent configure via properties file
|
||||
- disable OmniConstructor bytecode enhancement by default
|
||||
- fix a `Spring` framework compatibility issue with OmniConstructor
|
||||
|
||||
## 0.6.1
|
||||
- generate mock scanning log file automatically, for self-troubleshooting
|
||||
- fix a `Spock` test framework compatibility issue
|
||||
- fix a `Gradle` unit test runtime compatibility issue
|
||||
|
||||
## 0.6.0
|
||||
- add `OmniConstructor` and `OmniAccessor` tool for parameter preparation
|
||||
- fix a `ClassFormatError` caused by incorrect `FRAME FULL` operation
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Use TestableMock
|
||||
---
|
||||
|
||||
`TestableMock` is an assist tool for Java unit testing based on source code and bytecode enhancement, including the following functions:
|
||||
`TestableMock` is now not only a lightweight and easy-to-use unit testing mock tool, but also a comprehensive set of auxiliary tools aimed at **simplifying Java unit testing**, including the following functions:
|
||||
|
||||
- [Quickly 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
|
||||
- [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
|
||||
@@ -16,7 +16,7 @@ It is recommended to add a `property` field that identifies the TestableMock ver
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.6.0</testable.version>
|
||||
<testable.version>0.6.4</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -63,8 +63,8 @@ Add dependence of `TestableMock` in `build.gradle` file:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.4')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.4')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -93,3 +93,20 @@ See the [build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo
|
||||
> ```
|
||||
>
|
||||
> See [issue-43](https://github.com/alibaba/testable-mock/issues/43) for a complete example.
|
||||
|
||||
> If the project is using `Spock` test framework, you need to specify the bytecode version generated by `Groovy` to be 1.6 or above, the method is as follows (please modify the properties value according to the actual JVM version used).
|
||||
>
|
||||
> For Maven project, add `<maven.compiler.source>` and `<maven.compiler.target>` properties inside the `pom.xml` file, e.g.
|
||||
> ```xml
|
||||
> <properties>
|
||||
> <maven.compiler.source>1.6</maven.compiler.source>
|
||||
> <maven.compiler.target>1.6</maven.compiler.target>
|
||||
> </properties>
|
||||
> ```
|
||||
>
|
||||
> For Gradle project, add a `sourceCompatibility` property inside the `build.gradle` file, e.g.
|
||||
> ```groovy
|
||||
> sourceCompatibility = '6'
|
||||
> ```
|
||||
>
|
||||
> See project `demo/spock-demo` for a complete example.
|
||||
|
||||
@@ -3,52 +3,45 @@ 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 `@MockDiagnose` annotation to the mock class, and set the value `LogLevel.ENABLE`, so the detailed mock method replacement process will be printed when the test is run.
|
||||
For this reason, `TestableMock` will automatically save the mock scanning log of the last test run in the project build directory. The default location is `target/testable-agent.log` (Maven project) or `build/testable-agent.log` (Gradle project).
|
||||
|
||||
```java
|
||||
class DemoTest {
|
||||
@MockDiagnose(LogLevel.ENABLE)
|
||||
public static class Mock {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The output log example is as follows:
|
||||
Examples of log content are as follows:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[DIAGNOSE] Found 6 test cases
|
||||
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
[DIAGNOSE] Line 27, mock method "staticFunc" used
|
||||
[DIAGNOSE] Handling method commonFunc
|
||||
[DIAGNOSE] Line 34, mock method "trim" used
|
||||
[DIAGNOSE] Line 34, mock method "sub" used
|
||||
[DIAGNOSE] Line 34, mock method "startsWith" used
|
||||
[INFO] Start at Mon Jan 00 00:00:00 CST 0000
|
||||
... ...
|
||||
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[INFO] Found 6 test cases
|
||||
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
|
||||
[INFO] Found 8 mock methods
|
||||
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
|
||||
[INFO] Found method <init>
|
||||
[INFO] Found method newFunc
|
||||
[INFO] Line 19, mock method "createBlackBox" used
|
||||
[INFO] Found method outerFunc
|
||||
[INFO] Line 27, mock method "innerFunc" used
|
||||
[INFO] Line 27, mock method "staticFunc" used
|
||||
[INFO] Found method commonFunc
|
||||
[INFO] Line 34, mock method "trim" used
|
||||
[INFO] Line 34, mock method "sub" used
|
||||
[INFO] Line 34, mock method "startsWith" used
|
||||
... ...
|
||||
[INFO] Completed at Mon Jan 00 00:00:00 CST 0000
|
||||
```
|
||||
|
||||
The log shows all the mocked invocation and corresponding code line numbers in the class under test.
|
||||
|
||||
- Self troubleshooting:
|
||||
According to the targeted test classes, below are some simple clues for self-troubleshooting. Suppose the class under test is "com.demo.BizService", the test class is "com.demo.BizServiceTest", and the mock container class is "com.demo.BizServiceTest.Mock":
|
||||
|
||||
- If there is no output, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies
|
||||
- If the log file is no generated, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies
|
||||
- If only `com/demo/BizServiceTest$Mock` is found in the output, please check whether the mock class is created at correct place
|
||||
- If both the test class and the mock class are found, but the class under test `com/demo/BizService` not appeared, please check whether the test class is in the same package of the class under test, and the name is "<ClassUnderTest>+Test", otherwise `@MockWith` annotation should be used
|
||||
- If all the three classes are found, and `Found method xxx` are output, but there is no mock replacement happen at the expected code line, please check whether the mock method definition matches the target method
|
||||
|
||||
- If only the first line of `Handling mock class` is output, please check whether the mock class is created at correct place
|
||||
- If `Handling mock class` and `Handling test class` are output, please check whether the test class is in the same package of the class under test, and the name is "<ClassUnderTest>+Test", otherwise `@MockWith` annotation should be used
|
||||
- If `Handling source class` and `Handling method xxx` are output, but there is no mock replacement happen at the expected code line, please check whether the mock method definition matches the target method
|
||||
|
||||
For situations where expected mocking is not take effect, you could set the diagnosis level to `LogLevel.VERBOSE` for further investigation information.
|
||||
For situations where expected mocking is not take effect, you could add a `@MockDiagnose` annotation to the mock class, and set the diagnosis level to `LogLevel.VERBOSE` for further investigation information.
|
||||
|
||||
```java
|
||||
class DemoTest {
|
||||
class BizServiceTest {
|
||||
@MockDiagnose(LogLevel.VERBOSE)
|
||||
public static class Mock {
|
||||
...
|
||||
@@ -59,30 +52,30 @@ class DemoTest {
|
||||
Executing the unit test again will print out the signatures of all mock methods, and the signatures of all invocations scanned in the class under test:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[VERBOSE] Test case "should_mock_new_object"
|
||||
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[TIP] Test case "should_mock_new_object"
|
||||
... ...
|
||||
[VERBOSE] Test case "should_set_mock_context"
|
||||
[DIAGNOSE] Found 6 test cases
|
||||
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
|
||||
[VERBOSE] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
|
||||
[VERBOSE] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
|
||||
[TIP] Test case "should_set_mock_context"
|
||||
[INFO] Found 6 test cases
|
||||
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
|
||||
[TIP] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
|
||||
[TIP] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
|
||||
... ...
|
||||
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[VERBOSE] Line 13, constructing "java.lang.Object()"
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[VERBOSE] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[VERBOSE] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
|
||||
[VERBOSE] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[VERBOSE] Line 27, constructing "java.lang.StringBuilder()"
|
||||
[VERBOSE] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
|
||||
[VERBOSE] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
[TIP] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
|
||||
[INFO] Found 8 mock methods
|
||||
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
|
||||
[INFO] Found method <init>
|
||||
[TIP] Line 13, constructing "java.lang.Object()"
|
||||
[INFO] Found method newFunc
|
||||
[TIP] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
|
||||
[INFO] Line 19, mock method "createBlackBox" used
|
||||
[TIP] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
|
||||
[TIP] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
|
||||
[INFO] Found method outerFunc
|
||||
[TIP] Line 27, constructing "java.lang.StringBuilder()"
|
||||
[TIP] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
|
||||
[TIP] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
|
||||
[INFO] Line 27, mock method "innerFunc" used
|
||||
... ...
|
||||
```
|
||||
|
||||
@@ -92,3 +85,7 @@ The logs are formatted in follow pattern:
|
||||
- `Mock method "<MockMethodName>" as "<Signature>"` Mock method found in test class (the first parameter that identify the mock target class is currently kept)
|
||||
- `Line XX, constructing "<TypeName>" as "<Signature>"` Constructor invocation found in test under class
|
||||
- `Line XX, invoking "<MethodName>" as "<Signature>"` Member method invocation found in test under class
|
||||
|
||||
> In order to clearly distinguish between the type of method return value and the type of invoker, the method signature recorded in the log uses a method definition structure similar to `Kotlin`.
|
||||
|
||||
Comparing the actual signature of the original call with the signature defined by the mock method, the problem is usually found quickly.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
Fast Mocking
|
||||
---
|
||||
|
||||
Compared with the class-granularity mocking practices of existing mock tools, `TestableMock` allows developers to directly define a single method and use it for mocking. With the principle of convention over configuration, mock method replacement will automatically happen when the specified method in the test class match an invocation in the class under test.
|
||||
In unit testing, the main role of the mock method is to replace those methods with **need external dependencies**, **with time-consuming**, **has random results**, or other effects that affect the development of the test, but do not affect the key the logic to be tested. Generally speaking, a certain call needs to be mocked, which is usually only related to its own characteristics, and has nothing to do with the source of the invocation.
|
||||
|
||||
Based on the above information, `TestableMock` has designed a minimalist mock mechanism. Unlike the common mock tools that uses **class** as the definition granularity of mocking, and repeats the description of mock behavior in each test case, `TestableMock` allows each business class (class under test) to be associated with a set of reusable collection of mock methods (carried by the mock container class), following the principle of "convention over configuration", and mock method replacement will automatically happen when the specified method in the test class match an invocation in the class under test.
|
||||
|
||||
> In summary, there are two simple rules:
|
||||
> - Mock non-constructive method, copy the original method definition to the mock class, add a `@MockMethod` annotation
|
||||
@@ -55,7 +57,7 @@ private String use_any_mock_method_name(int i, int j) {
|
||||
|
||||
Sometimes, the mock method need to access the member variables in the original object that initiated the invocation, or invoke other methods of the original object. At this point, you can remove the `targetClass` parameter in the `@MockMethod` annotation, and then add a extra parameter whose type is the original object type of the method to the first index of the method parameter list.
|
||||
|
||||
The `TestableMock` convention is that when the `targetClass` parameter value of the `@MockMethod` annotation is empty, the first parameter of the mock method is the type of the target method, and the parameter name is arbitrary. In order to facilitate code reading, it is recommended to name this parameter as `self` or `src`. Example as follows:
|
||||
The `TestableMock` convention is that when the `targetClass` parameter of the `@MockMethod` annotation is not defined, the first parameter of the mock method is the type of the target method, and the parameter name is arbitrary. In order to facilitate code reading, it is recommended to name this parameter as `self` or `src`. Example as follows:
|
||||
|
||||
```java
|
||||
// Adds a `String` type parameter to the first position the mock method parameter list (parameter name is arbitrary)
|
||||
@@ -67,7 +69,7 @@ private String substring(String self, int i, int j) {
|
||||
}
|
||||
```
|
||||
|
||||
For complete code examples, see the `should_mock_common_method()` test cases in the `java-demo` and `kotlin-demo` sample projects. (Because Kotlin has made magical changes to the String type, the method under test in the Kotlin example adds a layer of encapsulation to the `BlackBox` class)
|
||||
For complete code examples, see the `should_mock_common_method()` test cases in the `java-demo` and `kotlin-demo` sample projects. (Because `Kotlin` has made magical changes to the String type, the method under test in the `Kotlin` example adds a layer of encapsulation to the `BlackBox` class)
|
||||
|
||||
### 1.2 Mock the member method of the class under test itself
|
||||
|
||||
@@ -161,7 +163,17 @@ For complete code examples, see the `should_get_source_method_name()` and `shoul
|
||||
|
||||
### 3. Verify the sequence and parameters of the mock method being invoked
|
||||
|
||||
In test cases, you can use the `TestableTool.verify()` method, and cooperate with `with()`, `withInOrder()`, `without()`, `withTimes()` and other methods to verify the mock call situation.
|
||||
In test cases, you can use the `InvokeVerifier.verify()` method, and cooperate with `with()`, `withInOrder()`, `without()`, `withTimes()` and other methods to verify the mock call situation.
|
||||
|
||||
For details, please refer to the [Check Mock Call](en-us/doc/matcher.md) document.
|
||||
|
||||
### 4. Special instructions
|
||||
|
||||
> **Naming Conventions for Test Classes and Mock Containers**
|
||||
>
|
||||
> By default, `TestableMock` assumes that the <u>package path of the test class and the class under test are the same, and the name is `<ClassUnderTest>+Test`</u> (usually Java project with `Maven` or `Gradle` conform to this convention).
|
||||
> At the same time, it is agreed that the mock container associated with the test class is <u>in its internal static class named `Mock`</u>, or <u>an independent class named `<ClassUnderTest>+Mock` under the same package path</u>.
|
||||
>
|
||||
> When the test class or mock container path does not follow to this convention, you can use the `@MockWith` annotation to specify it explicitly. For details, see [Use MockWith Annotation](en-us/doc/use-mock-with.md).
|
||||
|
||||
For more implementation details of `TestableMock`, please refer to the [Design and Principle](en-us/doc/design-and-mechanism.md) document.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
TestableMock简介
|
||||
---
|
||||
|
||||
单元测试中的Mock方法,通常是为了绕开那些依赖外部资源或无关功能的方法调用,使得测试重点能够集中在需要验证和保障的代码逻辑上。某个调用需要被Mock,往往只与其自身特征有关,而与调用的来源无关。
|
||||
单元测试中的Mock方法,通常是为了绕开那些依赖外部资源或无关功能的方法调用,使得测试重点能够集中在需要验证和保障的代码逻辑上。
|
||||
|
||||
在定义Mock方法时,开发者真正关心的只有一件事:"<u>这个调用,在测试的时候要换成那个假的Mock方法</u>"。
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ TestableMock的设计和原理
|
||||
这种设计基于两项基本假设:
|
||||
|
||||
1. 同一个测试类里,一个测试用例里需要Mock掉的方法,在其他测试用例里通常也都需要Mock。因为这些被Mock的方法往往访问了不便于测试的外部依赖。
|
||||
2. 需要Mock的调用都来自被测类的代码。(此假设是符合单元测试初衷的,即单元测试只应该关注当前单元的内部行为,单元外的逻辑应该被替换为Mock)
|
||||
2. 每个单元测试只关注被测单元内部的逻辑,单元外的无关调用应该被替换为Mock。即需要被Mock的调用应该都在被测类的代码中。
|
||||
|
||||
据此通过约定来简化符合该假设的单元测试场景,通过配置来支持其余复杂的使用场景。
|
||||
据此通过约定来简化符合以上假设的单元测试场景,通过配置来支持其余更复杂的使用场景。
|
||||
|
||||
`TestableMock`的原理可以用一句话概括:<u>利用JavaAgent动态修改字节码,把被测的业务类中与所有与Mock方法定义匹配的调用在单元测试运行时替换成对Mock方法的调用</u>。
|
||||
|
||||
|
||||
@@ -15,7 +15,15 @@
|
||||
|
||||
对于会修改类加载器或被测类字节码的Mock工具,譬如`PowerMock`和`JMockit`,尚无案例证明会与`TestableMock`发生冲突,但从原理来说二者可能存在不兼容风险,请谨慎使用。
|
||||
|
||||
#### 3. 父类变量指向子类对象时,如何实现Mock方法?
|
||||
#### 3. `TestableMock`支持哪些测试框架?
|
||||
|
||||
`TestableMock`的`PrivateAccessor`/`OmniConstructor`/`OmniAccessor`以及基础Mock功能均与测试框架无关,理论上适用于任何测试框架。
|
||||
|
||||
唯独Mock方法的调用校验器是与测试框架相关的,目前已支持`JUnit 4`/`JUnit 5`/`TestNG`/`Spock`四款主流框架。
|
||||
|
||||
若亲遇到在特定测试框架下的兼容问题,或希望增加其他框架的支持,请通过[Issues](https://github.com/alibaba/testable-mock/issues)告诉我们。
|
||||
|
||||
#### 4. 父类变量指向子类对象时,如何实现Mock方法?
|
||||
|
||||
在代码中,经常会有使用<u>接口变量或父类变量</u>指向子类实例,调用父类或子类方法的情况。
|
||||
|
||||
@@ -25,7 +33,7 @@
|
||||
|
||||
参见Java和Kotlin示例中`DemoInheritTest`测试类的用例。
|
||||
|
||||
#### 4. 如何Mock对于泛型方法(模板方法)?
|
||||
#### 5. 如何Mock对于泛型方法(模板方法)?
|
||||
|
||||
与普通方法的Mock方法相同,直接在Mock方法上使用相同的泛型参数即可。
|
||||
|
||||
@@ -33,25 +41,25 @@
|
||||
|
||||
> 由于JVM存在泛型擦除机制,对于Java项目也可以直接使用`Object`类型替代泛型参数,见Java版`DemoTemplateTest`测试类中被注释掉的"第二种写法"示例。
|
||||
|
||||
#### 5. 如何Mock在内部类代码里的调用?
|
||||
#### 6. 如何Mock在内部类代码里的调用?
|
||||
|
||||
在其所在外部类对应的Mock容器中定义所需的Mock方法即可。
|
||||
|
||||
参见Java和Kotlin示例中`DemoInnerClass`测试类的用例。
|
||||
|
||||
#### 6. 在Kotlin项目对`String`类中的方法进行Mock不生效?
|
||||
#### 7. 在Kotlin项目对`String`类中的方法进行Mock不生效?
|
||||
|
||||
Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.String`。但在构建生成自字节码的时候又会被替换为Java的`java.lang.String`类,因此无论将Mock目标写为`kotlin.String`或`java.lang.String`均无法正常匹配到原始的被调用方法。
|
||||
|
||||
实际场景中需要对`String`类中的方法进行Mock的场景很少,`TestableMock`暂未对这种情况做特别处理。
|
||||
|
||||
#### 7. `TestableMock`能否用于Android项目的测试?
|
||||
#### 8. `TestableMock`能否用于Android项目的测试?
|
||||
|
||||
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。
|
||||
|
||||
Android系统的`Dalvik`和`ART`虚拟机采用了与标准JVM不同的字节码体系,会影响`TestableMock`的正常工作。`Roboelectric`框架能在普通JVM虚拟机上运行Android单元测试,其速度比通过Android虚拟机运行单元测试快非常多,绝大多数Android App的单元测试都在使用`Roboelectric`框架。
|
||||
|
||||
#### 8. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
|
||||
#### 9. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
|
||||
|
||||
这个问题是由于系统ClassPath包含太多路径所致,与是否使用`TestableMock`无关。但需要注意的是,IntelliJ提供了两种辅助解决机制:`JAR manifest`和`classpath file`,若测试中使用了`TestableMock`,请选择`JAR manifest`。
|
||||
|
||||
|
||||
@@ -1,15 +1,43 @@
|
||||
全局运行参数
|
||||
---
|
||||
|
||||
`TestableMock`的许多功能采用了基于JavaAgent的运行时字节码修改实现。在JavaAgent启动时,可以通过额外的全局参数来调整`TestableMock`的执行过程。
|
||||
`TestableMock`的许多功能采用了基于JavaAgent的运行时字节码修改实现。在JavaAgent启动时,可以通过两种方式调整`TestableMock`的执行过程。
|
||||
|
||||
## 1.使用全局配置文件
|
||||
|
||||
在项目的`src/test/resources`目录下创建一个名为`testable.properties`的文本文件,其中每行为一条配置项,配置项的名称与值之间用`=`连接。
|
||||
|
||||
对于大多数情况,我们更推荐使用配置文件来调节`TestableMock`的行为,这种方式相比Agent参数更加易读。
|
||||
|
||||
### 可用配置项
|
||||
|
||||
| 配置项 | 描述 | 可用值和示例(`N/A`表示无需赋值) |
|
||||
| ---- | ---- | ---- |
|
||||
| dump.path | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 相对项目根目录的位置,例如:`target/bytecode` |
|
||||
| enhance.pkgPrefix.includes | 限定`TestableMock`仅对部分包生效 | 使用`,`分隔的包路径前缀列表,例如:`com.demo.svc,com.demo.dao` |
|
||||
| log.file | 指定TestableAgent日志文件位置 | 相对项目根目录的位置,例如:`target/testable/agent.log`,特殊值`null`表示禁用日志文件 |
|
||||
| log.level | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
|
||||
| mock.innerClass.name | 修改测试类中的内置Mock容器类名 | 任意符合Java类名的值,默认值为`Mock` |
|
||||
| mock.scope.default | 修改默认的Mock生效范围(详见[Mock生效范围](zh-cn/doc/scope-of-mock)) | 可用值为:`global`(全局生效) / `associated`(只对关联的测试用例生效) |
|
||||
| omni.constructor.enhance.enable | 是否启用`OmniConstructor`的字节码增强模式 | 可用值为:`true` / `false` |
|
||||
| omni.constructor.enhance.pkgPrefix.excludes | 对特定包禁用`OmniConstructor`的字节码增强模式 | 使用`,`分隔的包路径前缀列表,例如:`com.demo.model` |
|
||||
| thread.pool.enhance.enable | 是否启用基于`TransmittableThreadLocal`的Mock上下文存储 | 可用值为:`true` / `false` |
|
||||
|
||||
示例见`java-demo`和`kotlin-demo`项目中的`testable.properties`文件。
|
||||
|
||||
## 2.使用全局Agent参数
|
||||
|
||||
与其它基于JavaAgent的工具相似,`TestableMock`也支持通过在引入`testable-agent`包时,在末尾加上一个`=`符号,然后连接额外参数来传递用户的自定义参数。
|
||||
|
||||
### 可用参数清单
|
||||
|
||||
| 参数 | 描述 | 可用值和示例(`N/A`表示无需赋值) |
|
||||
| ---- | ---- | ---- |
|
||||
| configFile | 修改全局配置文件位置 | 相对项目根目录的位置,默认值为`src/test/resources/testable.properties` |
|
||||
| logLevel | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
|
||||
| dumpPath | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 例如 `/tmp/bytecode`(需要此目录事先存在) |
|
||||
| pkgPrefix | 限定`TestableMock`仅对部分包生效 | 使用`,`分隔的包路径前缀列表,例如 `com.demo.svc,com.demo.dao` |
|
||||
| logFile | 指定TestableAgent日志文件位置 | 相对项目根目录的位置,例如:`target/testable/agent.log`,特殊值`null`表示禁用日志文件 |
|
||||
| dumpPath | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 相对项目根目录的位置,例如:`target/bytecode` |
|
||||
| pkgPrefix | 限定`TestableMock`仅对部分包生效 | 使用`,`分隔的包路径前缀列表,例如:`com.demo.svc,com.demo.dao` |
|
||||
| mockScope | 修改默认的Mock生效范围(详见[Mock生效范围](zh-cn/doc/scope-of-mock)) | 可用值为:`global`(全局生效) / `associated`(只对关联的测试用例生效) |
|
||||
| useThreadPool | 启用基于`TransmittableThreadLocal`的Mock上下文存储(用于包含线程池的测试用例) | `N/A` |
|
||||
|
||||
@@ -25,13 +53,11 @@
|
||||
|
||||
### 添加运行参数
|
||||
|
||||
与其他基于JavaAgent的工具相似,`TestableMock`通过在单元测试引入`testable-agent`包的末尾加上一个`=`符号,然后连接额外参数来传递用户的自定义参数。
|
||||
|
||||
对于Maven项目,可将参数追加到`maven-surefire-plugin`参数`testable-agent`包尾部,紧接着`.jar`的位置。例如:
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar=mockScope=associated&pkgPrefix=com.demo.</argLine>
|
||||
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar=mockScope=associated&pkgPrefix=com.demo</argLine>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
@@ -39,7 +65,6 @@
|
||||
|
||||
对于Gradle项目,同样是直接将参数追加到引入`testable-agent`的配置末尾。例如:
|
||||
|
||||
|
||||
```groovy
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}=mockScope=associated&pkgPrefix=com.demo."
|
||||
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}=mockScope=associated&pkgPrefix=com.demo"
|
||||
```
|
||||
|
||||
@@ -1,47 +1,55 @@
|
||||
快速构造复杂的参数对象
|
||||
---
|
||||
|
||||
在单元测试中,测试数据的准备和构造是一件既必须又繁琐的任务,尤其遇到被测函数的入参类型结构复杂、没有合适的构造方法、成员对象使用私有内部类的时候,常规方法往往无处下手。为此`TestableMock`提供了`OmniConstructor`和`OmniAccessor`两个极简的工具类,从此让一切对象构造不再困难。
|
||||
在单元测试中,测试数据的准备和构造是一件既必须又繁琐的任务,面向对象的层层封装,在测试时就成为了初始化对象状态的重重阻碍。尤其遇到类型结构嵌套复杂、没有合适的构造方法、需要使用私有内部类等等状况时,常规手段往往显得力不从心。
|
||||
|
||||
为此`TestableMock`提供了`OmniConstructor`和`OmniAccessor`两个极简的工具类,从此让一切对象构造不再困难。
|
||||
|
||||
### 1. 一行代码构造任何对象
|
||||
|
||||
万能的对象构造器`OmniConstructor`有两个静态方法:
|
||||
不论目标类型多么奇葩,呼唤`OmniConstructor`,马上递给您~ 万能的对象构造器`OmniConstructor`有两个静态方法:
|
||||
|
||||
- `newInstance(任意类型)` ➜ 指定任意类型,返回一个该类型的对象
|
||||
- `newArray(任意类型, 数组大小)` ➜ 指定任意类型,返回一个该类型的数组
|
||||
|
||||
例如:
|
||||
用法举例:
|
||||
|
||||
```java
|
||||
// 构造一个ComplicatedClass类型的对象
|
||||
ComplicatedClass obj = OmniConstructor.newInstance(ComplicatedClass.class);
|
||||
// 构造一个ComplicatedClass[]类型,容量为5的数组
|
||||
ComplicatedClass[] arr = OmniConstructor.newArray(ComplicatedClass.class, 5);
|
||||
// 构造一个WhatEverClass类型的对象
|
||||
WhatEverClass obj = OmniConstructor.newInstance(WhatEverClass.class);
|
||||
// 构造一个WhatEverClass[]类型,容量为5的数组
|
||||
WhatEverClass[] arr = OmniConstructor.newArray(WhatEverClass.class, 5);
|
||||
```
|
||||
|
||||
值得一提的是,使用`OmniConstructor`构造出来的并非是一个所有成员值为`null`的简单空对象。该对象的所有成员,以及所有成员的所有子成员,都会在构造时被依次递归赋值。相比直接用`new`构造的对象,使用`OmniConstructor`能够确保对象完全初始化,无需担心测试过程中发生`NullPointerException`问题。
|
||||
|
||||
> **注意**:在`0.6.0`版本中,类型为接口或抽象类的成员字段依然会被初始化为`null`,此问题将在近期版本修复
|
||||
不仅如此,`OmniConstructor`构造出的绝非是所有成员值为`null`的简单空对象,而是所有成员、以及所有成员的所有子成员,都已经依次递归初始化的"丰满"对象。相比使用`new`进行构造,`OmniConstructor`能够确保对象结构完整,避免测试数据部分初始化导致的`NullPointerException`问题。
|
||||
|
||||
```java
|
||||
// 使用构造函数创建对象
|
||||
Parent parent = new Parent();
|
||||
// 内部成员未初始化,直接访问报NullPointerException异常
|
||||
// 内部成员未初始化,直接访问报NullPointerException异常(❌)
|
||||
parent.getChild().getGrandChild();
|
||||
|
||||
// 使用OmniConstructor创建对象
|
||||
Parent parent = OmniConstructor.newInstance(Parent.class);
|
||||
// 无需顾虑,安心访问任意子成员
|
||||
// 无需顾虑,安心访问任意子成员(✅)
|
||||
parent.getChild().getGrandChild().getContent();
|
||||
```
|
||||
|
||||
> **注意 1** :在当前版本中,类型为接口或抽象类的成员字段依然会被初始化为`null`,此问题将在后续版本修复
|
||||
>
|
||||
> **注意 2** :基于轻量优先原则,默认模式下,`OmniConstructor`仅利用类型原有的构造方法来创建对象,对于绝大多数POJO和`Model`层对象而言这种模式已经能够满足需要。
|
||||
> 但对于更复杂的情形,譬如遇到某些类型的构造方法会抛出异常或包含其他妨碍构造正常执行的语句时,对象构造可能会失败。
|
||||
> 此时可通过[Testable全局配置](zh-cn/doc/javaagent-args.md)`omni.constructor.enhance.enable = true`启用`OmniConstructor`的字节码增强模式,在该模式下,任何Java类型皆可构造。
|
||||
|
||||
除了用于构造方法的入参,`OmniConstructor`也可以用于快速构造Mock方法的返回值,相比将`null`作为Mock方法的返回值,使用完全初始化的对象能够更好保障测试的可靠性。
|
||||
|
||||
详见`java-demo`和`kotlin-demo`示例项目`DemoOmniMethodsTest`测试类中的用例。
|
||||
在`java-demo`和`kotlin-demo`示例项目的`DemoOmniMethodsTest`测试类中,详细展示了当目标类型有多层嵌套结构、构造方法无法正常使用,甚至没有公开的构造方法时,如何用`OmniConstructor`轻松创建所需对象。
|
||||
|
||||
### 2. 一行代码访问任意深度成员
|
||||
|
||||
在单元测试中,有时会遇到一些结构复杂的参数对象,但与特定测试用例有关的仅仅是该对象结构深处的个别几个属性和状态。`OmniAccessor`的灵感来自于`XML`语言中的`xpath`节点选择器,它有`get`、`set`两个主要的静态方法:
|
||||
对于测试数据而言,即使是结构复杂的参数对象,与特定测试用例有关的通常也只是其中的部分属性和状态,然而要为这些深藏在对象结构内部的字段赋值有时却并非易事。
|
||||
|
||||
做为`PrivateAccessor`功能的加加加强版,`OmniAccessor`的灵感来自于`XML`语言中的[XPath节点选择器](https://www.w3school.com.cn/xpath/xpath_syntax.asp),它提供了`get`、`set`两个主要的静态方法:
|
||||
|
||||
- `get(任意对象, "访问路径")` ➜ 返回根据路径匹配搜索到的所有成员对象
|
||||
- `set(任意对象, "访问路径", 新的值)` ➜ 根据路径匹配为指定位置的对象赋值
|
||||
@@ -76,7 +84,7 @@ OmniAccessor.set(parent, "children[2]/*/value", 100);
|
||||
- `{Children[]}`: 匹配所有类型是`Children`数组的子孙成员
|
||||
- `{Child}/{GrandChild}`: 匹配所有类型是`Child`的子孙成员里,所有类型是`GrandChild`子成员
|
||||
|
||||
成员名和类型可以在路径上混用,但暂不支持在同一级路径既指定成员名称又指定类型的写法
|
||||
成员名和类型可以在路径上混用(暂不支持在同一级路径同时指定成员名称和类型)
|
||||
|
||||
- `child/{GrandChild}`: 匹配名字为`child`的子孙成员里,所有类型是`GrandChild`的子成员
|
||||
- `{Child}/grandChild/content`: 匹配所有类型是`Child`的子孙成员里,名为`grandChild`子成员里的,名为`content`的子成员
|
||||
@@ -102,10 +110,8 @@ OmniAccessor.set(parent, "children[2]/*/value", 100);
|
||||
### 3. 特别说明
|
||||
|
||||
> **你真的需要用到`OmniAccessor`吗?**
|
||||
>
|
||||
>
|
||||
> `OmniAccessor`具有基于Fail-Fast机制的防代码重构能力,当用户提供的访问路径无法匹配到任何成员时,`OmniAccessor`将立即抛出`NoSuchMemberError`错误,使单元测试提前终止。然而相比常规的成员访问方式,`OmniAccessor`在IDE重构方面的支持依然偏弱。
|
||||
>
|
||||
> 对于复杂对象的内容赋值,大多数情况下,我们更推荐使用[构造者模式](https://developer.aliyun.com/article/705058),或者暴露Getter/Setter方法实现。这些常规手段虽然稍显笨拙(尤其在需要为许多相似的成员批量赋值的时候),但对业务逻辑的封装和重构都更加友好。
|
||||
> 仅当原类型不适合改造,且没有其它可访问目标成员的方法时,`OmniAccessor`才是最后的终极手段。
|
||||
>
|
||||
> 出于相同的原因,我们并不推荐在除单元测试之外的场景使用`OmniAccessor`方式来读写业务类的成员字段(虽然技术上可行)。
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
|
||||
为此,`TestableMock`对`PrivateAccessor`进行了增强,赋予其编译期私有成员校验能力。这项功能默认关闭,需要通过`@EnablePrivateAccess`注解开启。(实际上是通过该注解的`verifyTargetOnCompile`参数控制,由于此参数默认值为`true`,因此只需在被测类上添加该注解即可启用私有成员校验)
|
||||
|
||||
**注意 1.**:当私有成员校验功能开启时,`PrivateAccessor`工具类将只能用于访问**被测类**的私有成员,从某种角度而言,这也有助于限制将`PrivateAccessor`工具类用于与当前测试无关的“越权”操作。
|
||||
**注意 1**:当私有成员校验功能开启时,`PrivateAccessor`工具类将只能用于访问**被测类**的私有成员,从某种角度而言,这也有助于限制将`PrivateAccessor`工具类用于与当前测试无关的“越权”操作。
|
||||
|
||||
**注意 2.**:`TestableMock`默认约定测试类与**被测类**的包路径相同,且名称为`被测类+Test`。若测试类名称不符合此约定时,在使用`@EnablePrivateAccess`注解时,需用`srcClass`参数显式指明实际的**被测类**位置。
|
||||
**注意 2**:`TestableMock`默认约定测试类与**被测类**的包路径相同,且名称为`被测类+Test`。若测试类名称不符合此约定时,在使用`@EnablePrivateAccess`注解时,需用`srcClass`参数显式指明实际的**被测类**位置。
|
||||
|
||||
**注意 3**: 此机制目前只针对`Java`语言实现,对于`Kotlin`以及其他JVM方言均无效。
|
||||
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# Release Note
|
||||
|
||||
## 0.6.4
|
||||
- 移除`TestableNull`类型,让`OmniConstructor`更轻量
|
||||
- 支持JUnit 5的`@Nested`注解(issue-140)
|
||||
- 修复多处`OmniConstructor`的兼容性BUG(感谢[@ddatsh](https://github.com/ddatsh))
|
||||
|
||||
## 0.6.3
|
||||
- 支持自定义内部Mock容器类名(issue-137)
|
||||
- 支持自定义`OmniConstructor`字节码增强范围
|
||||
- 修复两处可导致`OmniConstructor`无限递归构建的问题(包括issue-138)
|
||||
- 修复一处非静态方法获取`this`引用失败导致的下标越界异常(issue-136)
|
||||
- 修复获取构建目录失败导致的空指针异常(issue-135)
|
||||
|
||||
## 0.6.2
|
||||
- 支持通过Properties文件配置TestableAgent
|
||||
- 默认禁用OmniConstructor相关的字节码增强
|
||||
- 修复OmniConstructor与`Spring`框架的兼容问题(issue-129)
|
||||
|
||||
## 0.6.1
|
||||
- 自动生成Mock扫描过程日志文件,便于自助排查问题
|
||||
- 修复一处`Spock`测试框架的兼容问题 (issue-121)
|
||||
- 修复一处`Gradle`运行单元测试的兼容问题(issue-123)
|
||||
|
||||
## 0.6.0
|
||||
- 增加`OmniConstructor`和`OmniAccessor`,支持快速构建方法入参
|
||||
- 修复一处`FRAME FULL`字节码处理的异常 (issue-117)
|
||||
@@ -110,26 +132,26 @@
|
||||
- 重名了现有的各种注解,以便与更好的与实际功能对应
|
||||
|
||||
## v0.1.0
|
||||
- move generated agent jar to class folder
|
||||
- support mock method of any object
|
||||
- 将生成的Agent Jar包自动移到打包工程的Class目录
|
||||
- 支持Mock任意类型的方法调用(不再局限于被测类中的方法)
|
||||
|
||||
## 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
|
||||
- 使用运行期字节码修改替代预先生成的`e.java`文件
|
||||
- 消除所有对特定单元测试框架的定制逻辑
|
||||
- 将测试类中的Testable引用字段从编译期生成改为运行期生成
|
||||
|
||||
## v0.0.4
|
||||
- use runtime byte code rewrite to invoke testable setup method
|
||||
- add `TestableUtil` class to fetch current test case and invocation source
|
||||
- 改用运行期字节码修改来调用Testable的`setup`方法
|
||||
- 添加`TestableUtil`工具类用于获取当前测试用例和调用来源
|
||||
|
||||
## 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
|
||||
- 使用静态方法封装私有成员的访问过程,不在原调用处直接添加反射代码
|
||||
- 使用`e.java`文件替代`testable`类型,提高代码可读性
|
||||
- 引入`agent`模块,使用运行期字节码修改实现`new`操作和私有成员调用的Mock
|
||||
|
||||
## v0.0.2
|
||||
- add support of member method mocking by compile time code modification
|
||||
- 基于编译期代码修改支持Mock私有成员方法的调用
|
||||
|
||||
## v0.0.1
|
||||
- PoC version
|
||||
- use compile time code modification to support new operation mocking and private field & method access
|
||||
- 首个概念验证版本
|
||||
- 基于编译期代码修改支持`new`操作的Mock和访问被测类私有成员
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
使用TestableMock
|
||||
---
|
||||
|
||||
`TestableMock`是基于源码和字节码增强的Java单元测试辅助工具,包含以下功能:
|
||||
`TestableMock`现在已不仅是一款轻量易上手的单元测试Mock工具,更是以**简化Java单元测试**为目标的综合辅助工具集,包含以下功能:
|
||||
|
||||
- [快速Mock任意调用](zh-cn/doc/use-mock.md):使被测类的任意方法调用快速替换为Mock方法,实现"指哪换哪",解决传统Mock工具使用繁琐的问题
|
||||
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.6.0</testable.version>
|
||||
<testable.version>0.6.4</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.0')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.0')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.6.4')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.4')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -93,3 +93,20 @@ test {
|
||||
> ```
|
||||
>
|
||||
> 完整示例参考[issue-43](https://github.com/alibaba/testable-mock/issues/43)
|
||||
|
||||
> 若项目使用`Spock`测试框架,需指定`Groovy`编译生成的JVM 1.6或以上版本字节码,方法如下(请根据实际使用的JVM版本修改属性值)。
|
||||
>
|
||||
> Maven项目在`pom.xml`中添加`<maven.compiler.source>`和`<maven.compiler.target>`属性,例如:
|
||||
> ```xml
|
||||
> <properties>
|
||||
> <maven.compiler.source>1.6</maven.compiler.source>
|
||||
> <maven.compiler.target>1.6</maven.compiler.target>
|
||||
> </properties>
|
||||
> ```
|
||||
>
|
||||
> Gradle项目在`build.gradle`中添加`sourceCompatibility`属性,例如:
|
||||
> ```groovy
|
||||
> sourceCompatibility = '6'
|
||||
> ```
|
||||
>
|
||||
> 完整代码可参考`demo/spock-demo`示例项目。
|
||||
|
||||
@@ -3,51 +3,45 @@
|
||||
|
||||
相比`Mockito`等由开发者手工放置Mock类的做法,`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也容易导致对“Mock究竟有没生效”的疑问。
|
||||
|
||||
若要排查Mock相关的问题,只需在相应的**Mock容器类**上添加`@MockDiagnose`注解,并配置参数值为`LogLevel.ENABLE`,在运行测试时就会打印出详细的Mock方法替换过程。例如:
|
||||
为此,`TestableMock`会在项目构建目录下自动保存最后一次测试运行过程的Mock扫描日志。默认位置为`target/testable-agent.log`(Maven项目)或`build/testable-agent.log`(Gradle项目)。
|
||||
|
||||
```java
|
||||
class DemoTest {
|
||||
@MockDiagnose(LogLevel.ENABLE)
|
||||
public static class Mock {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出日志示例如下:
|
||||
日志内容示例如下:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[DIAGNOSE] Found 6 test cases
|
||||
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
[DIAGNOSE] Line 27, mock method "staticFunc" used
|
||||
[DIAGNOSE] Handling method commonFunc
|
||||
[DIAGNOSE] Line 34, mock method "trim" used
|
||||
[DIAGNOSE] Line 34, mock method "sub" used
|
||||
[DIAGNOSE] Line 34, mock method "startsWith" used
|
||||
[INFO] Start at Mon Jan 00 00:00:00 CST 0000
|
||||
... ...
|
||||
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[INFO] Found 6 test cases
|
||||
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
|
||||
[INFO] Found 8 mock methods
|
||||
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
|
||||
[INFO] Found method <init>
|
||||
[INFO] Found method newFunc
|
||||
[INFO] Line 19, mock method "createBlackBox" used
|
||||
[INFO] Found method outerFunc
|
||||
[INFO] Line 27, mock method "innerFunc" used
|
||||
[INFO] Line 27, mock method "staticFunc" used
|
||||
[INFO] Found method commonFunc
|
||||
[INFO] Line 34, mock method "trim" used
|
||||
[INFO] Line 34, mock method "sub" used
|
||||
[INFO] Line 34, mock method "startsWith" used
|
||||
... ...
|
||||
[INFO] Completed at Mon Jan 00 00:00:00 CST 0000
|
||||
```
|
||||
|
||||
其中`Line XX, mock method "XXX" used`日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
|
||||
|
||||
简单排查方法:
|
||||
依据需排查的测试类,进行针对性排查。假设被测类为"com.demo.BizService",测试类为"com.demo.BizServiceTest",Mock容器类为"com.demo.BizServiceTest.Mock":
|
||||
|
||||
- 若没有任何输出,请检查`pom.xml`或`build.gradle`配置是否正确引入了`TestableMock`依赖
|
||||
- 若只输出了`Handling mock class`,请检查Mock容器类的名称和位置是否符合规范
|
||||
- 若只输出了`Handling mock class`和`Handling test class`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test",或者是否正确的使用了`@MockWith`注解
|
||||
- 若输出了`Handling source class`以及`Handling method xxx`,但预期的代码行位置没有发生Mock替换,请继续检查Mock方法定义是否未与目标方法匹配
|
||||
- 若该日志文件未生成,请检查`pom.xml`或`build.gradle`配置是否正确引入了`TestableMock`依赖
|
||||
- 若日志中只能找到`com/demo/BizServiceTest$Mock`,没有找到被测类和测试类,请检查Mock容器类的名称和位置是否符合规范
|
||||
- 若日志中找到了Mock类和测试类,但没有找到被测类`com/demo/BizService`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test",或者是否正确的使用了`@MockWith`注解
|
||||
- 若日志中三个类都已经找到,且有`Found method xxx`,但预期的代码行位置没有发生Mock替换,请继续检查Mock方法定义是否未与目标方法匹配
|
||||
|
||||
对于上述的最后一种情况(预期Mock未生效),可将日志级别提升到`LogLevel.VERBOSE`做进一步排查。例如:
|
||||
对于上述的最后一种情况(预期Mock未生效),可使用`@MockDiagnose`注解将相应Mock容器类的日志级别提升到`LogLevel.VERBOSE`做进一步排查。例如:
|
||||
|
||||
```java
|
||||
class DemoTest {
|
||||
class BizServiceTest {
|
||||
@MockDiagnose(LogLevel.VERBOSE)
|
||||
public static class Mock {
|
||||
...
|
||||
@@ -55,39 +49,43 @@ class DemoTest {
|
||||
}
|
||||
```
|
||||
|
||||
再次执行单元测试,此时将会打印出所有Mock方法的签名定义,以及被测类中扫描到所有调用的实际方法签名:
|
||||
再次执行单元测试,此时日志将会包含所有该Mock类中的方法签名定义,以及被测类中扫描到所有调用的实际方法签名:
|
||||
|
||||
```text
|
||||
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[VERBOSE] Test case "should_mock_new_object"
|
||||
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
|
||||
[TIP] Test case "should_mock_new_object"
|
||||
... ...
|
||||
[VERBOSE] Test case "should_set_mock_context"
|
||||
[DIAGNOSE] Found 6 test cases
|
||||
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
|
||||
[VERBOSE] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
|
||||
[VERBOSE] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
|
||||
[TIP] Test case "should_set_mock_context"
|
||||
[INFO] Found 6 test cases
|
||||
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
|
||||
[TIP] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
|
||||
[TIP] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
|
||||
... ...
|
||||
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
|
||||
[DIAGNOSE] Found 8 mock methods
|
||||
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
|
||||
[DIAGNOSE] Handling method <init>
|
||||
[VERBOSE] Line 13, constructing "java.lang.Object()"
|
||||
[DIAGNOSE] Handling method newFunc
|
||||
[VERBOSE] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
|
||||
[DIAGNOSE] Line 19, mock method "createBlackBox" used
|
||||
[VERBOSE] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
|
||||
[VERBOSE] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
|
||||
[DIAGNOSE] Handling method outerFunc
|
||||
[VERBOSE] Line 27, constructing "java.lang.StringBuilder()"
|
||||
[VERBOSE] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
|
||||
[VERBOSE] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
|
||||
[DIAGNOSE] Line 27, mock method "innerFunc" used
|
||||
[TIP] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
|
||||
[INFO] Found 8 mock methods
|
||||
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
|
||||
[INFO] Found method <init>
|
||||
[TIP] Line 13, constructing "java.lang.Object()"
|
||||
[INFO] Found method newFunc
|
||||
[TIP] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
|
||||
[INFO] Line 19, mock method "createBlackBox" used
|
||||
[TIP] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
|
||||
[TIP] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
|
||||
[INFO] Found method outerFunc
|
||||
[TIP] Line 27, constructing "java.lang.StringBuilder()"
|
||||
[TIP] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
|
||||
[TIP] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
|
||||
[INFO] Line 27, mock method "innerFunc" used
|
||||
... ...
|
||||
```
|
||||
|
||||
输出日志结构参考如下:
|
||||
|
||||
- `Mock constructor "<Mock方法名>" as "<方法签名>" for "<类型>"` 在测试类中扫描到的**Mock构造方法**及其签名
|
||||
- `Mock method "<Mock方法名>" as "<方法签名>"` 在测试类中扫描到的**普通Mock方法**及其签名
|
||||
- `Mock constructor "<Mock方法名>" as "<方法签名>" for "<类型>"` 在Mock类中扫描到的**Mock构造方法**及其签名
|
||||
- `Mock method "<Mock方法名>" as "<方法签名>"` 在Mock类中扫描到的**普通Mock方法**及其签名
|
||||
- `Line XX, constructing "<类型>" as "<方法签名>"` 在被测类中扫描掉的**构造方法调用**及其签名
|
||||
- `Line XX, invoking "<方法名>" as "<方法签名>"` 在被测类中扫描到的**成员方法调用**及其签名
|
||||
|
||||
> 为了便于清楚的区分返回值类型和调用目标类型,日志中记录的方法签名采用了类似`Kotlin`的方法定义结构。
|
||||
|
||||
对比原调用的实际签名和Mock方法定义的签名,通常很快就能够找出问题所在。
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
快速Mock被测类的任意方法调用
|
||||
---
|
||||
|
||||
相比以往Mock工具以类为粒度的Mock方式,`TestableMock`允许用户直接定义需要Mock的单个方法,并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测方法中的指定方法调用。
|
||||
在单元测试中,Mock方法的主要作用是替代某些**需要外部依赖**、**执行过程耗时**、**执行结果随机**或其他影响测试正常开展,却并不影响关键待测逻辑的调用。通常来说,某个调用需要被Mock,往往只与其自身特征有关,而与调用的来源无关。
|
||||
|
||||
> 规则归纳起来就两条:
|
||||
基于上述特点,`TestableMock`设计了一种极简的Mock机制。与以往Mock工具以**类**作为Mock的定义粒度,在每个测试用例里各自重复描述Mock行为的方式不同,`TestableMock`让每个业务类(被测类)关联一组可复用的Mock方法集合(使用Mock容器类承载),并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测类中的指定方法调用。
|
||||
|
||||
> 实际规则约定归纳起来只有两条:
|
||||
> - Mock非构造方法,拷贝原方法定义到Mock容器类,加`@MockMethod`注解
|
||||
> - Mock构造方法,拷贝原方法定义到Mock容器类,返回值换成构造的类型,方法名随意,加`@MockContructor`注解
|
||||
|
||||
具体的Mock方法定义约定如下。
|
||||
具体使用方法如下。
|
||||
|
||||
### 0. 前置步骤,准备Mock容器
|
||||
|
||||
@@ -29,11 +31,11 @@ public class DemoTest {
|
||||
|
||||
此时被测类中所有对该需覆写方法的调用,将在单元测试运行时,将自动被替换为对上述自定义Mock方法的调用。
|
||||
|
||||
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在Mock容器类定义如下方法:
|
||||
例如,被测类中有一处`"something".substring(0, 4)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在Mock容器类定义如下方法:
|
||||
|
||||
```java
|
||||
// 原方法签名为`String substring(int, int)`
|
||||
// 调用此方法的对象`"anything"`类型为`String`
|
||||
// 调用此方法的对象`"something"`类型为`String`
|
||||
@MockMethod(targetClass = String.class)
|
||||
private String substring(int i, int j) {
|
||||
return "sub_string";
|
||||
@@ -55,7 +57,7 @@ private String use_any_mock_method_name(int i, int j) {
|
||||
|
||||
有时,在Mock方法里会需要访问发起调用的原始对象中的成员变量,或是调用原始对象的其他方法。此时,可以将`@MockMethod`注解中的`targetClass`参数去除,然后在方法参数列表首位增加一个类型为该方法原本所属对象类型的参数。
|
||||
|
||||
`TestableMock`约定,当`@MockMethod`注解的`targetClass`参数值为空时,Mock方法的首位参数即为目标方法所属类型,参数名称随意。通常为了便于代码阅读,建议将此参数统一命名为`self`或`src`。举例如下:
|
||||
`TestableMock`约定,当`@MockMethod`注解的`targetClass`参数未定义时,Mock方法的首位参数即为目标方法所属类型,参数名称随意。通常为了便于代码阅读,建议将此参数统一命名为`self`或`src`。举例如下:
|
||||
|
||||
```java
|
||||
// Mock方法在参数列表首位增加一个类型为`String`的参数(名字随意)
|
||||
@@ -67,7 +69,7 @@ private String substring(String self, int i, int j) {
|
||||
}
|
||||
```
|
||||
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_mock_common_method()`测试用例。(由于Kotlin对String类型进行了魔改,故Kotlin示例中将被测方法在`BlackBox`类里加了一层封装)
|
||||
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_mock_common_method()`测试用例。(由于`Kotlin`对String类型进行了魔改,故`Kotlin`示例中将被测方法在`BlackBox`类里加了一层封装)
|
||||
|
||||
### 1.2 覆写被测类自身的成员方法
|
||||
|
||||
@@ -161,16 +163,15 @@ private Data mockDemo() {
|
||||
|
||||
### 3. 验证Mock方法被调用的顺序和参数
|
||||
|
||||
在测试用例中可用通过`TestableTool.verify()`方法,配合`with()`、`withInOrder()`、`without()`、`withTimes()`等方法实现对Mock调用情况的验证。
|
||||
在测试用例中可用通过`InvokeVerifier.verify()`方法,配合`with()`、`withInOrder()`、`without()`、`withTimes()`等方法实现对Mock调用情况的验证。
|
||||
|
||||
详见[校验Mock调用](zh-cn/doc/matcher.md)文档。
|
||||
|
||||
|
||||
### 4. 特别说明
|
||||
|
||||
> **Mock只对被测类的代码有效**
|
||||
>
|
||||
> 在`TestableMock`的[Issues](https://github.com/alibaba/testable-mock/issues)列表中,最常见的一类问题是“Mock为什么没生效”,其中最多的一种情况是“在测试用例里直接调用了Mock的方法,发现没有替换”。这是因为<u>Mock替换只会作用在**被测类**的代码里</u>哦(~ ̄▽ ̄)~。知道大家是想快速验证一下`TestableMock`的功能,不过测试用例的代码真滴无需被Mock(这心意我们领了👻)。
|
||||
> 在`TestableMock`的[Issues](https://github.com/alibaba/testable-mock/issues)列表中,最常见的一类问题是“Mock为什么没生效”,其中最多的一种情况是“在测试用例里直接调用了Mock的方法,发现没有替换”。这是因为<u>Mock替换只会作用在**被测类**的代码里</u>。知道大家是想快速验证一下`TestableMock`的功能(这心意我们领了👻),不过测试用例的代码真的无需被Mock哦(~ ̄▽ ̄)~。
|
||||
>
|
||||
> 除去这种情况,若Mock未生效,请参考[自助问题排查](zh-cn/doc/troubleshooting.md)提供的方法对比<u>Mock方法签名</u>和<u>目标位置的调用方法签名</u>。若依然无法定位原因,欢迎提交Issues告诉我们。
|
||||
|
||||
@@ -180,3 +181,5 @@ private Data mockDemo() {
|
||||
> 同时约定测试类关联的Mock容器为<u>在其内部且名为`Mock`的静态类</u>,或<u>相同包路径下名为`被测类名+Mock`的独立类</u>。
|
||||
>
|
||||
> 当测试类或Mock容器路径不符合此约定时,可使用`@MockWith`注解显式指定,详见[使用MockWith注解](zh-cn/doc/use-mock-with.md)。
|
||||
|
||||
关于`TestableMock`的更多实现细节可参考[设计和原理](zh-cn/doc/design-and-mechanism.md)文档。
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.6.0</version>
|
||||
<version>0.6.4</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.alibaba.testable.agent;
|
||||
|
||||
import com.alibaba.testable.agent.config.ArgumentParser;
|
||||
import com.alibaba.testable.agent.config.PropertiesParser;
|
||||
import com.alibaba.testable.agent.transformer.TestableClassTransformer;
|
||||
import com.alibaba.testable.agent.util.GlobalConfig;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import com.alibaba.ttl.threadpool.agent.TtlAgent;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
@@ -13,51 +15,26 @@ import java.lang.instrument.Instrumentation;
|
||||
*/
|
||||
public class PreMain {
|
||||
|
||||
private static final String AND = "&";
|
||||
private static final String USE_THREAD_POOL = "useThreadPool";
|
||||
private static final String LOG_LEVEL = "logLevel";
|
||||
private static final String DUMP_PATH = "dumpPath";
|
||||
private static final String PKG_PREFIX = "pkgPrefix";
|
||||
private static final String MOCK_SCOPE = "mockScope";
|
||||
private static final String EQUAL = "=";
|
||||
private static boolean enhanceThreadLocal = false;
|
||||
|
||||
public static void premain(String agentArgs, Instrumentation inst) {
|
||||
parseArgs(agentArgs);
|
||||
if (enhanceThreadLocal) {
|
||||
ArgumentParser.parseArgs(agentArgs);
|
||||
PropertiesParser.parseFile(ArgumentParser.configFilePath);
|
||||
GlobalConfig.setupLogRootPath();
|
||||
if (GlobalConfig.shouldEnhanceThreadLocal()) {
|
||||
// add transmittable thread local transformer
|
||||
TtlAgent.premain(agentArgs, inst);
|
||||
}
|
||||
// add testable mock transformer
|
||||
inst.addTransformer(new TestableClassTransformer());
|
||||
cleanup();
|
||||
}
|
||||
|
||||
private static void parseArgs(String args) {
|
||||
if (args == null) {
|
||||
return;
|
||||
}
|
||||
for (String a : args.split(AND)) {
|
||||
int i = a.indexOf(EQUAL);
|
||||
if (i > 0) {
|
||||
// parameter with key = value
|
||||
String k = a.substring(0, i);
|
||||
String v = a.substring(i + 1);
|
||||
if (k.equals(LOG_LEVEL)) {
|
||||
GlobalConfig.setLogLevel(v);
|
||||
} else if (k.equals(DUMP_PATH)) {
|
||||
GlobalConfig.setDumpPath(v);
|
||||
} else if (k.equals(PKG_PREFIX)) {
|
||||
GlobalConfig.setPkgPrefix(v);
|
||||
} else if (k.equals(MOCK_SCOPE)) {
|
||||
GlobalConfig.setDefaultMockScope(MockScope.of(v));
|
||||
}
|
||||
} else {
|
||||
// parameter with single value
|
||||
if (a.equals(USE_THREAD_POOL)) {
|
||||
enhanceThreadLocal = true;
|
||||
}
|
||||
private static void cleanup() {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
LogUtil.cleanup();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.alibaba.testable.agent.config;
|
||||
|
||||
import com.alibaba.testable.agent.util.GlobalConfig;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
|
||||
public class ArgumentParser {
|
||||
|
||||
private static final String AND = "&";
|
||||
private static final String USE_THREAD_POOL = "useThreadPool";
|
||||
private static final String LOG_LEVEL = "logLevel";
|
||||
private static final String LOG_FILE = "logFile";
|
||||
private static final String DUMP_PATH = "dumpPath";
|
||||
private static final String PKG_PREFIX = "pkgPrefix";
|
||||
private static final String MOCK_SCOPE = "mockScope";
|
||||
private static final String CONFIG_FILE = "configFile";
|
||||
private static final String EQUAL = "=";
|
||||
public static String configFilePath = null;
|
||||
|
||||
public static void parseArgs(String args) {
|
||||
if (args == null) {
|
||||
return;
|
||||
}
|
||||
for (String a : args.split(AND)) {
|
||||
int i = a.indexOf(EQUAL);
|
||||
if (i > 0) {
|
||||
// parameter with key = value
|
||||
String k = a.substring(0, i);
|
||||
String v = a.substring(i + 1);
|
||||
if (k.equals(LOG_LEVEL)) {
|
||||
GlobalConfig.setLogLevel(v);
|
||||
} else if (k.equals(LOG_FILE)) {
|
||||
GlobalConfig.setLogFile(v);
|
||||
} else if (k.equals(DUMP_PATH)) {
|
||||
GlobalConfig.setDumpPath(v);
|
||||
} else if (k.equals(PKG_PREFIX)) {
|
||||
GlobalConfig.setPkgPrefixWhiteList(v);
|
||||
} else if (k.equals(MOCK_SCOPE)) {
|
||||
GlobalConfig.setDefaultMockScope(MockScope.of(v));
|
||||
} else if (k.equals(CONFIG_FILE)) {
|
||||
configFilePath = v;
|
||||
}
|
||||
} else {
|
||||
// parameter with single value
|
||||
if (a.equals(USE_THREAD_POOL)) {
|
||||
GlobalConfig.enableEnhanceThreadLocal(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.alibaba.testable.agent.config;
|
||||
|
||||
import com.alibaba.testable.agent.util.GlobalConfig;
|
||||
import com.alibaba.testable.agent.util.PathUtil;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Properties;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.PROPERTY_USER_DIR;
|
||||
|
||||
public class PropertiesParser {
|
||||
|
||||
private static final String DEFAULT_CONFIG_FILE = "src/test/resources/testable.properties";
|
||||
private static final String DUMP_PATH = "dump.path";
|
||||
private static final String PKG_PREFIX_INCLUDES = "enhance.pkgPrefix.includes";
|
||||
private static final String LOG_FILE = "log.file";
|
||||
private static final String LOG_LEVEL = "log.level";
|
||||
private static final String INNER_MOCK_CLASS_NAME = "mock.innerClass.name";
|
||||
private static final String DEFAULT_MOCK_SCOPE = "mock.scope.default";
|
||||
private static final String ENABLE_OMNI_INJECT = "omni.constructor.enhance.enable";
|
||||
private static final String OMNI_INJECT_EXCLUDES = "omni.constructor.enhance.pkgPrefix.excludes";
|
||||
private static final String ENABLE_THREAD_POOL = "thread.pool.enhance.enable";
|
||||
|
||||
public static void parseFile(String configFilePath) {
|
||||
String path = (configFilePath == null) ? DEFAULT_CONFIG_FILE : configFilePath;
|
||||
String fullPath = PathUtil.join(System.getProperty(PROPERTY_USER_DIR), path);
|
||||
Properties pps = new Properties();
|
||||
try {
|
||||
InputStream in = new BufferedInputStream(new FileInputStream(fullPath));
|
||||
pps.load(in);
|
||||
LogUtil.verbose("Loaded configure file %s", fullPath);
|
||||
} catch (IOException e) {
|
||||
if (configFilePath == null) {
|
||||
LogUtil.verbose("No configure file found, skip.");
|
||||
} else {
|
||||
LogUtil.warn("No configure file found at %s", fullPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
parsePropertiesContent(pps);
|
||||
}
|
||||
|
||||
private static void parsePropertiesContent(Properties pps) {
|
||||
Enumeration<?> en = pps.propertyNames();
|
||||
while(en.hasMoreElements()) {
|
||||
String k = (String)en.nextElement();
|
||||
String v = pps.getProperty(k);
|
||||
if (k.equals(DUMP_PATH)) {
|
||||
GlobalConfig.setDumpPath(v);
|
||||
} else if (k.equals(PKG_PREFIX_INCLUDES)) {
|
||||
GlobalConfig.setPkgPrefixWhiteList(v);
|
||||
} else if (k.equals(LOG_FILE)) {
|
||||
GlobalConfig.setLogFile(v);
|
||||
} else if (k.equals(LOG_LEVEL)) {
|
||||
GlobalConfig.setLogLevel(v);
|
||||
} else if (k.equals(INNER_MOCK_CLASS_NAME)) {
|
||||
GlobalConfig.setInnerMockClassName(v);
|
||||
} else if (k.equals(DEFAULT_MOCK_SCOPE)) {
|
||||
GlobalConfig.setDefaultMockScope(MockScope.of(v));
|
||||
} else if (k.equals(ENABLE_OMNI_INJECT)) {
|
||||
GlobalConfig.enableEnhanceOmniConstructor(Boolean.parseBoolean(v));
|
||||
} else if (k.equals(OMNI_INJECT_EXCLUDES)) {
|
||||
GlobalConfig.setOmniPkgPrefixBlackList(v);
|
||||
} else if (k.equals(ENABLE_THREAD_POOL)) {
|
||||
GlobalConfig.enableEnhanceThreadLocal(Boolean.parseBoolean(v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,12 +9,16 @@ public class ConstPool {
|
||||
public static final String FIELD_TARGET_CLASS = "targetClass";
|
||||
public static final String FIELD_SCOPE = "scope";
|
||||
|
||||
public static final String PROPERTY_USER_DIR = "user.dir";
|
||||
public static final String PROPERTY_TEMP_DIR = "java.io.tmpdir";
|
||||
|
||||
public static final String MOCK_WITH = "com.alibaba.testable.core.annotation.MockWith";
|
||||
public static final String DUMP_TO = "com.alibaba.testable.core.annotation.DumpTo";
|
||||
public static final String MOCK_DIAGNOSE = "com.alibaba.testable.core.annotation.MockDiagnose";
|
||||
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 CGLIB_CLASS_PATTERN = "$$EnhancerBy";
|
||||
public static final String KOTLIN_POSTFIX_COMPANION = "$Companion";
|
||||
public static final String KOTLIN_PREFIX_ACCESS = "access$";
|
||||
|
||||
|
||||
@@ -101,7 +101,8 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
// must get label before method description changed
|
||||
ImmutablePair<LabelNode, LabelNode> labels = getStartAndEndLabel(mn);
|
||||
mn.desc = MethodUtil.addParameterAtBegin(mn.desc, targetClassName);
|
||||
int parameterOffset = MethodUtil.isStatic(mn) ? 0 : 1;
|
||||
// in certain case, local variable table of non-static method can be empty (issue-136)
|
||||
int parameterOffset = MethodUtil.isStatic(mn) ? 0 : Math.min(mn.localVariables.size(), 1);
|
||||
mn.localVariables.add(parameterOffset, new LocalVariableNode(SELF_REF, targetClassName, null,
|
||||
labels.left, labels.right, parameterOffset));
|
||||
for (int i = parameterOffset + 1; i < mn.localVariables.size(); i++) {
|
||||
@@ -113,7 +114,9 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
} else if (in instanceof VarInsnNode && ((VarInsnNode)in).var >= parameterOffset) {
|
||||
((VarInsnNode)in).var++;
|
||||
} else if (in instanceof FrameNode && ((FrameNode)in).type == F_FULL) {
|
||||
((FrameNode)in).local.add(parameterOffset, ClassUtil.toSlashSeparateJavaStyleName(targetClassName));
|
||||
// For groovy adaptation (issue-121)
|
||||
int pos = ((FrameNode)in).local.size() == 0 ? 0 : parameterOffset;
|
||||
((FrameNode)in).local.add(pos, ClassUtil.toSlashSeparateJavaStyleName(targetClassName));
|
||||
}
|
||||
}
|
||||
mn.maxLocals++;
|
||||
@@ -121,7 +124,8 @@ public class MockClassHandler extends BaseClassWithContextHandler {
|
||||
}
|
||||
|
||||
private ImmutablePair<LabelNode, LabelNode> getStartAndEndLabel(MethodNode mn) {
|
||||
if (MethodUtil.isStatic(mn)) {
|
||||
// in certain case, local variable table of non-static method can be empty (issue-136)
|
||||
if (MethodUtil.isStatic(mn) || mn.localVariables.isEmpty()) {
|
||||
LabelNode startLabel = null, endLabel = null;
|
||||
for (AbstractInsnNode n = mn.instructions.getFirst(); n != null; n = n.getNext()) {
|
||||
if (n instanceof LabelNode) {
|
||||
|
||||
@@ -18,12 +18,13 @@ import static com.alibaba.testable.core.constant.ConstPool.THIS_REF;
|
||||
*/
|
||||
public class OmniClassHandler extends BaseClassHandler {
|
||||
|
||||
private static final String TESTABLE_NULL_TYPE = "com.alibaba.testable.core.model.TestableNull";
|
||||
private static final String NULL_TYPE = "javax.lang.model.type.NullType";
|
||||
private static final String VOID_TYPE = "java/lang/Void";
|
||||
private static final String IGNORE = "ignore";
|
||||
private static final String METHOD_START = "(";
|
||||
private static final String VOID_METHOD_END = ")V";
|
||||
private static final String VOID_METHOD = "()V";
|
||||
private static final String ENABLE_CONFIGURATION = "Lorg/springframework/context/annotation/Configuration;";
|
||||
private static final String CLASS_ABSTRACT_COLLECTION = "java/util/AbstractCollection";
|
||||
|
||||
private static final String[] JUNIT_TEST_ANNOTATIONS = new String[] {
|
||||
JUnit4Framework.ANNOTATION_TEST, JUnit5Framework.ANNOTATION_TEST, JUnit5Framework.ANNOTATION_PARAMETERIZED_TEST
|
||||
@@ -31,37 +32,59 @@ public class OmniClassHandler extends BaseClassHandler {
|
||||
|
||||
@Override
|
||||
protected void transform(ClassNode cn) {
|
||||
if ((cn.access & ACC_INTERFACE) != 0 || cn.superName == null) {
|
||||
if (isInterface(cn) || isJunitTestClass(cn) || isUninstantiableClass(cn) || hasSpecialAnnotation(cn)) {
|
||||
return;
|
||||
}
|
||||
addConstructorWithVoidTypeParameter(cn);
|
||||
}
|
||||
|
||||
private void addConstructorWithVoidTypeParameter(ClassNode cn) {
|
||||
MethodNode constructor = new MethodNode(ACC_PUBLIC, CONSTRUCTOR,
|
||||
METHOD_START + ClassUtil.toByteCodeClassName(VOID_TYPE) + VOID_METHOD_END, null, null);
|
||||
LabelNode start = new LabelNode(new Label());
|
||||
LabelNode end = new LabelNode(new Label());
|
||||
if (cn.superName.equals(CLASS_OBJECT)) {
|
||||
// JUnit require test class contains only one constructor
|
||||
if (isJUnitTestClass(cn)) {
|
||||
return;
|
||||
}
|
||||
MethodNode constructor = new MethodNode(ACC_PUBLIC, CONSTRUCTOR,
|
||||
METHOD_START + ClassUtil.toByteCodeClassName(NULL_TYPE) + VOID_METHOD_END, null, null);
|
||||
LabelNode start = new LabelNode(new Label());
|
||||
LabelNode end = new LabelNode(new Label());
|
||||
constructor.instructions = invokeSuperWithoutParameter(start, end);
|
||||
constructor.localVariables = createLocalVariables(cn, start, end);
|
||||
constructor.maxStack = 1;
|
||||
constructor.maxLocals = 2;
|
||||
cn.methods.add(constructor);
|
||||
} else {
|
||||
MethodNode constructor = new MethodNode(ACC_PUBLIC, CONSTRUCTOR,
|
||||
METHOD_START + ClassUtil.toByteCodeClassName(NULL_TYPE) + VOID_METHOD_END, null, null);
|
||||
LabelNode start = new LabelNode(new Label());
|
||||
LabelNode end = new LabelNode(new Label());
|
||||
constructor.instructions = invokeSuperWithTestableNullParameter(cn, start, end);
|
||||
constructor.instructions = invokeSuperWithTestableVoidParameter(cn, start, end);
|
||||
constructor.localVariables = createLocalVariables(cn, start, end);
|
||||
constructor.maxStack = 3;
|
||||
constructor.maxLocals = 2;
|
||||
cn.methods.add(constructor);
|
||||
}
|
||||
constructor.maxLocals = 2;
|
||||
cn.methods.add(constructor);
|
||||
}
|
||||
|
||||
private boolean isJUnitTestClass(ClassNode cn) {
|
||||
private boolean hasSpecialAnnotation(ClassNode cn) {
|
||||
if (cn.visibleAnnotations == null) {
|
||||
return false;
|
||||
}
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
if (an.desc.equals(ENABLE_CONFIGURATION)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isUninstantiableClass(ClassNode cn) {
|
||||
// if the class has no even default constructor, skip it
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (mn.name.equals(CONSTRUCTOR)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isInterface(ClassNode cn) {
|
||||
// is interface, Object class or Void class
|
||||
return (cn.access & ACC_INTERFACE) != 0 || cn.superName == null || VOID_TYPE.equals(cn.name);
|
||||
}
|
||||
|
||||
private boolean isJunitTestClass(ClassNode cn) {
|
||||
// junit require test class contains only one constructor
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (mn.visibleAnnotations == null) {
|
||||
continue;
|
||||
@@ -87,16 +110,17 @@ public class OmniClassHandler extends BaseClassHandler {
|
||||
return il;
|
||||
}
|
||||
|
||||
private InsnList invokeSuperWithTestableNullParameter(ClassNode cn, LabelNode start, LabelNode end) {
|
||||
private InsnList invokeSuperWithTestableVoidParameter(ClassNode cn, LabelNode start, LabelNode end) {
|
||||
InsnList il = new InsnList();
|
||||
il.add(start);
|
||||
il.add(new VarInsnNode(ALOAD, 0));
|
||||
il.add(new TypeInsnNode(NEW, ClassUtil.toSlashSeparatedName(TESTABLE_NULL_TYPE)));
|
||||
il.add(new InsnNode(DUP));
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, ClassUtil.toSlashSeparatedName(TESTABLE_NULL_TYPE), CONSTRUCTOR,
|
||||
VOID_METHOD, false));
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, cn.superName, CONSTRUCTOR,
|
||||
METHOD_START + ClassUtil.toByteCodeClassName(NULL_TYPE) + VOID_METHOD_END, false));
|
||||
if (cn.superName.equals(CLASS_ABSTRACT_COLLECTION)) {
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, cn.superName, CONSTRUCTOR, VOID_METHOD, false));
|
||||
} else {
|
||||
il.add(new VarInsnNode(ALOAD, 1));
|
||||
il.add(new MethodInsnNode(INVOKESPECIAL, cn.superName, CONSTRUCTOR,
|
||||
METHOD_START + ClassUtil.toByteCodeClassName(VOID_TYPE) + VOID_METHOD_END, false));
|
||||
}
|
||||
il.add(new InsnNode(RETURN));
|
||||
il.add(end);
|
||||
return il;
|
||||
@@ -105,7 +129,7 @@ public class OmniClassHandler extends BaseClassHandler {
|
||||
private List<LocalVariableNode> createLocalVariables(ClassNode cn, LabelNode start, LabelNode end) {
|
||||
return CollectionUtil.listOf(
|
||||
new LocalVariableNode(THIS_REF, ClassUtil.toByteCodeClassName(cn.name), null, start, end, 0),
|
||||
new LocalVariableNode(IGNORE, ClassUtil.toByteCodeClassName(NULL_TYPE), null, start, end, 1)
|
||||
new LocalVariableNode(IGNORE, ClassUtil.toByteCodeClassName(VOID_TYPE), null, start, end, 1)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
|
||||
private void transformMethod(MethodNode mn, Set<MethodInfo> memberInjectMethods,
|
||||
Set<MethodInfo> newOperatorInjectMethods) {
|
||||
LogUtil.diagnose(" Handling method %s", mn.name);
|
||||
LogUtil.diagnose(" Found method %s", mn.name);
|
||||
AbstractInsnNode[] instructions = mn.instructions.toArray();
|
||||
if (instructions.length == 0) {
|
||||
// native method (issue-52)
|
||||
|
||||
@@ -2,17 +2,12 @@ package com.alibaba.testable.agent.handler;
|
||||
|
||||
import com.alibaba.testable.agent.handler.test.*;
|
||||
import com.alibaba.testable.agent.model.TestCaseMethodType;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.alibaba.testable.core.constant.ConstPool.THIS_REF;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
@@ -24,12 +19,14 @@ public class TestClassHandler extends BaseClassWithContextHandler {
|
||||
private static final String DESC_METHOD_CLEAN = "()V";
|
||||
|
||||
private int testCaseCount = 0;
|
||||
private boolean shouldGenerateCleanupMethod = true;
|
||||
|
||||
private final Framework[] frameworkClasses = new Framework[] {
|
||||
new JUnit4Framework(),
|
||||
new JUnit5Framework(),
|
||||
new TestNgFramework(),
|
||||
new TestNgOnClassFramework()
|
||||
new TestNgOnClassFramework(),
|
||||
new SpockFramework()
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -40,17 +37,19 @@ public class TestClassHandler extends BaseClassWithContextHandler {
|
||||
protected void transform(ClassNode cn) {
|
||||
Framework framework = checkFramework(cn);
|
||||
if (framework == null) {
|
||||
LogUtil.warn("Failed to detect test framework for " + cn.name);
|
||||
LogUtil.warn("Failed to detect test framework for %s", cn.name);
|
||||
return;
|
||||
}
|
||||
if (!framework.hasTestAfterMethod) {
|
||||
addTestAfterMethod(cn, framework.getTestAfterAnnotation());
|
||||
}
|
||||
for (MethodNode mn : cn.methods) {
|
||||
handleTestableUtil(mn);
|
||||
handleTestCaseMethod(mn, framework);
|
||||
}
|
||||
LogUtil.diagnose(String.format(" Found %d test cases", testCaseCount));
|
||||
if (shouldGenerateCleanupMethod) {
|
||||
MethodNode cleanupMethod = framework.getCleanupMethod(cn.name);
|
||||
injectMockContextClean(cleanupMethod);
|
||||
cn.methods.add(cleanupMethod);
|
||||
}
|
||||
LogUtil.diagnose(" Found %d test cases", testCaseCount);
|
||||
}
|
||||
|
||||
private Framework checkFramework(ClassNode cn) {
|
||||
@@ -76,31 +75,15 @@ public class TestClassHandler extends BaseClassWithContextHandler {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addTestAfterMethod(ClassNode cn, String testAfterAnnotation) {
|
||||
MethodNode afterTestMethod = new MethodNode(ACC_PUBLIC, "testableAfterTestCase", "()V", null, null);
|
||||
afterTestMethod.visibleAnnotations = Collections.singletonList(new AnnotationNode(testAfterAnnotation));
|
||||
InsnList il = new InsnList();
|
||||
LabelNode startLabel = new LabelNode(new Label());
|
||||
LabelNode endLabel = new LabelNode(new Label());
|
||||
il.add(startLabel);
|
||||
il.add(new InsnNode(RETURN));
|
||||
il.add(endLabel);
|
||||
afterTestMethod.instructions = il;
|
||||
afterTestMethod.localVariables = Collections.singletonList(new LocalVariableNode(THIS_REF,
|
||||
ClassUtil.toByteCodeClassName(cn.name), null, startLabel, endLabel, 0));
|
||||
afterTestMethod.maxLocals = 1;
|
||||
afterTestMethod.maxStack = 0;
|
||||
cn.methods.add(afterTestMethod);
|
||||
}
|
||||
|
||||
private void handleTestCaseMethod(MethodNode mn, Framework framework) {
|
||||
TestCaseMethodType type = framework.checkMethodType(mn);
|
||||
if (type.equals(TestCaseMethodType.TEST)) {
|
||||
LogUtil.verbose(String.format(" Test case \"%s\"", mn.name));
|
||||
LogUtil.verbose(" Test case \"%s\"", mn.name);
|
||||
injectMockContextInit(mn);
|
||||
testCaseCount++;
|
||||
} else if (type.equals(TestCaseMethodType.AFTER_TEST)) {
|
||||
injectMockContextClean(mn);
|
||||
shouldGenerateCleanupMethod = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.alibaba.testable.agent.handler.test;
|
||||
|
||||
import com.alibaba.testable.agent.model.TestCaseMethodType;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.agent.util.CollectionUtil;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.alibaba.testable.core.constant.ConstPool.THIS_REF;
|
||||
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
|
||||
import static org.objectweb.asm.Opcodes.RETURN;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
abstract public class CommonFramework implements Framework {
|
||||
|
||||
private static final String DEFAULT_CLEANUP_METHOD = "testableCleanup";
|
||||
|
||||
@Override
|
||||
public boolean fit(Set<String> classAnnotations, Set<String> methodAnnotations) {
|
||||
return CollectionUtil.containsAny(methodAnnotations, getTestMethodAnnotations());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TestCaseMethodType checkMethodType(MethodNode mn) {
|
||||
if (mn.visibleAnnotations == null) {
|
||||
return TestCaseMethodType.OTHERS;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (getTestMethodAnnotations().contains(an.desc)) {
|
||||
return TestCaseMethodType.TEST;
|
||||
} else if (an.desc.equals(getCleanupMethodAnnotation())) {
|
||||
return TestCaseMethodType.AFTER_TEST;
|
||||
}
|
||||
}
|
||||
return TestCaseMethodType.OTHERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodNode getCleanupMethod(String className) {
|
||||
MethodNode cleanupMethod = new MethodNode(ACC_PUBLIC, DEFAULT_CLEANUP_METHOD, "()V", null, null);
|
||||
cleanupMethod.visibleAnnotations = Collections.singletonList(new AnnotationNode(getCleanupMethodAnnotation()));
|
||||
InsnList il = new InsnList();
|
||||
LabelNode startLabel = new LabelNode(new Label());
|
||||
LabelNode endLabel = new LabelNode(new Label());
|
||||
il.add(startLabel);
|
||||
il.add(new InsnNode(RETURN));
|
||||
il.add(endLabel);
|
||||
cleanupMethod.instructions = il;
|
||||
cleanupMethod.localVariables = Collections.singletonList(new LocalVariableNode(THIS_REF,
|
||||
ClassUtil.toByteCodeClassName(className), null, startLabel, endLabel, 0));
|
||||
cleanupMethod.maxLocals = 1;
|
||||
cleanupMethod.maxStack = 0;
|
||||
return cleanupMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all annotations that identify test case method
|
||||
* @return list of annotation full name
|
||||
*/
|
||||
public abstract List<String> getTestMethodAnnotations();
|
||||
|
||||
/**
|
||||
* Get annotation that identify test cleanup method
|
||||
* @return full name of cleanup method annotation
|
||||
*/
|
||||
public abstract String getCleanupMethodAnnotation();
|
||||
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
package com.alibaba.testable.agent.handler.test;
|
||||
|
||||
import com.alibaba.testable.agent.model.TestCaseMethodType;
|
||||
import com.alibaba.testable.agent.util.CollectionUtil;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
abstract public class Framework {
|
||||
|
||||
public boolean hasTestAfterMethod;
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public interface Framework {
|
||||
|
||||
/**
|
||||
* Check whether the test class using current test framework
|
||||
@@ -18,31 +16,20 @@ abstract public class Framework {
|
||||
* @param methodAnnotations annotations of all methods
|
||||
* @return fit or not
|
||||
*/
|
||||
public boolean fit(Set<String> classAnnotations, Set<String> methodAnnotations) {
|
||||
if (methodAnnotations.contains(getTestAfterAnnotation())) {
|
||||
hasTestAfterMethod = true;
|
||||
return true;
|
||||
} else {
|
||||
return CollectionUtil.containsAny(methodAnnotations, getTestAnnotations());
|
||||
}
|
||||
}
|
||||
boolean fit(Set<String> classAnnotations, Set<String> methodAnnotations);
|
||||
|
||||
public TestCaseMethodType checkMethodType(MethodNode mn) {
|
||||
if (mn.visibleAnnotations == null) {
|
||||
return TestCaseMethodType.OTHERS;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (getTestAnnotations().contains(an.desc)) {
|
||||
return TestCaseMethodType.TEST;
|
||||
} else if (an.desc.equals(getTestAfterAnnotation())) {
|
||||
return TestCaseMethodType.AFTER_TEST;
|
||||
}
|
||||
}
|
||||
return TestCaseMethodType.OTHERS;
|
||||
}
|
||||
/**
|
||||
* Check whether a method is test or cleanup method
|
||||
* @param mn method node
|
||||
* @return test method / cleanup method / other method
|
||||
*/
|
||||
TestCaseMethodType checkMethodType(MethodNode mn);
|
||||
|
||||
public abstract List<String> getTestAnnotations();
|
||||
|
||||
public abstract String getTestAfterAnnotation();
|
||||
/**
|
||||
* Generate cleanup method with correct name and annotations
|
||||
* @param className full name of test class
|
||||
* @return cleanup method for current framework
|
||||
*/
|
||||
MethodNode getCleanupMethod(String className);
|
||||
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@ package com.alibaba.testable.agent.handler.test;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class JUnit4Framework extends Framework {
|
||||
public class JUnit4Framework extends CommonFramework {
|
||||
|
||||
public static final String ANNOTATION_TEST = "Lorg/junit/Test;";
|
||||
private static final String ANNOTATION_AFTER_TEST = "Lorg/junit/After;";
|
||||
private static final String ANNOTATION_CLEANUP = "Lorg/junit/After;";
|
||||
|
||||
@Override
|
||||
public List<String> getTestAnnotations() {
|
||||
public List<String> getTestMethodAnnotations() {
|
||||
return Collections.singletonList(ANNOTATION_TEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTestAfterAnnotation() {
|
||||
return ANNOTATION_AFTER_TEST;
|
||||
public String getCleanupMethodAnnotation() {
|
||||
return ANNOTATION_CLEANUP;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,19 @@ package com.alibaba.testable.agent.handler.test;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class JUnit5Framework extends Framework {
|
||||
public class JUnit5Framework extends CommonFramework {
|
||||
|
||||
public static final String ANNOTATION_TEST = "Lorg/junit/jupiter/api/Test;";
|
||||
public static final String ANNOTATION_PARAMETERIZED_TEST = "Lorg/junit/jupiter/params/ParameterizedTest;";
|
||||
private static final String ANNOTATION_AFTER_TEST = "Lorg/junit/jupiter/api/AfterEach;";
|
||||
private static final String ANNOTATION_CLEANUP = "Lorg/junit/jupiter/api/AfterEach;";
|
||||
|
||||
@Override
|
||||
public List<String> getTestAnnotations() {
|
||||
public List<String> getTestMethodAnnotations() {
|
||||
return Arrays.asList(ANNOTATION_TEST, ANNOTATION_PARAMETERIZED_TEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTestAfterAnnotation() {
|
||||
return ANNOTATION_AFTER_TEST;
|
||||
public String getCleanupMethodAnnotation() {
|
||||
return ANNOTATION_CLEANUP;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.alibaba.testable.agent.handler.test;
|
||||
|
||||
import com.alibaba.testable.agent.model.TestCaseMethodType;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.alibaba.testable.core.constant.ConstPool.THIS_REF;
|
||||
import static org.objectweb.asm.Opcodes.*;
|
||||
|
||||
public class SpockFramework implements Framework {
|
||||
|
||||
public static final String ANNOTATION_TEST = "Lorg/spockframework/runtime/model/FeatureMetadata;";
|
||||
private static final String NAME_CLEANUP = "cleanup";
|
||||
private static final String DESC_CLEANUP = "()Ljava/lang/Object;";
|
||||
|
||||
@Override
|
||||
public boolean fit(Set<String> classAnnotations, Set<String> methodAnnotations) {
|
||||
return methodAnnotations.contains(ANNOTATION_TEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TestCaseMethodType checkMethodType(MethodNode mn) {
|
||||
if (NAME_CLEANUP.equals(mn.name)) {
|
||||
return TestCaseMethodType.AFTER_TEST;
|
||||
} else if (mn.visibleAnnotations == null) {
|
||||
return TestCaseMethodType.OTHERS;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (ANNOTATION_TEST.equals(an.desc)) {
|
||||
return TestCaseMethodType.TEST;
|
||||
}
|
||||
}
|
||||
return TestCaseMethodType.OTHERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodNode getCleanupMethod(String className) {
|
||||
MethodNode cleanupMethod = new MethodNode(ACC_PRIVATE, NAME_CLEANUP, DESC_CLEANUP, null, null);
|
||||
InsnList il = new InsnList();
|
||||
LabelNode startLabel = new LabelNode(new Label());
|
||||
LabelNode endLabel = new LabelNode(new Label());
|
||||
il.add(startLabel);
|
||||
il.add(new InsnNode(ACONST_NULL));
|
||||
il.add(new InsnNode(ARETURN));
|
||||
il.add(endLabel);
|
||||
cleanupMethod.instructions = il;
|
||||
cleanupMethod.localVariables = Collections.singletonList(new LocalVariableNode(THIS_REF,
|
||||
ClassUtil.toByteCodeClassName(className), null, startLabel, endLabel, 0));
|
||||
cleanupMethod.maxLocals = 1;
|
||||
cleanupMethod.maxStack = 1;
|
||||
return cleanupMethod;
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,18 @@ package com.alibaba.testable.agent.handler.test;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class TestNgFramework extends Framework {
|
||||
public class TestNgFramework extends CommonFramework {
|
||||
|
||||
private static final String ANNOTATION_TEST = "Lorg/testng/annotations/Test;";
|
||||
private static final String ANNOTATION_AFTER_TEST = "Lorg/testng/annotations/AfterMethod;";
|
||||
private static final String ANNOTATION_CLEANUP = "Lorg/testng/annotations/AfterMethod;";
|
||||
|
||||
@Override
|
||||
public List<String> getTestAnnotations() {
|
||||
public List<String> getTestMethodAnnotations() {
|
||||
return Collections.singletonList(ANNOTATION_TEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTestAfterAnnotation() {
|
||||
return ANNOTATION_AFTER_TEST;
|
||||
public String getCleanupMethodAnnotation() {
|
||||
return ANNOTATION_CLEANUP;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,7 @@ public class TestNgOnClassFramework extends TestNgFramework {
|
||||
|
||||
@Override
|
||||
public boolean fit(Set<String> classAnnotations, Set<String> methodAnnotations) {
|
||||
if (CollectionUtil.containsAny(classAnnotations, getTestAnnotations())) {
|
||||
if (methodAnnotations.contains(getTestAfterAnnotation())) {
|
||||
hasTestAfterMethod = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return CollectionUtil.containsAny(classAnnotations, getTestMethodAnnotations());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -28,7 +22,7 @@ public class TestNgOnClassFramework extends TestNgFramework {
|
||||
return (mn.access & ACC_PUBLIC) != 0 ? TestCaseMethodType.TEST : TestCaseMethodType.OTHERS;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (an.desc.equals(getTestAfterAnnotation())) {
|
||||
if (an.desc.equals(getCleanupMethodAnnotation())) {
|
||||
return TestCaseMethodType.AFTER_TEST;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,17 +16,16 @@ import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.InnerClassNode;
|
||||
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.KOTLIN_POSTFIX_COMPANION;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.*;
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.*;
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toJavaStyleClassName;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.DOLLAR;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.TEST_POSTFIX;
|
||||
import static org.objectweb.asm.Opcodes.*;
|
||||
import static com.alibaba.testable.core.util.PathUtil.createFolder;
|
||||
import static org.objectweb.asm.Opcodes.ACC_STATIC;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
@@ -35,26 +34,24 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
|
||||
private static final String FIELD_VALUE = "value";
|
||||
private static final String FIELD_TREAT_AS = "treatAs";
|
||||
private static final String FIELD_PATH = "path";
|
||||
private static final String COMMA = ",";
|
||||
private static final String CLASS_NAME_MOCK = "Mock";
|
||||
private static final String CLASS_JUNIT_5_NESTED = "Lorg/junit/jupiter/api/Nested;";
|
||||
|
||||
/**
|
||||
* Just avoid spend time to scan those surely non-user classes, should keep these lists as tiny as possible
|
||||
*/
|
||||
private final String[] BLACKLIST_PREFIXES = new String[] { "sun/", "com/sun/" };
|
||||
private final String[] BLACKLIST_PREFIXES = new String[] { "sun/", "com/sun/", "javax/crypto", "org/gradle/" };
|
||||
|
||||
public MockClassParser mockClassParser = new MockClassParser();
|
||||
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
|
||||
ProtectionDomain protectionDomain, byte[] classFileBuffer) {
|
||||
// className is in slash-separated format
|
||||
if (isSystemClass(className)) {
|
||||
// Ignore system class and reloaded class
|
||||
return null;
|
||||
}
|
||||
LogUtil.verbose("Handle class: " + className);
|
||||
byte[] bytes = new OmniClassHandler().getBytes(classFileBuffer);
|
||||
byte[] bytes = shouldOmniEnhance(className) ? new OmniClassHandler().getBytes(classFileBuffer) : classFileBuffer;
|
||||
ClassNode cn = ClassUtil.getClassNode(className);
|
||||
if (cn != null) {
|
||||
return transformMock(bytes, cn);
|
||||
@@ -62,29 +59,35 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private boolean shouldOmniEnhance(String className) {
|
||||
String[] blackList = GlobalConfig.getOmniPkgPrefixBlackList();
|
||||
return GlobalConfig.shouldEnhanceOmniConstructor() &&
|
||||
(blackList == null || !isInPrefixList(className, blackList));
|
||||
}
|
||||
|
||||
private byte[] transformMock(byte[] bytes, ClassNode cn) {
|
||||
String className = cn.name;
|
||||
try {
|
||||
if (mockClassParser.isMockClass(cn)) {
|
||||
// it's a mock class
|
||||
LogUtil.diagnose("Handling mock class %s", className);
|
||||
LogUtil.diagnose("Found mock class %s", className);
|
||||
bytes = new MockClassHandler(className).getBytes(bytes);
|
||||
dumpByte(className, GlobalConfig.getDumpPath(), bytes);
|
||||
BytecodeUtil.dumpByte(className, GlobalConfig.getDumpPath(), bytes);
|
||||
} else {
|
||||
String mockClass = foundMockForTestClass(className);
|
||||
if (mockClass != null) {
|
||||
// it's a test class with testable enabled
|
||||
LogUtil.diagnose("Handling test class %s", className);
|
||||
LogUtil.diagnose("Found test class %s", className);
|
||||
bytes = new TestClassHandler().getBytes(bytes);
|
||||
dumpByte(className, GlobalConfig.getDumpPath(), bytes);
|
||||
BytecodeUtil.dumpByte(className, GlobalConfig.getDumpPath(), bytes);
|
||||
} else {
|
||||
mockClass = foundMockForSourceClass(className);
|
||||
if (mockClass != null) {
|
||||
// it's a source class with testable enabled
|
||||
List<MethodInfo> injectMethods = mockClassParser.getTestableMockMethods(mockClass);
|
||||
LogUtil.diagnose("Handling source class %s", className);
|
||||
LogUtil.diagnose("Found source class %s", className);
|
||||
bytes = new SourceClassHandler(injectMethods, mockClass).getBytes(bytes);
|
||||
dumpByte(className, GlobalConfig.getDumpPath(), bytes);
|
||||
BytecodeUtil.dumpByte(className, GlobalConfig.getDumpPath(), bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,26 +98,10 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
} finally {
|
||||
LogUtil.resetLogLevel();
|
||||
}
|
||||
dumpByte(className, getDumpPathByAnnotation(cn), bytes);
|
||||
BytecodeUtil.dumpByte(className, getDumpPathByAnnotation(cn), bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private void dumpByte(String className, String dumpPath, byte[] bytes) {
|
||||
if (dumpPath == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String dumpFile = StringUtil.joinPath(dumpPath,
|
||||
className.replace(SLASH, DOT).replace(DOLLAR, UNDERLINE) + ".class");
|
||||
LogUtil.verbose("Dump class: " + dumpFile);
|
||||
FileOutputStream stream = new FileOutputStream(dumpFile);
|
||||
stream.write(bytes);
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private String foundMockForSourceClass(String className) {
|
||||
String mockClass = lookForMockWithAnnotationAsSourceClass(className);
|
||||
if (mockClass != null) {
|
||||
@@ -133,7 +120,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
}
|
||||
|
||||
private String foundMockForTestClass(String className) {
|
||||
ClassNode cn = ClassUtil.getClassNode(className);
|
||||
ClassNode cn = adaptInnerClass(ClassUtil.getClassNode(className));
|
||||
if (cn != null) {
|
||||
String mockClass = lookForMockWithAnnotationAsTestClass(cn);
|
||||
if (mockClass != null) {
|
||||
@@ -147,6 +134,18 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
return lookForOuterMockClass(className);
|
||||
}
|
||||
|
||||
private ClassNode adaptInnerClass(ClassNode cn) {
|
||||
if (cn == null || cn.visibleAnnotations == null) {
|
||||
return cn;
|
||||
}
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
if (an.desc.equals(CLASS_JUNIT_5_NESTED)) {
|
||||
return ClassUtil.getClassNode(ClassUtil.toOuterClassName(cn.name));
|
||||
}
|
||||
}
|
||||
return cn;
|
||||
}
|
||||
|
||||
private String lookForOuterMockClass(String className) {
|
||||
String mockClassName = ClassUtil.getMockClassName(ClassUtil.getSourceClassName(className));
|
||||
if (mockClassParser.isMockClass(ClassUtil.getClassNode(mockClassName))) {
|
||||
@@ -157,23 +156,21 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
|
||||
private boolean isSystemClass(String className) {
|
||||
// className can be null for Java 8 lambdas
|
||||
if (null == className) {
|
||||
if (null == className || className.contains(CGLIB_CLASS_PATTERN)) {
|
||||
return true;
|
||||
}
|
||||
String whitePrefix = GlobalConfig.getPkgPrefix();
|
||||
if (whitePrefix != null) {
|
||||
for (String prefix : whitePrefix.split(COMMA)) {
|
||||
if (className.startsWith(prefix)) {
|
||||
// Only consider package in provided list as non-system class
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
for (String prefix : BLACKLIST_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
String[] pkgPrefixWhiteList = GlobalConfig.getPkgPrefixWhiteList();
|
||||
if (pkgPrefixWhiteList != null) {
|
||||
// Only consider package in provided list as non-system class
|
||||
return !isInPrefixList(className, pkgPrefixWhiteList);
|
||||
}
|
||||
return isInPrefixList(className, BLACKLIST_PREFIXES);
|
||||
}
|
||||
|
||||
private boolean isInPrefixList(String name, String[] prefixList) {
|
||||
for (String prefix : prefixList) {
|
||||
if (name.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -204,7 +201,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
ClassNode innerClassNode = ClassUtil.getClassNode(ic.name);
|
||||
if (ic.name.equals(getInnerMockClassName(cn.name)) && mockClassParser.isMockClass(innerClassNode)) {
|
||||
if ((ic.access & ACC_STATIC) == 0) {
|
||||
LogUtil.warn(String.format("Mock class in \"%s\" is not declared as static", cn.name));
|
||||
LogUtil.warn("Mock class in \"%s\" is not declared as static", cn.name);
|
||||
} else {
|
||||
ic.access = BytecodeUtil.toPublicAccess(ic.access);
|
||||
return ic.name;
|
||||
@@ -257,7 +254,11 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
if (cn.visibleAnnotations != null) {
|
||||
for (AnnotationNode an : cn.visibleAnnotations) {
|
||||
if (toJavaStyleClassName(an.desc).equals(ConstPool.DUMP_TO)) {
|
||||
return AnnotationUtil.getAnnotationParameter(an, FIELD_PATH, null, String.class);
|
||||
String path = AnnotationUtil.getAnnotationParameter(an, FIELD_VALUE, null, String.class);
|
||||
String fullPath = PathUtil.join(System.getProperty(PROPERTY_USER_DIR), path);
|
||||
if (createFolder(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,7 +274,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
}
|
||||
|
||||
private String getInnerMockClassName(String className) {
|
||||
return className + DOLLAR + CLASS_NAME_MOCK;
|
||||
return className + DOLLAR + GlobalConfig.getInnerMockClassName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.alibaba.testable.core.constant.ConstPool.*;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.UNDERLINE;
|
||||
import static org.objectweb.asm.Opcodes.*;
|
||||
|
||||
/**
|
||||
@@ -193,4 +199,26 @@ public class BytecodeUtil {
|
||||
access |= ACC_PUBLIC;
|
||||
return access;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump byte code to specified class file
|
||||
* @param className original class name
|
||||
* @param dumpPath folder to store class file
|
||||
* @param bytes original class bytes
|
||||
*/
|
||||
public static void dumpByte(String className, String dumpPath, byte[] bytes) {
|
||||
if (dumpPath == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String dumpFile = PathUtil.join(dumpPath,
|
||||
className.replace(SLASH, DOT).replace(DOLLAR, UNDERLINE) + ".class");
|
||||
LogUtil.verbose("Dump class: " + dumpFile);
|
||||
FileOutputStream stream = new FileOutputStream(dumpFile);
|
||||
stream.write(bytes);
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -227,14 +226,25 @@ public class ClassUtil {
|
||||
ClassNode cn = new ClassNode();
|
||||
try {
|
||||
new ClassReader(className).accept(cn, 0);
|
||||
} catch (IOException e) {
|
||||
} catch (Throwable e) {
|
||||
// Could be IOException, ClassCircularityError or NullPointerException
|
||||
// Ignore all of them
|
||||
return null;
|
||||
}
|
||||
return cn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get outer class name from a inner class name
|
||||
* @param name inner class name
|
||||
* @return outer class name
|
||||
*/
|
||||
public static String toOuterClassName(String name) {
|
||||
int pos = name.lastIndexOf("$");
|
||||
return (pos > 0) ? name.substring(0, pos) : name;
|
||||
}
|
||||
|
||||
private static String toDescriptor(Byte type, String objectType) {
|
||||
return "(" + (char)type.byteValue() + ")L" + objectType + ";";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@ public class DiagnoseUtil {
|
||||
private static void setupDiagnose(AnnotationNode an, String fieldDiagnose) {
|
||||
LogLevel level = AnnotationUtil.getAnnotationParameter(an, fieldDiagnose, null, LogLevel.class);
|
||||
if (level != null) {
|
||||
LogUtil.setLevel(level == LogLevel.ENABLE ? LogUtil.LogLevel.LEVEL_DIAGNOSE :
|
||||
(level == LogLevel.VERBOSE ? LogUtil.LogLevel.LEVEL_VERBOSE : LogUtil.LogLevel.LEVEL_MUTE));
|
||||
LogUtil.setLevel(level);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import com.alibaba.testable.core.model.LogLevel;
|
||||
import com.alibaba.testable.core.model.MockScope;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.PROPERTY_TEMP_DIR;
|
||||
import static com.alibaba.testable.agent.constant.ConstPool.PROPERTY_USER_DIR;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.COMMA;
|
||||
import static com.alibaba.testable.core.constant.ConstPool.DOT;
|
||||
import static com.alibaba.testable.core.util.PathUtil.createFolder;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
@@ -13,23 +23,32 @@ public class GlobalConfig {
|
||||
private static final String MUTE = "mute";
|
||||
private static final String DEBUG = "debug";
|
||||
private static final String VERBOSE = "verbose";
|
||||
private static final String DISABLE_LOG_FILE = "null";
|
||||
private static final String TESTABLE_AGENT_LOG = "testable-agent.log";
|
||||
private static final String DEFAULT_MAVEN_OUTPUT_FOLDER = "target";
|
||||
private static final String DEFAULT_GRADLE_OUTPUT_FOLDER = "gradle";
|
||||
|
||||
private static String logFile = null;
|
||||
private static String dumpPath = null;
|
||||
private static String pkgPrefix = null;
|
||||
private static String[] pkgPrefixWhiteList = null;
|
||||
private static String[] omniPkgPrefixBlackList = null;
|
||||
private static MockScope defaultMockScope = MockScope.GLOBAL;
|
||||
private static boolean enhanceThreadLocal = false;
|
||||
private static boolean enhanceOmniConstructor = false;
|
||||
private static String innerMockClassName = "Mock";
|
||||
|
||||
public static boolean setLogLevel(String level) {
|
||||
public static void setLogLevel(String level) {
|
||||
if (level.equals(MUTE)) {
|
||||
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_MUTE);
|
||||
return true;
|
||||
LogUtil.setDefaultLevel(LogLevel.DISABLE);
|
||||
} else if (level.equals(DEBUG)) {
|
||||
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_DIAGNOSE);
|
||||
return true;
|
||||
LogUtil.setDefaultLevel(LogLevel.ENABLE);
|
||||
} else if (level.equals(VERBOSE)) {
|
||||
LogUtil.setDefaultLevel(LogUtil.LogLevel.LEVEL_VERBOSE);
|
||||
return true;
|
||||
LogUtil.setDefaultLevel(LogLevel.VERBOSE);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void setLogFile(String path) {
|
||||
logFile = path;
|
||||
}
|
||||
|
||||
public static String getDumpPath() {
|
||||
@@ -37,15 +56,26 @@ public class GlobalConfig {
|
||||
}
|
||||
|
||||
public static void setDumpPath(String path) {
|
||||
dumpPath = path;
|
||||
String fullPath = PathUtil.join(System.getProperty(PROPERTY_USER_DIR), path);
|
||||
if (createFolder(fullPath)) {
|
||||
dumpPath = fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getPkgPrefix() {
|
||||
return pkgPrefix;
|
||||
public static String[] getPkgPrefixWhiteList() {
|
||||
return pkgPrefixWhiteList;
|
||||
}
|
||||
|
||||
public static void setPkgPrefix(String prefix) {
|
||||
pkgPrefix = prefix;
|
||||
public static void setPkgPrefixWhiteList(String prefixes) {
|
||||
pkgPrefixWhiteList = parsePkgPrefixList(prefixes).toArray(new String[0]);
|
||||
}
|
||||
|
||||
public static String[] getOmniPkgPrefixBlackList() {
|
||||
return omniPkgPrefixBlackList;
|
||||
}
|
||||
|
||||
public static void setOmniPkgPrefixBlackList(String prefixes) {
|
||||
omniPkgPrefixBlackList = parsePkgPrefixList(prefixes).toArray(new String[0]);
|
||||
}
|
||||
|
||||
public static MockScope getDefaultMockScope() {
|
||||
@@ -55,4 +85,65 @@ public class GlobalConfig {
|
||||
public static void setDefaultMockScope(MockScope scope) {
|
||||
defaultMockScope = scope;
|
||||
}
|
||||
|
||||
public static void setupLogRootPath() {
|
||||
if (logFile == null) {
|
||||
// Use default log file location
|
||||
String baseFolder = getBuildOutputFolder();
|
||||
if (!baseFolder.isEmpty()) {
|
||||
String logFilePath = PathUtil.join(baseFolder, TESTABLE_AGENT_LOG);
|
||||
LogUtil.setGlobalLogPath(logFilePath);
|
||||
LogUtil.verbose("Generate testable agent log file at: %s", logFilePath);
|
||||
}
|
||||
} else if (!DISABLE_LOG_FILE.equals(logFile)) {
|
||||
// Use custom log file location
|
||||
LogUtil.setGlobalLogPath(PathUtil.join(System.getProperty(PROPERTY_USER_DIR), logFile));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getBuildOutputFolder() {
|
||||
String contextFolder = System.getProperty(PROPERTY_USER_DIR);
|
||||
URL rootResourceFolder = Object.class.getResource("/");
|
||||
if (rootResourceFolder != null) {
|
||||
return PathUtil.getFirstLevelFolder(contextFolder, rootResourceFolder.getPath());
|
||||
} else if (PathUtil.folderExists(PathUtil.join(contextFolder, DEFAULT_MAVEN_OUTPUT_FOLDER))) {
|
||||
return PathUtil.join(contextFolder, DEFAULT_MAVEN_OUTPUT_FOLDER);
|
||||
} else if (PathUtil.folderExists(PathUtil.join(contextFolder, DEFAULT_GRADLE_OUTPUT_FOLDER))) {
|
||||
return PathUtil.join(contextFolder, DEFAULT_GRADLE_OUTPUT_FOLDER);
|
||||
} else {
|
||||
return System.getProperty(PROPERTY_TEMP_DIR);
|
||||
}
|
||||
}
|
||||
|
||||
public static void enableEnhanceThreadLocal(boolean enabled) {
|
||||
enhanceThreadLocal = enabled;
|
||||
}
|
||||
|
||||
public static boolean shouldEnhanceThreadLocal() {
|
||||
return enhanceThreadLocal;
|
||||
}
|
||||
|
||||
public static void enableEnhanceOmniConstructor(boolean enabled) {
|
||||
enhanceOmniConstructor = enabled;
|
||||
}
|
||||
|
||||
public static boolean shouldEnhanceOmniConstructor() {
|
||||
return enhanceOmniConstructor;
|
||||
}
|
||||
|
||||
public static void setInnerMockClassName(String name) {
|
||||
innerMockClassName = name;
|
||||
}
|
||||
|
||||
public static String getInnerMockClassName() {
|
||||
return innerMockClassName;
|
||||
}
|
||||
|
||||
private static List<String> parsePkgPrefixList(String prefixes) {
|
||||
List<String> whiteList = new ArrayList<String>();
|
||||
for (String p : prefixes.split(COMMA)) {
|
||||
whiteList.add(ClassUtil.toSlashSeparatedName(p.endsWith(DOT) ? p : p + DOT));
|
||||
}
|
||||
return whiteList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class PathUtil {
|
||||
|
||||
/**
|
||||
* Join a path text and a file name to full file path
|
||||
* @param folder path text
|
||||
* @param file file name
|
||||
* @return joined full file path
|
||||
*/
|
||||
public static String join(String folder, String file) {
|
||||
return (folder.endsWith(File.separator) ? folder : (folder + File.separator)) + file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the absolute path of the first sub-folder from root folder to target path
|
||||
* @param rootFolder specify root folder path
|
||||
* @param targetPath any sub path inside root folder
|
||||
* @return first sub-folder path
|
||||
*/
|
||||
public static String getFirstLevelFolder(String rootFolder, String targetPath) {
|
||||
if (!targetPath.startsWith(rootFolder) || targetPath.length() <= rootFolder.length() + 1) {
|
||||
return "";
|
||||
}
|
||||
char separator = targetPath.charAt(rootFolder.length());
|
||||
int pos = targetPath.indexOf(separator, rootFolder.length() + 1);
|
||||
return pos > 0 ? targetPath.substring(0, pos) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether specified path exists and is folder
|
||||
* @param path path to check
|
||||
* @return exists or not
|
||||
*/
|
||||
public static boolean folderExists(String path) {
|
||||
return new File(path).isDirectory();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
@@ -21,14 +19,4 @@ public class StringUtil {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* join a path text and a file name to full file path
|
||||
* @param folder path text
|
||||
* @param file file name
|
||||
* @return joined full file path
|
||||
*/
|
||||
public static String joinPath(String folder, String file) {
|
||||
return (folder.endsWith(File.separator) ? folder : (folder + File.separator)) + file;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,5 +23,13 @@ class ClassUtilTest {
|
||||
ClassUtil.fitCompanionClassName("com/alibaba/testable/demo/BlackBox$Companion"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_get_outer_class_name() {
|
||||
assertEquals("com/alibaba/demo/basic/DemoMockTest",
|
||||
ClassUtil.toOuterClassName("com/alibaba/demo/basic/DemoMockTest$Inner"));
|
||||
assertEquals("com/alibaba/demo/basic/DemoMockTest",
|
||||
ClassUtil.toOuterClassName("com/alibaba/demo/basic/DemoMockTest"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class PathUtilTest {
|
||||
|
||||
@Test
|
||||
void should_get_sub_folder() {
|
||||
assertEquals("/ab/cd", PathUtil.getFirstLevelFolder("/ab", "/ab/cd/ef/gh"));
|
||||
assertEquals("c:\\ab\\cd", PathUtil.getFirstLevelFolder("c:\\ab", "c:\\ab\\cd\\ef\\gh"));
|
||||
assertEquals("", PathUtil.getFirstLevelFolder("/ab", "/ab"));
|
||||
assertEquals("", PathUtil.getFirstLevelFolder("/ab/cd", "/ab"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.6.0</version>
|
||||
<version>0.6.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.6.0</version>
|
||||
<version>0.6.4</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-core</artifactId>
|
||||
|
||||
@@ -14,8 +14,8 @@ public @interface DumpTo {
|
||||
|
||||
/**
|
||||
* dump class byte code to specified folder
|
||||
* @return an exist folder
|
||||
* @return folder path relate to the project root path
|
||||
*/
|
||||
String path() default "";
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@@ -21,5 +21,6 @@ public class ConstPool {
|
||||
public static final String SLASH = "/";
|
||||
public static final String DOLLAR = "$";
|
||||
public static final String UNDERLINE = "_";
|
||||
public static final String COMMA = ",";
|
||||
|
||||
}
|
||||
|
||||
@@ -3,22 +3,26 @@ package com.alibaba.testable.core.model;
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
|
||||
public enum LogLevel {
|
||||
|
||||
/**
|
||||
* Be quiet
|
||||
* Mute
|
||||
*/
|
||||
DISABLE,
|
||||
|
||||
DISABLE(0),
|
||||
/**
|
||||
* Print diagnose logs
|
||||
* Warn only
|
||||
*/
|
||||
ENABLE,
|
||||
|
||||
DEFAULT(1),
|
||||
/**
|
||||
* Print verbose log
|
||||
* Show diagnose messages
|
||||
*/
|
||||
VERBOSE
|
||||
ENABLE(2),
|
||||
/**
|
||||
* Show detail progress logs
|
||||
*/
|
||||
VERBOSE(3);
|
||||
|
||||
public int level;
|
||||
LogLevel(int level) {
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.alibaba.testable.core.model;
|
||||
|
||||
import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.type.NullType;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.lang.model.type.TypeVisitor;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class TestableNull implements NullType {
|
||||
|
||||
@Override
|
||||
public TypeKind getKind() {
|
||||
return TypeKind.NULL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R, P> R accept(TypeVisitor<R, P> v, P p) {
|
||||
return v.visitNull(this, p);
|
||||
}
|
||||
|
||||
public List<? extends AnnotationMirror> getAnnotationMirrors() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public <A extends Annotation> A getAnnotation(Class<A> annotationType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationType) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package com.alibaba.testable.core.tool;
|
||||
|
||||
import com.alibaba.testable.core.exception.ClassConstructionException;
|
||||
import com.alibaba.testable.core.model.TestableNull;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import com.alibaba.testable.core.util.TypeUtil;
|
||||
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.lang.reflect.*;
|
||||
import java.util.*;
|
||||
|
||||
@@ -16,6 +15,7 @@ import static com.alibaba.testable.core.constant.ConstPool.DOLLAR;
|
||||
public class OmniConstructor {
|
||||
|
||||
private static final int INITIAL_CAPACITY = 6;
|
||||
private static final int ZERO = 0;
|
||||
|
||||
private OmniConstructor() {}
|
||||
|
||||
@@ -26,7 +26,7 @@ public class OmniConstructor {
|
||||
* @return 返回新创建的对象
|
||||
*/
|
||||
public static <T> T newInstance(Class<T> clazz) {
|
||||
return handleCircleReference(newInstance(clazz, new HashSet<Class<?>>(INITIAL_CAPACITY)));
|
||||
return handleCircleReference(newInstance(clazz, new HashSet<Class<?>>(INITIAL_CAPACITY), ZERO));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,10 +37,11 @@ public class OmniConstructor {
|
||||
* @return 返回新创建的对象数组
|
||||
*/
|
||||
public static <T> T[] newArray(Class<T> clazz, int size) {
|
||||
return (T[])handleCircleReference(newArray(clazz, size, new HashSet<Class<?>>(INITIAL_CAPACITY)));
|
||||
return (T[])handleCircleReference(newArray(clazz, size, new HashSet<Class<?>>(INITIAL_CAPACITY), ZERO));
|
||||
}
|
||||
|
||||
private static <T> T newInstance(Class<T> clazz, Set<Class<?>> classPool) {
|
||||
private static <T> T newInstance(Class<T> clazz, Set<Class<?>> classPool, int level) {
|
||||
LogUtil.verbose(level, "Creating %s", clazz.getName());
|
||||
if (classPool.contains(clazz)) {
|
||||
return null;
|
||||
}
|
||||
@@ -49,7 +50,7 @@ public class OmniConstructor {
|
||||
if (clazz.isPrimitive()) {
|
||||
return newPrimitive(clazz);
|
||||
} else if (clazz.isArray()) {
|
||||
return (T)newArray(clazz.getComponentType(), 0, classPool);
|
||||
return (T)newArray(clazz.getComponentType(), 0, classPool, level);
|
||||
} else if (clazz.isEnum()) {
|
||||
return newEnum(clazz);
|
||||
} else if (clazz.isInterface()) {
|
||||
@@ -57,7 +58,7 @@ public class OmniConstructor {
|
||||
} else if (Modifier.isAbstract(clazz.getModifiers())) {
|
||||
return newAbstractClass(clazz);
|
||||
}
|
||||
return newObject(clazz, classPool);
|
||||
return newObject(clazz, classPool, level);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new ClassConstructionException("Failed to find constructor", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
@@ -73,26 +74,26 @@ public class OmniConstructor {
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T newObject(Class<T> clazz, Set<Class<?>> classPool)
|
||||
private static <T> T newObject(Class<T> clazz, Set<Class<?>> classPool, int level)
|
||||
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
||||
Object ins = createInstance(getBestConstructor(clazz), classPool);
|
||||
Object ins = createInstance(getBestConstructor(clazz), classPool, level);
|
||||
if (!TypeUtil.isBasicType(clazz)) {
|
||||
for (Field f : TypeUtil.getAllFields(clazz)) {
|
||||
f.setAccessible(true);
|
||||
// skip "$jacocoData" field added by jacoco
|
||||
if (f.get(ins) == null && !f.getName().startsWith(DOLLAR)) {
|
||||
f.set(ins, newInstance(f.getType(), classPool));
|
||||
f.set(ins, newInstance(f.getType(), classPool, level + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return (T)ins;
|
||||
}
|
||||
|
||||
private static Object newArray(Class<?> clazz, int size, Set<Class<?>> classPool) {
|
||||
private static Object newArray(Class<?> clazz, int size, Set<Class<?>> classPool, int level) {
|
||||
// primary[] cannot be cast to Object[], have to use Object instead of T[]
|
||||
Object array = Array.newInstance(clazz, size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
Array.set(array, i, newInstance(clazz, classPool));
|
||||
Array.set(array, i, newInstance(clazz, classPool, level + 1));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
@@ -143,10 +144,12 @@ public class OmniConstructor {
|
||||
try {
|
||||
if (instance.getClass().isArray()) {
|
||||
for (int i = 0; i < Array.getLength(instance); i++) {
|
||||
handleCircleReference(Array.get(instance, i), new HashMap<Class<?>, Object>(INITIAL_CAPACITY));
|
||||
handleCircleReference(Array.get(instance, i), instance.getClass().getComponentType(),
|
||||
new HashMap<Class<?>, Object>(INITIAL_CAPACITY), ZERO);
|
||||
}
|
||||
} else {
|
||||
handleCircleReference(instance, new HashMap<Class<?>, Object>(INITIAL_CAPACITY));
|
||||
handleCircleReference(instance, instance.getClass(),
|
||||
new HashMap<Class<?>, Object>(INITIAL_CAPACITY), ZERO);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new ClassConstructionException("Failed to access field", e);
|
||||
@@ -154,40 +157,46 @@ public class OmniConstructor {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static <T> void handleCircleReference(T instance, Map<Class<?>, Object> classPool)
|
||||
private static <T> void handleCircleReference(T instance, Class<?> type, Map<Class<?>, Object> classPool, int level)
|
||||
throws IllegalAccessException {
|
||||
classPool.put(instance.getClass(), instance);
|
||||
for (Field f : TypeUtil.getAllFields(instance.getClass())) {
|
||||
if (instance == null) {
|
||||
// don't travel null object
|
||||
return;
|
||||
}
|
||||
LogUtil.verbose(level, "Verifying %s", type.getName());
|
||||
classPool.put(type, instance);
|
||||
for (Field f : TypeUtil.getAllFields(type)) {
|
||||
f.setAccessible(true);
|
||||
Object fieldIns = f.get(instance);
|
||||
Class<?> fieldType = f.getType();
|
||||
if (fieldType.isArray()) {
|
||||
if (fieldIns != null) {
|
||||
for (int i = 0; i < Array.getLength(fieldIns); i++) {
|
||||
handleCircleReference(Array.get(fieldIns, i), classPool);
|
||||
handleCircleReference(Array.get(fieldIns, i), fieldType.getComponentType(),
|
||||
classPool, level + 1);
|
||||
}
|
||||
}
|
||||
} else if (!fieldType.isPrimitive() && !TypeUtil.isBasicType(fieldType)) {
|
||||
if (fieldIns == null && classPool.containsKey(fieldType)) {
|
||||
f.set(instance, classPool.get(fieldType));
|
||||
} else if (fieldIns != null && !classPool.containsKey(fieldType)) {
|
||||
handleCircleReference(fieldIns, classPool);
|
||||
} else if (!classPool.containsKey(fieldType)) {
|
||||
handleCircleReference(fieldIns, fieldType, classPool, level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
classPool.remove(instance.getClass());
|
||||
classPool.remove(type);
|
||||
}
|
||||
|
||||
private static Object createInstance(Constructor<?> constructor, Set<Class<?>> classPool)
|
||||
private static Object createInstance(Constructor<?> constructor, Set<Class<?>> classPool, int level)
|
||||
throws InstantiationException, IllegalAccessException, InvocationTargetException {
|
||||
constructor.setAccessible(true);
|
||||
Class<?>[] types = constructor.getParameterTypes();
|
||||
if (types.length == 1 && types[0].equals(NullType.class)) {
|
||||
return constructor.newInstance(new TestableNull());
|
||||
if (types.length == 1 && types[0].equals(Void.class)) {
|
||||
return constructor.newInstance(OmniConstructor.newInstance(Void.class));
|
||||
} else {
|
||||
Object[] args = new Object[types.length];
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
args[i] = newInstance(types[i], classPool);
|
||||
args[i] = newInstance(types[i], classPool, level + 1);
|
||||
}
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
@@ -198,9 +207,9 @@ public class OmniConstructor {
|
||||
int minimalParametersSize = 999;
|
||||
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
|
||||
Class<?>[] types = constructor.getParameterTypes();
|
||||
if (types.length == 1 && types[0].equals(NullType.class)) {
|
||||
if (types.length == 1 && types[0].equals(Void.class)) {
|
||||
return constructor;
|
||||
} else if (types.length < minimalParametersSize) {
|
||||
} else if (types.length < minimalParametersSize && !anyMatch(types, clazz)) {
|
||||
minimalParametersSize = types.length;
|
||||
bestConstructor = constructor;
|
||||
}
|
||||
@@ -208,4 +217,16 @@ public class OmniConstructor {
|
||||
return bestConstructor;
|
||||
}
|
||||
|
||||
private static boolean anyMatch(Class<?>[] types, Class<?> clazz) {
|
||||
for (Class<?> t : types) {
|
||||
if (clazz.getName().equals(t.getName())) {
|
||||
return true;
|
||||
}
|
||||
if (clazz.getSuperclass() != null && clazz.getSuperclass().getName().equals(t.getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,64 +1,68 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
|
||||
import com.alibaba.testable.core.model.LogLevel;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class LogUtil {
|
||||
|
||||
public enum LogLevel {
|
||||
/**
|
||||
* Mute
|
||||
*/
|
||||
LEVEL_MUTE(0),
|
||||
/**
|
||||
* Warn only
|
||||
*/
|
||||
LEVEL_WARN(1),
|
||||
/**
|
||||
* Show diagnose messages
|
||||
*/
|
||||
LEVEL_DIAGNOSE(2),
|
||||
/**
|
||||
* Show detail progress logs
|
||||
*/
|
||||
LEVEL_VERBOSE(3);
|
||||
|
||||
int level;
|
||||
LogLevel(int l) {
|
||||
level = l;
|
||||
}
|
||||
}
|
||||
|
||||
private static LogLevel defaultLogLevel = LogLevel.LEVEL_WARN;
|
||||
private static LogLevel currentLogLevel = LogLevel.LEVEL_WARN;
|
||||
private static LogLevel defaultLogLevel = LogLevel.DEFAULT;
|
||||
private static LogLevel currentLogLevel = LogLevel.DEFAULT;
|
||||
private static FileOutputStream logFileStream = null;
|
||||
|
||||
public static void verbose(String msg, Object... args) {
|
||||
if (isVerboseEnabled()) {
|
||||
System.out.println(String.format("[VERBOSE] " + msg, args));
|
||||
}
|
||||
verbose(0, msg, args);
|
||||
}
|
||||
|
||||
public static void diagnose(String msg, Object... args) {
|
||||
if (currentLogLevel.level >= LogLevel.LEVEL_DIAGNOSE.level) {
|
||||
System.out.println(String.format("[DIAGNOSE] " + msg, args));
|
||||
diagnose(0, msg, args);
|
||||
}
|
||||
|
||||
public static void verbose(int indent, String msg, Object... args) {
|
||||
if (isVerboseEnabled()) {
|
||||
String text = String.format(space(indent) + msg + "\n", args);
|
||||
System.out.print("[VERBOSE] ");
|
||||
System.out.print(text);
|
||||
write("[TIP] ");
|
||||
write(text);
|
||||
}
|
||||
}
|
||||
|
||||
public static void diagnose(int indent, String msg, Object... args) {
|
||||
String text = String.format(space(indent) + msg + "\n", args);
|
||||
if (currentLogLevel.level >= LogLevel.ENABLE.level) {
|
||||
System.out.print("[DIAGNOSE] ");
|
||||
System.out.print(text);
|
||||
}
|
||||
write("[INFO] ");
|
||||
write(text);
|
||||
}
|
||||
|
||||
public static void warn(String msg, Object... args) {
|
||||
if (currentLogLevel.level >= LogLevel.LEVEL_WARN.level) {
|
||||
System.err.println(String.format("[WARN] " + msg, args));
|
||||
String text = String.format("[WARN] " + msg + "\n", args);
|
||||
if (currentLogLevel.level >= LogLevel.DEFAULT.level) {
|
||||
System.err.print(text);
|
||||
}
|
||||
write(text);
|
||||
}
|
||||
|
||||
public static void error(String msg, Object... args) {
|
||||
System.err.println(String.format("[ERROR] " + msg, args));
|
||||
String text = String.format("[ERROR] " + msg + "\n", args);
|
||||
System.err.print(text);
|
||||
write(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* a pre-check method for reduce verbose parameter calculation
|
||||
*/
|
||||
public static boolean isVerboseEnabled() {
|
||||
return currentLogLevel.level >= LogLevel.LEVEL_VERBOSE.level;
|
||||
return currentLogLevel.level >= LogLevel.VERBOSE.level;
|
||||
}
|
||||
|
||||
public static void setLevel(LogLevel level) {
|
||||
@@ -74,4 +78,40 @@ public class LogUtil {
|
||||
currentLogLevel = defaultLogLevel;
|
||||
}
|
||||
|
||||
public static void setGlobalLogPath(String logFilePath) {
|
||||
try {
|
||||
if (PathUtil.createFolder(PathUtil.getFolder(logFilePath))) {
|
||||
logFileStream = new FileOutputStream(logFilePath);
|
||||
diagnose("Start at %s", new Date().toString());
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
warn("Failed to create log file %s", logFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
public static void cleanup() {
|
||||
try {
|
||||
if (logFileStream != null) {
|
||||
diagnose("Completed at %s", new Date().toString());
|
||||
logFileStream.flush();
|
||||
logFileStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
warn("Log file is not closed properly");
|
||||
}
|
||||
}
|
||||
|
||||
private static String space(int indent) {
|
||||
return StringUtil.repeat(" ", indent);
|
||||
}
|
||||
|
||||
private static void write(String text) {
|
||||
try {
|
||||
if (logFileStream != null) {
|
||||
logFileStream.write(text.getBytes());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class PathUtil {
|
||||
|
||||
/**
|
||||
* Create folder recursively
|
||||
* @param folderPath folder to create
|
||||
* @return whether creation success
|
||||
*/
|
||||
public static boolean createFolder(String folderPath) {
|
||||
File folder = new File(folderPath);
|
||||
if (folder.isDirectory()) {
|
||||
return true;
|
||||
}
|
||||
return folder.mkdirs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parent folder of specified path
|
||||
* @param path path of file or folder
|
||||
* @return parent folder path
|
||||
*/
|
||||
public static String getFolder(String path) {
|
||||
return new File(path).getParent();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
|
||||
public class StringUtil {
|
||||
|
||||
public static String repeat(String str, int n) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < n; i++) {
|
||||
sb.append(str);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -107,7 +108,7 @@ public class TypeUtil {
|
||||
return clazz.isEnum() || clazz.equals(Integer.class) || clazz.equals(Short.class) || clazz.equals(Long.class)
|
||||
|| clazz.equals(Byte.class) || clazz.equals(Character.class) || clazz.equals(Float.class)
|
||||
|| clazz.equals(Double.class) || clazz.equals(Boolean.class) || clazz.equals(Class.class)
|
||||
|| clazz.equals(String.class);
|
||||
|| clazz.equals(String.class) || clazz.equals(Date.class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.6.0</version>
|
||||
<version>0.6.4</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.6.0</version>
|
||||
<version>0.6.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.6.0</testable.version>
|
||||
<testable.version>0.6.4</testable.version>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.6.0</version>
|
||||
<version>0.6.4</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-processor</artifactId>
|
||||
|
||||
23
tool/bump-to-next-release.sh
Executable file
23
tool/bump-to-next-release.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Release next version
|
||||
|
||||
VERSION=$(cat testable-parent/pom.xml | grep '<testable.version>' | sed -e 's/^.*>\([^<]*\)<.*$/\1/g')
|
||||
echo "Current version is: ${VERSION}"
|
||||
read -p "Next version should be: " NEXT
|
||||
|
||||
for pom in testable-all/pom.xml testable-maven-plugin/pom.xml testable-processor/pom.xml testable-agent/pom.xml testable-core/pom.xml testable-parent/pom.xml; do
|
||||
sed -i '' "s/<version>${VERSION}<\/version>/<version>${NEXT}<\/version>/" $pom
|
||||
done
|
||||
for gradle in demo/java-demo/build.gradle demo/kotlin-demo/build.gradle.kts demo/spock-demo/build.gradle; do
|
||||
sed -i '' "s/testable-\([a-z]*\):${VERSION}/testable-\1:${NEXT}/" $gradle
|
||||
done
|
||||
for pom in testable-parent/pom.xml demo/java-demo/pom.xml demo/kotlin-demo/pom.xml demo/spock-demo/pom.xml; do
|
||||
sed -i '' "s/<testable.version>${VERSION}<\/testable.version>/<testable.version>${NEXT}<\/testable.version>/" $pom
|
||||
done
|
||||
for md in docs/zh-cn/doc/setup.md docs/en-us/doc/setup.md; do
|
||||
sed -i '' "s/${VERSION}/${NEXT}/" $md
|
||||
done
|
||||
|
||||
echo "Done. Please setup maven configuration for sonatype, and manually run \"mvn clean deploy -Prelease\""
|
||||
echo "Then commit the new version with \"git add . && git commit -m 'release v${NEXT}' && git tag v${NEXT} && git push --tag\""
|
||||
45
tool/generate-snapshot-release.sh
Executable file
45
tool/generate-snapshot-release.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Release SNAPSHOT version
|
||||
|
||||
if [ "${SNAPSHOT_MVN_REPO}" = "" ]; then
|
||||
echo "\"SNAPSHOT_MVN_REPO\" environment variable should be setup before execution"
|
||||
exit -1
|
||||
fi
|
||||
|
||||
VERSION=$(cat testable-parent/pom.xml | grep '<testable.version>' | sed -e 's/^.*>\([^<]*\)<.*$/\1/g')
|
||||
MAJOR_VERSION=$(echo $VERSION | sed 's/^\(.*\)\.[0-9]\{1,\}.*/\1/g')
|
||||
declare -i MINOR_VERSION=$(echo $VERSION | sed 's/^.*\.\([0-9]\{1,\}\)/\1/g')
|
||||
MINOR_VERSION=$MINOR_VERSION+1
|
||||
SNAPSHOT="${MAJOR_VERSION}.${MINOR_VERSION}-SNAPSHOT"
|
||||
|
||||
echo "Current version: ${VERSION}"
|
||||
echo "Next snapshot version: ${SNAPSHOT}"
|
||||
read -p "Confirm (Y/N) ? " confirm && [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]] || exit 1
|
||||
|
||||
|
||||
sed -i '' '/<\/profiles>/,$d' testable-parent/pom.xml
|
||||
cat << EOF >> testable-parent/pom.xml
|
||||
<profile>
|
||||
<id>snapshot</id>
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>releases</id>
|
||||
<url>http://${SNAPSHOT_MVN_REPO}/mvn/releases</url>
|
||||
</repository>
|
||||
<snapshotRepository>
|
||||
<id>snapshots</id>
|
||||
<url>http://${SNAPSHOT_MVN_REPO}/mvn/snapshots</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
EOF
|
||||
|
||||
for pom in testable-all/pom.xml testable-maven-plugin/pom.xml testable-processor/pom.xml testable-agent/pom.xml testable-core/pom.xml testable-parent/pom.xml; do
|
||||
sed -i '' "s#<version>${VERSION}</version>#<version>${SNAPSHOT}</version>#g" $pom
|
||||
done
|
||||
sed -i '' "s#<testable.version>${VERSION}</testable.version>#<testable.version>${SNAPSHOT}</testable.version>#g" testable-parent/pom.xml
|
||||
|
||||
echo "Done. Please setup maven configuration for snapshot repository and manually run \"mvn clean deploy -Psnapshot\""
|
||||
Reference in New Issue
Block a user