Compare commits

..

24 Commits

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

View File

@@ -8,7 +8,8 @@
## 目录结构
```bash
|-- testable-core ➜ 核心组件,提供测试辅助功能、注解和工具类
|-- testable-core ➜ 核心组件,提供注解和工具类
|-- testable-processor ➜ 编译期代码预处理组件,提供测试辅助功能
|-- testable-agent ➜ JavaAgent组件提供Mock测试相关功能
|-- testable-maven-plugin ➜ Maven插件组件用于简化JavaAgent注入
|-- demo
@@ -21,6 +22,8 @@
主项目使用JDK 1.6+和Maven 3+版本构建,其中`demo`子项目需要JDK 1.8+版本。
由于`Testable`的测试也用到了`Testable`本身,本地首次构建时候需要使用`install`而不能只做`package`
```bash
mvn clean package
mvn clean install
```

View File

@@ -16,7 +16,7 @@
<properties>
<java.version>1.8</java.version>
<testable.version>0.2.0-SNAPSHOT</testable.version>
<testable.version>0.2.2-SNAPSHOT</testable.version>
</properties>
<dependencies>
@@ -27,7 +27,7 @@
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-core</artifactId>
<artifactId>testable-processor</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
package com.alibaba.testable.demo.model;
public interface Box {
void put(String something);
}

View File

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

View File

@@ -0,0 +1,25 @@
package com.alibaba.testable.demo.service;
import org.springframework.stereotype.Service;
@Service
public class DemoPrivateAccessService {
private int count;
/**
* private method
*/
private String privateFunc(String s, int i) {
return s + " - " + i;
}
/**
* method with private field access
*/
public String privateFieldAccessFunc() {
count += 2;
return String.valueOf(count);
}
}

View File

@@ -1,8 +1,8 @@
package com.alibaba.testable.demo;
package com.alibaba.testable.demo.service;
import com.alibaba.testable.core.accessor.PrivateAccessor;
import com.alibaba.testable.core.annotation.EnablePrivateAccess;
import com.alibaba.testable.core.annotation.TestableMock;
import com.alibaba.testable.demo.model.BlackBox;
import com.alibaba.testable.demo.model.Box;
import org.junit.jupiter.api.Test;
import java.util.concurrent.Executors;
@@ -10,8 +10,7 @@ import java.util.concurrent.Executors;
import static com.alibaba.testable.core.tool.TestableTool.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
@EnablePrivateAccess
class DemoServiceTest {
class DemoMockServiceTest {
@TestableMock(targetMethod = CONSTRUCTOR)
private BlackBox createBlackBox(String text) {
@@ -19,7 +18,7 @@ class DemoServiceTest {
}
@TestableMock
private String innerFunc(DemoService self, String text) {
private String innerFunc(DemoMockService self, String text) {
return "mock_" + text;
}
@@ -39,7 +38,17 @@ class DemoServiceTest {
}
@TestableMock
private String callFromDifferentMethod(DemoService self) {
private BlackBox secretBox(BlackBox ignore) {
return new BlackBox("not_secret_box");
}
@TestableMock
private void put(Box self, String something) {
self.put("put_" + something + "_mocked");
}
@TestableMock
private String callFromDifferentMethod(DemoMockService self) {
if (TEST_CASE.equals("should_able_to_get_test_case_name")) {
return "mock_special";
}
@@ -49,41 +58,39 @@ class DemoServiceTest {
}
}
private DemoService demoService = new DemoService();
private DemoMockService demoService = new DemoMockService();
@Test
void should_able_to_test_private_method() throws Exception {
assertEquals("hello - 1", demoService.privateFunc("hello", 1));
assertEquals("hello - 1", PrivateAccessor.invoke(demoService, "privateFunc", "hello", 1));
}
@Test
void should_able_to_test_private_field() throws Exception {
demoService.count = 2;
assertEquals("4", demoService.privateFieldAccessFunc());
PrivateAccessor.set(demoService, "count", 3);
assertEquals("5", demoService.privateFieldAccessFunc());
assertEquals(new Integer(5), PrivateAccessor.get(demoService, "count"));
}
@Test
void should_able_to_test_new_object() throws Exception {
void should_able_to_mock_new_object() throws Exception {
assertEquals("mock_something", demoService.newFunc());
verify("createBlackBox").times(1);
verify("createBlackBox").with("something");
}
@Test
void should_able_to_test_member_method() throws Exception {
void should_able_to_mock_member_method() throws Exception {
assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello"));
verify("innerFunc").times(1);
verify("innerFunc").with("hello");
}
@Test
void should_able_to_test_common_method() throws Exception {
void should_able_to_mock_common_method() throws Exception {
assertEquals("trim_string__sub_string__false", demoService.commonFunc());
verify("trim").times(1);
verify("sub").times(1);
verify("startsWith").times(1);
verify("trim").withTimes(1);
verify("sub").withTimes(1);
verify("startsWith").withTimes(1);
}
@Test
void should_able_to_mock_static_method() throws Exception {
assertEquals("not_secret_box", demoService.getBox().get());
verify("secretBox").withTimes(1);
}
@Test
void should_able_to_mock_override_method() throws Exception {
BlackBox box = (BlackBox)demoService.putBox();
verify("put").withTimes(1);
assertEquals("put_data_mocked", box.get());
}
@Test
@@ -93,7 +100,7 @@ class DemoServiceTest {
// asynchronous
assertEquals("mock_one_mock_others",
Executors.newSingleThreadExecutor().submit(() -> demoService.callerOne() + "_" + demoService.callerTwo()).get());
verify("callFromDifferentMethod").times(4);
verify("callFromDifferentMethod").withTimes(4);
}
@Test
@@ -102,7 +109,7 @@ class DemoServiceTest {
assertEquals("mock_special", demoService.callerOne());
// asynchronous
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoService.callerOne()).get());
verify("callFromDifferentMethod").times(2);
verify("callFromDifferentMethod").withTimes(2);
}
}

View File

@@ -0,0 +1,29 @@
package com.alibaba.testable.demo.service;
import com.alibaba.testable.core.accessor.PrivateAccessor;
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@EnablePrivateAccess
class DemoPrivateAccessServiceTest {
private DemoPrivateAccessService demoService = new DemoPrivateAccessService();
@Test
void should_able_to_mock_private_method() throws Exception {
assertEquals("hello - 1", demoService.privateFunc("hello", 1));
assertEquals("hello - 1", PrivateAccessor.invoke(demoService, "privateFunc", "hello", 1));
}
@Test
void should_able_to_mock_private_field() throws Exception {
demoService.count = 2;
assertEquals("4", demoService.privateFieldAccessFunc());
PrivateAccessor.set(demoService, "count", 3);
assertEquals("5", demoService.privateFieldAccessFunc());
assertEquals(new Integer(5), PrivateAccessor.get(demoService, "count"));
}
}

View File

@@ -17,7 +17,7 @@
<properties>
<java.version>1.8</java.version>
<kotlin.version>1.3.72</kotlin.version>
<testable.version>0.2.0-SNAPSHOT</testable.version>
<testable.version>0.2.2-SNAPSHOT</testable.version>
</properties>
<dependencies>
@@ -40,7 +40,7 @@
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-core</artifactId>
<artifactId>testable-processor</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>

View File

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

View File

@@ -0,0 +1,38 @@
package com.alibaba.testable.demo.model
class BlackBox(private var data: String) : Box {
override fun put(something: String) {
data = something
}
fun get(): String {
return data
}
fun trim(): String {
return data.trim()
}
fun substring(from: Int, to: Int): String {
return data.substring(from, to)
}
fun startsWith(prefix: String): Boolean {
return data.startsWith(prefix)
}
companion object {
fun secretBox(): BlackBox {
return BlackBox("secret")
}
}
}
object ColorBox {
fun createBox(color: String, box: BlackBox): BlackBox {
return BlackBox("${color}_${box.get()}")
}
}

View File

@@ -0,0 +1,7 @@
package com.alibaba.testable.demo.model
interface Box {
fun put(something: String)
}

View File

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

View File

@@ -0,0 +1,26 @@
package com.alibaba.testable.demo.service
import org.springframework.stereotype.Service
@Service
class DemoPrivateAccessService {
private var count = 0
/**
* private method
*/
private fun privateFunc(s: String, i: Int): String {
return "$s - $i"
}
/**
* method with private field access
*/
fun privateFieldAccessFunc(): String {
count += 2
return count.toString()
}
}

View File

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

View File

@@ -1,22 +1,22 @@
package com.alibaba.testable.demo
package com.alibaba.testable.demo.service
import com.alibaba.testable.core.accessor.PrivateAccessor
import com.alibaba.testable.core.annotation.EnablePrivateAccess
import com.alibaba.testable.core.annotation.TestableMock
import com.alibaba.testable.core.tool.TestableTool.*
import com.alibaba.testable.demo.model.BlackBox
import com.alibaba.testable.demo.model.Box
import com.alibaba.testable.demo.model.ColorBox
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.util.concurrent.Executors
@EnablePrivateAccess
internal class DemoServiceTest {
internal class DemoMockServiceTest {
@TestableMock(targetMethod = CONSTRUCTOR)
private fun createBlackBox(text: String) = BlackBox("mock_$text")
@TestableMock
private fun innerFunc(self: DemoService, text: String) = "mock_$text"
private fun innerFunc(self: DemoMockService, text: String) = "mock_$text"
@TestableMock
private fun trim(self: BlackBox) = "trim_string"
@@ -28,7 +28,22 @@ internal class DemoServiceTest {
private fun startsWith(self: BlackBox, s: String) = false
@TestableMock
private fun callFromDifferentMethod(self: DemoService): String {
private fun secretBox(ignore: BlackBox): BlackBox {
return BlackBox("not_secret_box")
}
@TestableMock
private fun createBox(ignore: ColorBox, color: String, box: BlackBox): BlackBox {
return BlackBox("White_${box.get()}")
}
@TestableMock
private fun put(self: Box, something: String) {
self.put("put_" + something + "_mocked")
}
@TestableMock
private fun callFromDifferentMethod(self: DemoMockService): String {
return if (TEST_CASE == "should_able_to_get_test_case_name") {
"mock_special"
} else {
@@ -39,38 +54,40 @@ internal class DemoServiceTest {
}
}
private val demoService = DemoService()
private val demoService = DemoMockService()
@Test
fun should_able_to_test_private_method() {
assertEquals("hello - 1", PrivateAccessor.invoke(demoService, "privateFunc", "hello", 1))
}
@Test
fun should_able_to_test_private_field() {
PrivateAccessor.set(demoService, "count", 3)
assertEquals("5", demoService.privateFieldAccessFunc())
assertEquals(5, PrivateAccessor.get(demoService, "count"))
}
@Test
fun should_able_to_test_new_object() {
fun should_able_to_mock_new_object() {
assertEquals("mock_something", demoService.newFunc())
verify("createBlackBox").times(1)
verify("createBlackBox").with("something")
}
@Test
fun should_able_to_test_member_method() {
fun should_able_to_mock_member_method() {
assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello"))
verify("innerFunc").times(1)
verify("innerFunc").with("hello")
}
@Test
fun should_able_to_test_common_method() {
fun should_able_to_mock_common_method() {
assertEquals("trim_string__sub_string__false", demoService.commonFunc())
verify("trim").times(1)
verify("sub").times(1)
verify("startsWith").times(1)
verify("trim").withTimes(1)
verify("sub").withTimes(1)
verify("startsWith").withTimes(1)
}
@Test
fun should_able_to_mock_static_method() {
assertEquals("White_not_secret_box", demoService.getBox().get())
verify("secretBox").withTimes(1)
verify("createBox").withTimes(1)
}
@Test
fun should_able_to_mock_override_method() {
val box = demoService.putBox() as BlackBox
verify("put").withTimes(1)
assertEquals("put_data_mocked", box.get())
}
@Test
@@ -81,7 +98,7 @@ internal class DemoServiceTest {
assertEquals("mock_one_mock_others", Executors.newSingleThreadExecutor().submit<String> {
demoService.callerOne() + "_" + demoService.callerTwo()
}.get())
verify("callFromDifferentMethod").times(4)
verify("callFromDifferentMethod").withTimes(4)
}
@Test
@@ -92,6 +109,6 @@ internal class DemoServiceTest {
assertEquals("mock_special", Executors.newSingleThreadExecutor().submit<String> {
demoService.callerOne()
}.get())
verify("callFromDifferentMethod").times(2)
verify("callFromDifferentMethod").withTimes(2)
}
}

View File

@@ -0,0 +1,23 @@
package com.alibaba.testable.demo.service
import com.alibaba.testable.core.accessor.PrivateAccessor
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
internal class DemoPrivateAccessServiceTest {
private val demoService = DemoPrivateAccessService()
@Test
fun should_able_to_mock_private_method() {
assertEquals("hello - 1", PrivateAccessor.invoke(demoService, "privateFunc", "hello", 1))
}
@Test
fun should_able_to_mock_private_field() {
PrivateAccessor.set(demoService, "count", 3)
assertEquals("5", demoService.privateFieldAccessFunc())
assertEquals(5, PrivateAccessor.get(demoService, "count"))
}
}

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
常见用户问题
---
**1. 如何Mock被测类中通过`@Autowired`初始化的字段?**
直接创建被测类对象,然后利用`Testable`访问私有成员的能力直接给这些字段赋值即可。
**2. 通过<u>接口对象或基类对象</u>指向派生类的实例,调用执行了派生类实现的方法。使用`@TestableMock`定义Mock方法时首个参数类型应该用 接口/基类 还是 派生类?**
应该使用 接口/基类 类型,参见`should_able_to_mock_override_method`测试用例。

46
docs/known-issues.md Normal file
View File

@@ -0,0 +1,46 @@
已知问题
---
**1. 访问私有方法或私有成员代码在IDE提示语法错误**
使用`@EnablePrivateAccessor`注解后访问私有方法或成员变量虽然能正常通过编译但在IDE上依然会提示语法错误。
这个问题与使用`Lombok`工具库后使用生成的`getter``setter`会被IDE报语法错误一样需要通过IDE插件来解决。
当前`Testable`尚未提供相关插件。也可以改用`PrivateAccessor`工具类来访问私有成员来避免IDE的异常信息。
**2. 通过IDE运行单个测试用例时Mock功能失效**
这是由于IDE运行单个测试用例时只会运行`maven-surefire-plugin`插件,跳过了`testable-maven-plugin`插件执行导致Mock功能所需的JavaAgent没有随测试启动。
解决方法有两种:
**方法一**:在单元测试配置的"虚拟机参数VM Option"属性值末尾添加JavaAgent启动参数`-javaagent:${HOME}/.m2/repository/com/alibaba/testable/testable-agent/0.2.2/testable-agent-0.2.2.jar`
> PS请将路径中的版本号替换成实际使用的版本号
![idea-vm-option](https://testable-code.oss-cn-beijing.aliyuncs.com/idea-vm-option.png)
**方法二**:不使用`testable-maven-plugin`插件直接配置JavaAgent参数到`maven-surefire-plugin`插件上。(`JMockit`也是使用了这种方法)配置方法为:
> PS请将路径中的版本号替换成实际使用的版本号
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/0.2.2/testable-agent-0.2.2.jar</argLine>
</configuration>
</plugin>
```
用这种方法需要注意,如果项目同时还使用了`Jacoco``on-the-fly`模式(默认模式)统计单元测试覆盖率,则需要在参数中再添加一个`@{argLine}`参数,完整配置如下:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/0.2.2/testable-agent-0.2.2.jar</argLine>
</configuration>
</plugin>
```

View File

@@ -1,5 +1,14 @@
# Release Note
## v0.2.2
- support mock method parameters check
- fix a compatibility issue with jvm 9+
## v0.2.1
- support mock static method
- support mock kotlin companion object method
- support mock invoke by interface / base class object
## v0.2.0
- use `TestableTool` class to expose test context and verify mock invoke
- add `testable-maven-plugin` module to simplify javaagent configuration

144
docs/usage.md Normal file
View File

@@ -0,0 +1,144 @@
使用说明
---
## 引入Testable
首先在项目`pom.xml`文件中添加`testable-processor`依赖:
```xml
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-processor</artifactId>
<version>${testable.version}</version>
<scope>provided</scope>
</dependency>
```
此时项目就获得了在单元测试中随意访问被测类私有字段和方法的能力(需配合注解使用,见下文详述)。
若要开启极速Mock功能还需在`pom.xml`里加上`testable-maven-plugin`插件。
```xml
<plugin>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-maven-plugin</artifactId>
<version>${testable.version}</version>
<executions>
<execution>
<id>prepare</id>
<goals>
<goal>prepare</goal>
</goals>
</execution>
</executions>
</plugin>
```
> PS其中`${testable.version}`需替换为具体版本号,当前最新版本为`0.2.2-SNAPSHOT`
## 使用Testable
`Testable`目前能为测试类提供两项增强能力__直接访问被测类的私有成员__ 和 __极速Mock被测方法中的调用__
### 访问私有成员字段和方法
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
- 调用被测类的私有方法
- 读取被测类的私有成员
- 修改被测类的私有成员
- 修改被测类的常量成员使用final或static final修饰的成员
访问和修改私有、常量成员时IDE可能会提示语法有误但编译器将能够正常运行测试。
若不希望看到IDE的语法错误提醒或是在基于JVM的非Java语言项目里譬如Kotlin语言也可以借助`PrivateAccessor`工具类来实现私有成员的访问。
效果见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_private_method()``should_able_to_mock_private_field()`测试用例。
### Mock被测类的任意方法调用
**1. <u>覆写任意类的方法调用</u>**
在测试类里定义一个有`@TestableMock`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,然后在其参数列表首位再增加一个类型为该方法原本所属对象类型的参数。
此时被测类中所有对该需覆写方法的调用将在单元测试运行时将自动被替换为对上述自定义Mock方法的调用。
**注意**:也可以将需覆写的方法名写到`@TestableMock`注解的`targetMethod`参数里这样Mock方法自身就可以随意命名了当遇到重名的待覆写方法时特别有用
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在测试类定义如下方法:
```java
// 原方法签名为`String substring(int, int)`
// 调用此方法的对象`"anything"`类型为`String`
// 则Mock方法签名在其参数列表首位增加一个类型为`String`的参数(名字随意)
// 此参数可用于获得当时的实际调用者的值和上下文
@TestableMock
private String substring(String self, int i, int j) {
return "sub_string";
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_common_method()`测试用例。(由于Kotlin对String类型进行了魔改故Kotlin示例中将被测方法在`BlackBox`类里加了一层封装)
**2. <u>覆写被测类自身的成员方法</u>**
有时候在对某些方法进行测试时希望将被测类自身的另外一些成员方法Mock掉。
操作方法与前一种情况相同Mock方法的第一个参数类型需与被测类相同即可实现对被测类自身不论是公有或私有成员方法的覆写。
例如,被测类中有一个签名为`String innerFunc(String)`的私有方法,我们希望在测试的时候将它替换掉,则只需在测试类定义如下方法:
```java
// 被测类型是`DemoMockService`
// 因此在定义Mock方法时在目标方法参数首位加一个类型为`DemoMockService`的参数(名字随意)
@TestableMock
private String innerFunc(DemoMockService self, String text) {
return "mock_" + text;
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_member_method()`测试用例。
**3. <u>覆写任意类的静态方法</u>**
对于静态方法的Mock与普通方法相同。但需要注意的是对于静态方法传入Mock方法的第一个参数实际值始终是`null`
例如,在被测类中调用了`BlackBox`类型中的静态方法`secretBox()`,改方法签名为`BlackBox secretBox()`则Mock方法如下
```java
// 目标静态方法定义在`BlackBox`类型中
// 在定义Mock方法时在目标方法参数首位加一个类型为`BlackBox`的参数(名字随意)
// 此参数仅用于标识目标类型,实际传入值将始终为`null`
@TestableMock
private BlackBox secretBox(BlackBox ignore) {
return new BlackBox("not_secret_box");
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_static_method()`测试用例。
**4. <u>覆写任意类的new操作</u>**
在测试类里定义一个有`@TestableMock`注解的普通方法,将注解的`targetMethod`参数写为"<init>",然后使该方法与要被创建类型的构造函数参数、返回值类型完全一致,方法名称随意。
此时被测类中所有用`new`创建指定类的操作并使用了与Mock方法参数一致的构造函数将被替换为对该自定义方法的调用。
例如,在被测类中有一处`new BlackBox("something")`调用希望在测试时将它换掉通常是换成Mock对象或换成使用测试参数创建的临时对象则只需定义如下Mock方法
```java
// 要覆写的构造函数签名为`BlackBox(String)`
// 无需在Mock方法参数列表增加额外参数由于使用了`targetMethod`参数Mock方法的名称随意起
// 此处的`CONSTRUCTOR`为`TestableTool`辅助类提供的常量,值为"<init>"
@TestableMock(targetMethod = CONSTRUCTOR)
private BlackBox createBlackBox(String text) {
return new BlackBox("mock_" + text);
}
```
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_mock_new_object()`测试用例。
**5. <u>识别当前测试用例和调用来源</u>**
在Mock方法中可以通过`TestableTool.TEST_CASE``TestableTool.SOURCE_METHOD`来识别**当前运行的测试用例名称**和**进入该Mock方法前的被测类方法名称**,从而区分处理不同的调用场景。
完整代码示例见`java-demo``kotlin-demo`示例项目中的`should_able_to_get_source_method_name()``should_able_to_get_test_case_name()`测试用例。

View File

@@ -10,6 +10,7 @@
<modules>
<module>testable-core</module>
<module>testable-processor</module>
<module>testable-agent</module>
<module>testable-maven-plugin</module>
<module>demo</module>

View File

@@ -5,7 +5,7 @@
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-agent</artifactId>
<version>0.2.0-SNAPSHOT</version>
<version>0.2.2-SNAPSHOT</version>
<packaging>jar</packaging>
<name>testable-agent</name>
@@ -14,8 +14,10 @@
<project.compiler.level>1.6</project.compiler.level>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<asm.lib.version>8.0.1</asm.lib.version>
<testable.version>0.2.0-SNAPSHOT</testable.version>
<junit.version>5.6.2</junit.version>
<testable.version>0.2.2-SNAPSHOT</testable.version>
<plugin.compiler.version>3.8.1</plugin.compiler.version>
<plugin.surefire.version>3.0.0-M5</plugin.surefire.version>
<plugin.jar.version>3.2.0</plugin.jar.version>
<plugin.shade.version>3.2.4</plugin.shade.version>
</properties>
@@ -29,7 +31,7 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.2</version>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -89,6 +91,11 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${plugin.surefire.version}</version>
</plugin>
</plugins>
</build>

View File

@@ -12,5 +12,7 @@ public class ConstPool {
public static final String TEST_POSTFIX = "Test";
public static final String TESTABLE_INJECT_REF = "_testableInternalRef";
public static final String FIELD_TARGET_METHOD = "targetMethod";
public static final String TESTABLE_MOCK = "com.alibaba.testable.core.annotation.TestableMock";
}

View File

@@ -19,8 +19,10 @@ public class SourceClassHandler extends BaseClassHandler {
private final List<MethodInfo> injectMethods;
private final Set<Integer> invokeOps = new HashSet<Integer>() {{
add(Opcodes.INVOKESPECIAL);
add(Opcodes.INVOKEVIRTUAL);
add(Opcodes.INVOKESPECIAL);
add(Opcodes.INVOKESTATIC);
add(Opcodes.INVOKEINTERFACE);
}};
public SourceClassHandler(List<MethodInfo> injectMethods) {
@@ -57,10 +59,11 @@ public class SourceClassHandler extends BaseClassHandler {
MethodInsnNode node = (MethodInsnNode)instructions[i];
String memberInjectMethodName = getMemberInjectMethodName(memberInjectMethodList, node);
if (memberInjectMethodName != null) {
// it's a member method and an inject method for it exist
// it's a member or static method and an inject method for it exist
int rangeStart = getMemberMethodStart(instructions, i);
if (rangeStart >= 0) {
instructions = replaceMemberCallOps(cn, mn, instructions, node.owner, memberInjectMethodName, rangeStart, i);
instructions = replaceMemberCallOps(cn, mn, memberInjectMethodName, instructions,
node.owner, node.getOpcode(), rangeStart, i);
i = rangeStart;
}
} else if (ConstPool.CONSTRUCTOR.equals(node.name)) {
@@ -82,7 +85,8 @@ public class SourceClassHandler extends BaseClassHandler {
private String getMemberInjectMethodName(List<MethodInfo> memberInjectMethodList, MethodInsnNode node) {
for (MethodInfo m : memberInjectMethodList) {
if (m.getClazz().equals(node.owner) && m.getName().equals(node.name) && m.getDesc().equals(node.desc)) {
String nodeOwner = ClassUtil.fitCompanionClassName(node.owner);
if (m.getClazz().equals(nodeOwner) && m.getName().equals(node.name) && m.getDesc().equals(node.desc)) {
return m.getMockName();
}
}
@@ -116,13 +120,16 @@ public class SourceClassHandler extends BaseClassHandler {
int stackLevel = ClassUtil.getParameterTypes(((MethodInsnNode)instructions[rangeEnd]).desc).size();
for (int i = rangeEnd - 1; i >= 0; i--) {
switch (instructions[i].getOpcode()) {
case Opcodes.INVOKEINTERFACE:
case Opcodes.INVOKEVIRTUAL:
case Opcodes.INVOKESPECIAL:
case Opcodes.INVOKEDYNAMIC:
case Opcodes.INVOKESTATIC:
case Opcodes.INVOKEINTERFACE:
case Opcodes.INVOKEDYNAMIC:
stackLevel += ClassUtil.getParameterTypes(((MethodInsnNode)instructions[i]).desc).size();
break;
case -1:
// reach LineNumberNode or LabelNode
return i + 1;
default:
stackLevel -= BytecodeUtil.stackEffect(instructions[i].getOpcode());
}
@@ -153,19 +160,32 @@ public class SourceClassHandler extends BaseClassHandler {
ClassUtil.toByteCodeClassName(classType);
}
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions,
String ownerClass, String substitutionMethod, int start, int end) {
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, String substitutionMethod,
AbstractInsnNode[] instructions, String ownerClass,
int opcode, int start, int end) {
mn.maxStack++;
MethodInsnNode method = (MethodInsnNode)instructions[end];
String testClassName = ClassUtil.getTestClassName(cn.name);
mn.instructions.insertBefore(instructions[start], new FieldInsnNode(GETSTATIC, testClassName,
ConstPool.TESTABLE_INJECT_REF, ClassUtil.toByteCodeClassName(testClassName)));
if (Opcodes.INVOKESTATIC == opcode || isCompanionMethod(ownerClass, opcode)) {
// append a null value if it was a static invoke or in kotlin companion class
mn.instructions.insertBefore(instructions[start], new InsnNode(ACONST_NULL));
if (ClassUtil.isCompanionClassName(ownerClass)) {
// for kotlin companion class, remove the byte code of reference to "companion" static field
mn.instructions.remove(instructions[end - 1]);
}
}
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,
substitutionMethod, addFirstParameter(method.desc, ownerClass), false));
substitutionMethod, addFirstParameter(method.desc, ClassUtil.fitCompanionClassName(ownerClass)), false));
mn.instructions.remove(instructions[end]);
return mn.instructions.toArray();
}
private boolean isCompanionMethod(String ownerClass, int opcode) {
return Opcodes.INVOKEVIRTUAL == opcode && ClassUtil.isCompanionClassName(ownerClass);
}
private String addFirstParameter(String desc, String ownerClass) {
return "(" + ClassUtil.toByteCodeClassName(ownerClass) + desc.substring(1);
}

View File

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

View File

@@ -6,6 +6,7 @@ import com.alibaba.testable.agent.handler.TestClassHandler;
import com.alibaba.testable.agent.tool.ImmutablePair;
import com.alibaba.testable.agent.model.MethodInfo;
import com.alibaba.testable.agent.tool.ComparableWeakRef;
import com.alibaba.testable.agent.util.AnnotationUtil;
import com.alibaba.testable.agent.util.ClassUtil;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.AnnotationNode;
@@ -28,7 +29,6 @@ import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassNa
public class TestableClassTransformer implements ClassFileTransformer {
private final Set<ComparableWeakRef<String>> loadedClassNames = ComparableWeakRef.getWeekHashSet();
private static final String TARGET_METHOD = "targetMethod";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
@@ -64,6 +64,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
}
private boolean isSystemClass(ClassLoader loader, String className) {
// className can be null for Java 8 lambdas
return !(loader instanceof URLClassLoader) || null == className || className.startsWith("jdk/");
}
@@ -89,7 +90,8 @@ public class TestableClassTransformer implements ClassFileTransformer {
for (AnnotationNode an : mn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_MOCK)) {
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
String targetMethod = getAnnotationParameter(an, TARGET_METHOD, mn.name);
String targetMethod = AnnotationUtil.getAnnotationParameter(
an, ConstPool.FIELD_TARGET_METHOD, mn.name, String.class);
if (targetMethod.equals(ConstPool.CONSTRUCTOR)) {
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
methodInfos.add(new MethodInfo(sourceClassName, targetMethod, mn.name, mn.desc));
@@ -111,18 +113,4 @@ public class TestableClassTransformer implements ClassFileTransformer {
return pos < 0 ? null : ImmutablePair.of(desc.substring(1, pos + 1), "(" + desc.substring(pos + 1));
}
/**
* Read value of annotation parameter
*/
private <T> T getAnnotationParameter(AnnotationNode an, String key, T defaultValue) {
if (an.values != null) {
for (int i = 0; i < an.values.size(); i += 2) {
if (an.values.get(i).equals(key)) {
return (T)(an.values.get(i + 1));
}
}
}
return defaultValue;
}
}

View File

@@ -0,0 +1,24 @@
package com.alibaba.testable.agent.util;
import org.objectweb.asm.tree.AnnotationNode;
/**
* @author flin
*/
public class AnnotationUtil {
/**
* Read value of annotation parameter
*/
public static <T> T getAnnotationParameter(AnnotationNode an, String key, T defaultValue, Class<T> clazz) {
if (an.values != null) {
for (int i = 0; i < an.values.size(); i += 2) {
if (an.values.get(i).equals(key)) {
return clazz.cast(an.values.get(i + 1));
}
}
}
return defaultValue;
}
}

View File

@@ -5,41 +5,57 @@ import com.alibaba.testable.agent.tool.ComparableWeakRef;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.util.*;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
/**
* @author flin
*/
public class ClassUtil {
private static final char TYPE_BYTE = 'B';
private static final char TYPE_CHAR = 'C';
private static final char TYPE_DOUBLE = 'D';
private static final char TYPE_FLOAT = 'F';
private static final char TYPE_INT = 'I';
private static final char TYPE_LONG = 'J';
private static final char TYPE_CLASS = 'L';
private static final char TYPE_SHORT = 'S';
private static final char TYPE_BOOL = 'Z';
private static final char PARAM_END = ')';
private static final char CLASS_END = ';';
private static final char TYPE_ARRAY = '[';
public static final byte TYPE_BYTE = 'B';
public static final byte TYPE_CHAR = 'C';
public static final byte TYPE_DOUBLE = 'D';
public static final byte TYPE_FLOAT = 'F';
public static final byte TYPE_INT = 'I';
public static final byte TYPE_LONG = 'J';
public static final byte TYPE_CLASS = 'L';
public static final byte TYPE_SHORT = 'S';
public static final byte TYPE_BOOL = 'Z';
private static final byte PARAM_END = ')';
private static final byte CLASS_END = ';';
private static final byte TYPE_ARRAY = '[';
private static final Map<Character, String> TYPE_MAPPING = new HashMap<Character, String>();
public static final String CLASS_OBJECT = "java/lang/Object";
private static final String CLASS_BYTE = "java/lang/Byte";
private static final String CLASS_CHARACTER = "java/lang/Character";
private static final String CLASS_DOUBLE = "java/lang/Double";
private static final String CLASS_FLOAT = "java/lang/Float";
private static final String CLASS_INTEGER = "java/lang/Integer";
private static final String CLASS_LONG = "java/lang/Long";
private static final String CLASS_SHORT = "java/lang/Short";
private static final String CLASS_BOOLEAN = "java/lang/Boolean";
private static final String METHOD_VALUE_OF = "valueOf";
private final static String JOINER = "::";
private static final Map<Byte, String> TYPE_MAPPING = new HashMap<Byte, String>();
private static final Map<ComparableWeakRef<String>, Boolean> loadedClass =
new WeakHashMap<ComparableWeakRef<String>, Boolean>();
static {
TYPE_MAPPING.put(TYPE_BYTE, "java/lang/Byte");
TYPE_MAPPING.put(TYPE_CHAR, "java/lang/Character");
TYPE_MAPPING.put(TYPE_DOUBLE, "java/lang/Double");
TYPE_MAPPING.put(TYPE_FLOAT, "java/lang/Float");
TYPE_MAPPING.put(TYPE_INT, "java/lang/Integer");
TYPE_MAPPING.put(TYPE_LONG, "java/lang/Long");
TYPE_MAPPING.put(TYPE_SHORT, "java/lang/Short");
TYPE_MAPPING.put(TYPE_BOOL, "java/lang/Boolean");
TYPE_MAPPING.put(TYPE_BYTE, CLASS_BYTE);
TYPE_MAPPING.put(TYPE_CHAR, CLASS_CHARACTER);
TYPE_MAPPING.put(TYPE_DOUBLE, CLASS_DOUBLE);
TYPE_MAPPING.put(TYPE_FLOAT, CLASS_FLOAT);
TYPE_MAPPING.put(TYPE_INT, CLASS_INTEGER);
TYPE_MAPPING.put(TYPE_LONG, CLASS_LONG);
TYPE_MAPPING.put(TYPE_SHORT, CLASS_SHORT);
TYPE_MAPPING.put(TYPE_BOOL, CLASS_BOOLEAN);
}
/**
@@ -48,7 +64,8 @@ public class ClassUtil {
* @param annotationName annotation to look for
*/
public static boolean anyMethodHasAnnotation(String className, String annotationName) {
Boolean found = loadedClass.get(new ComparableWeakRef<String>(className));
String cacheKey = className + JOINER + annotationName;
Boolean found = loadedClass.get(new ComparableWeakRef<String>(cacheKey));
if (found != null) {
return found;
}
@@ -59,7 +76,7 @@ public class ClassUtil {
if (mn.visibleAnnotations != null) {
for (AnnotationNode an : mn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(annotationName)) {
loadedClass.put(new ComparableWeakRef<String>(className), true);
loadedClass.put(new ComparableWeakRef<String>(cacheKey), true);
return true;
}
}
@@ -68,10 +85,26 @@ public class ClassUtil {
} catch (Exception e) {
// ignore
}
loadedClass.put(new ComparableWeakRef<String>(className), false);
loadedClass.put(new ComparableWeakRef<String>(cacheKey), false);
return false;
}
/**
* fit kotlin companion class name to original name
* @param name a class name (which could be a companion class)
*/
public static boolean isCompanionClassName(String name) {
return name.endsWith("$Companion");
}
/**
* fit kotlin companion class name to original name
* @param name a class name (which could be a companion class)
*/
public static String fitCompanionClassName(String name) {
return name.replaceAll("\\$Companion$", "");
}
/**
* get test class name from source class name
* @param sourceClassName source class name
@@ -123,13 +156,27 @@ public class ClassUtil {
return desc.substring(returnTypeEdge + 1);
} else if (typeChar == TYPE_CLASS) {
return desc.substring(returnTypeEdge + 2, desc.length() - 1);
} else if (TYPE_MAPPING.containsKey(typeChar)) {
return TYPE_MAPPING.get(typeChar);
} else if (TYPE_MAPPING.containsKey((byte)typeChar)) {
return TYPE_MAPPING.get((byte)typeChar);
} else {
return "";
}
}
/**
* Get method node to convert primary type to object type
* @param type primary type to convert
*/
public static MethodInsnNode getPrimaryTypeConvertMethod(Byte type) {
String objectType = TYPE_MAPPING.get(type);
return (objectType == null) ? null :
new MethodInsnNode(INVOKESTATIC, objectType, METHOD_VALUE_OF, toDescriptor(type, objectType), false);
}
private static String toDescriptor(Byte type, String objectType) {
return "(" + (char)type.byteValue() + ")L" + objectType + ";";
}
/**
* convert slash separated name to dot separated name
*/
@@ -148,7 +195,7 @@ public class ClassUtil {
* convert dot separated name to byte code class name
*/
public static String toByteCodeClassName(String className) {
return TYPE_CLASS + toSlashSeparatedName(className) + CLASS_END;
return (char)TYPE_CLASS + toSlashSeparatedName(className) + (char)CLASS_END;
}
/**
@@ -169,5 +216,4 @@ public class ClassUtil {
return b == TYPE_BYTE || b == TYPE_CHAR || b == TYPE_DOUBLE || b == TYPE_FLOAT
|| b == TYPE_INT || b == TYPE_LONG || b == TYPE_SHORT || b == TYPE_BOOL;
}
}

View File

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

View File

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

View File

@@ -12,9 +12,9 @@ class ClassUtilTest {
@Test
void should_able_to_get_annotation() {
assertEquals(false, ClassUtil.anyMethodHasAnnotation("class.not.exist", ""));
assertEquals(false, ClassUtil.anyMethodHasAnnotation("org.junit.jupiter.api.Assertions", "annotation.not.exist"));
assertEquals(true, ClassUtil.anyMethodHasAnnotation("org.junit.jupiter.api.Assertions", "org.apiguardian.api.API"));
assertFalse(ClassUtil.anyMethodHasAnnotation("class.not.exist", ""));
assertFalse(ClassUtil.anyMethodHasAnnotation("com.alibaba.testable.agent.util.ClassUtilTest", "annotation.not.exist"));
assertTrue(ClassUtil.anyMethodHasAnnotation("com.alibaba.testable.agent.util.ClassUtilTest", "org.junit.jupiter.api.Test"));
}
@Test
@@ -40,5 +40,17 @@ class ClassUtilTest {
assertEquals("Ljava/lang/String;", ClassUtil.toByteCodeClassName("java.lang.String"));
}
@Test
void should_able_to_fit_companion_class_name() {
assertEquals("com/intellij/rt/debugger/agent/CaptureAgent$ParamKeyProvider",
ClassUtil.fitCompanionClassName("com/intellij/rt/debugger/agent/CaptureAgent$ParamKeyProvider"));
assertEquals("com/alibaba/testable/demo/BlackBox",
ClassUtil.fitCompanionClassName("com/alibaba/testable/demo/BlackBox"));
assertEquals("com/alibaba/testable/demo/BlackBox$Companion",
ClassUtil.fitCompanionClassName("com/alibaba/testable/demo/BlackBox$Companion$Companion"));
assertEquals("com/alibaba/testable/demo/BlackBox",
ClassUtil.fitCompanionClassName("com/alibaba/testable/demo/BlackBox$Companion"));
}
}

View File

@@ -7,7 +7,7 @@
<description>Unit test enhancement toolkit</description>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-core</artifactId>
<version>0.2.0-SNAPSHOT</version>
<version>0.2.2-SNAPSHOT</version>
<name>testable-core</name>
<properties>
@@ -15,20 +15,15 @@
<project.compiler.level>1.6</project.compiler.level>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<plugin.compiler.version>3.8.1</plugin.compiler.version>
<plugin.surefire.version>3.0.0-M5</plugin.surefire.version>
<junit.version>5.6.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>sun.jdk</groupId>
<artifactId>tools</artifactId>
<version>${java.version}</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.2</version>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
@@ -43,9 +38,13 @@
<source>${project.compiler.level}</source>
<target>${project.compiler.level}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${plugin.surefire.version}</version>
</plugin>
</plugins>
</build>

View File

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

View File

@@ -1,5 +1,7 @@
package com.alibaba.testable.core.annotation;
import com.alibaba.testable.core.model.MockType;
import java.lang.annotation.*;
/**
@@ -12,6 +14,11 @@ import java.lang.annotation.*;
@Documented
public @interface TestableMock {
/**
* type of mock method
*/
MockType value() default MockType.MEMBER_METHOD;
/**
* mock specified method instead of method with same name
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,175 @@
package com.alibaba.testable.core.tool;
import com.alibaba.testable.core.error.VerifyFailedError;
import com.alibaba.testable.core.model.Verification;
import java.security.InvalidParameterException;
import java.util.List;
/**
* @author flin
*/
public class InvokeVerifier {
private final List<Object[]> records;
private Verification lastVerification = null;
public InvokeVerifier(List<Object[]> records) {
this.records = records;
}
public InvokeVerifier with(Object arg1) {
return with(new Object[]{arg1});
}
public InvokeVerifier with(Object arg1, Object arg2) {
return with(new Object[]{arg1, arg2});
}
public InvokeVerifier with(Object arg1, Object arg2, Object arg3) {
return with(new Object[]{arg1, arg2, arg3});
}
public InvokeVerifier with(Object arg1, Object arg2, Object arg3, Object arg4) {
return with(new Object[]{arg1, arg2, arg3, arg4});
}
public InvokeVerifier with(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) {
return with(new Object[]{arg1, arg2, arg3, arg4, arg5});
}
public InvokeVerifier withInOrder(Object arg1) {
return withInOrder(new Object[]{arg1});
}
public InvokeVerifier withInOrder(Object arg1, Object arg2) {
return withInOrder(new Object[]{arg1, arg2});
}
public InvokeVerifier withInOrder(Object arg1, Object arg2, Object arg3) {
return withInOrder(new Object[]{arg1, arg2, arg3});
}
public InvokeVerifier withInOrder(Object arg1, Object arg2, Object arg3, Object arg4) {
return withInOrder(new Object[]{arg1, arg2, arg3, arg4});
}
public InvokeVerifier withInOrder(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) {
return withInOrder(new Object[]{arg1, arg2, arg3, arg4, arg5});
}
/**
* Expect mock method invoked with specified parameters
* @param args parameters to compare
*/
public InvokeVerifier with(Object[] args) {
boolean found = false;
for (int i = 0; i < records.size(); i++) {
try {
withInternal(args, i);
found = true;
break;
} catch (AssertionError e) {
// continue
}
}
if (!found) {
throw new VerifyFailedError("has not invoke with " + desc(args));
}
lastVerification = new Verification(args, false);
return this;
}
/**
* Expect next mock method call was invoked with specified parameters
* @param args parameters to compare
*/
public InvokeVerifier withInOrder(Object[] args) {
withInternal(args, 0);
lastVerification = new Verification(args, true);
return this;
}
/**
* Expect mock method had never invoked with specified parameters
* @param args parameters to compare
*/
public InvokeVerifier without(Object[] args) {
for (Object[] r : records) {
if (r.length == args.length) {
for (int i = 0; i < r.length; i++) {
if (!r[i].equals(args[i])) {
break;
}
}
throw new VerifyFailedError("was invoked with " + desc(args));
}
}
return this;
}
/**
* Expect mock method have been invoked specified times
* @param expectedCount times to compare
*/
public InvokeVerifier withTimes(int expectedCount) {
if (expectedCount != records.size()) {
throw new VerifyFailedError("times: " + records.size(), "times: " + expectedCount);
}
lastVerification = null;
return this;
}
/**
* Expect several consecutive invocations with the same parameters
* @param count number of invocations
*/
public InvokeVerifier times(int count) {
if (count < 2) {
throw new InvalidParameterException("should only use times() method with count equal or larger than 2.");
} else if (lastVerification == null) {
throw new InvalidParameterException("should only use times() after with() or withInOrder() method.");
}
for (int i = 0; i < count - 1; i++) {
if (lastVerification.inOrder) {
withInOrder(lastVerification.parameters);
} else {
with(lastVerification.parameters);
}
}
lastVerification = null;
return this;
}
private void withInternal(Object[] args, int order) {
if (records.isEmpty()) {
throw new VerifyFailedError("has not more invoke");
}
Object[] record = records.get(order);
if (record.length != args.length) {
throw new VerifyFailedError(desc(args), desc(record));
}
for (int i = 0; i < args.length; i++) {
if (!args[i].getClass().equals(record[i].getClass())) {
throw new VerifyFailedError("parameter " + (i + 1) + " type mismatch",
": " + args[i].getClass(), ": " + record[i].getClass());
}
if (!args[i].equals(record[i])) {
throw new VerifyFailedError("parameter " + (i + 1) + " mismatched", desc(args), desc(record));
}
}
records.remove(order);
}
private String desc(Object[] args) {
StringBuilder sb = new StringBuilder(": ");
for (int i = 0; i < args.length; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(args[i]);
}
return sb.toString();
}
}

View File

@@ -1,5 +1,6 @@
package com.alibaba.testable.core.tool;
import com.alibaba.testable.core.util.InvokeRecordUtil;
import com.alibaba.testable.core.util.TestableUtil;
/**
@@ -26,10 +27,10 @@ public class TestableTool {
* Get counter to check whether specified mock method invoked
* @param mockMethodName name of a mock method
*/
public static InvokeCounter verify(String mockMethodName) {
String testClass = Thread.currentThread().getStackTrace()[TestableUtil.INDEX_OF_TEST_CLASS].getClassName();
public static InvokeVerifier verify(String mockMethodName) {
String testClass = Thread.currentThread().getStackTrace()[InvokeRecordUtil.INDEX_OF_TEST_CLASS].getClassName();
String testCaseName = TestableUtil.currentTestCaseName(testClass);
return new InvokeCounter(TestableUtil.getInvokeCount(mockMethodName, testCaseName));
return new InvokeVerifier(InvokeRecordUtil.getInvokeRecord(mockMethodName, testCaseName));
}
}

View File

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

View File

@@ -2,34 +2,12 @@ package com.alibaba.testable.core.util;
import com.alibaba.testable.core.constant.ConstPool;
import java.util.HashMap;
import java.util.Map;
/**
* @author flin
*/
public class TestableUtil {
private static final Map<String, Integer> INVOKE_RECORDS = new HashMap<String, Integer>();
private final static String JOINER = "->";
/**
* [0]Thread -> [1]TestableUtil/TestableTool -> [2]TestClass
*/
public static final int INDEX_OF_TEST_CLASS = 2;
/**
* Record mock method invoke event
*/
public static void countMockInvoke() {
StackTraceElement mockMethodTraceElement = Thread.currentThread().getStackTrace()[INDEX_OF_TEST_CLASS];
String mockMethodName = mockMethodTraceElement.getMethodName();
String testClass = mockMethodTraceElement.getClassName();
String testCaseName = TestableUtil.currentTestCaseName(testClass);
String key = testCaseName + JOINER + mockMethodName;
int count = getInvokeCount(mockMethodName, testCaseName);
INVOKE_RECORDS.put(key, count + 1);
}
/**
* Get the last visit method in source file
* @param testClassRef usually `this` variable of the test class
@@ -72,15 +50,6 @@ public class TestableUtil {
return "";
}
public static int getInvokeCount(String mockMethodName, String testCaseName) {
String key = testCaseName + JOINER + mockMethodName;
Integer count = INVOKE_RECORDS.get(key);
if (count == null) {
count = 0;
}
return count;
}
private static String findLastMethodFromSourceClass(String sourceClassName, StackTraceElement[] stack) {
for (StackTraceElement element : stack) {
if (element.getClassName().equals(sourceClassName)) {

View File

@@ -36,7 +36,7 @@ public class TypeUtil {
/**
* type equals
*/
public static boolean typeEquals(Class<?>[] classesLeft, Class<?>[] classesRight) {
private static boolean typeEquals(Class<?>[] classesLeft, Class<?>[] classesRight) {
if (classesLeft.length != classesRight.length) {
return false;
}

View File

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

View File

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

View File

@@ -4,12 +4,12 @@
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-maven-plugin</artifactId>
<packaging>maven-plugin</packaging>
<version>0.2.0-SNAPSHOT</version>
<version>0.2.2-SNAPSHOT</version>
<name>testable-maven-plugin</name>
<url>http://maven.apache.org</url>
<properties>
<testable.version>0.2.0-SNAPSHOT</testable.version>
<testable.version>0.2.2-SNAPSHOT</testable.version>
<java.version>1.6</java.version>
<project.compiler.level>1.6</project.compiler.level>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<description>Unit test enhancement toolkit</description>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-processor</artifactId>
<version>0.2.2-SNAPSHOT</version>
<name>testable-processor</name>
<properties>
<java.version>1.6</java.version>
<project.compiler.level>1.6</project.compiler.level>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<plugin.compiler.version>3.8.1</plugin.compiler.version>
<plugin.surefire.version>3.0.0-M5</plugin.surefire.version>
<junit.version>5.6.2</junit.version>
<testable.version>0.2.2-SNAPSHOT</testable.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-core</artifactId>
<version>${testable.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>tools-jar</id>
<activation>
<file>
<exists>${java.home}/../lib/tools.jar</exists>
</file>
</activation>
<dependencies>
<dependency>
<groupId>sun.jdk</groupId>
<artifactId>tools</artifactId>
<version>${java.version}</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${plugin.compiler.version}</version>
<configuration>
<source>${project.compiler.level}</source>
<target>${project.compiler.level}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${plugin.surefire.version}</version>
</plugin>
</plugins>
</build>
</project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
package com.alibaba.testable.core.translator;
package com.alibaba.testable.processor.translator;
import com.alibaba.testable.core.constant.ConstPool;
import com.alibaba.testable.core.generator.PrivateAccessStatementGenerator;
import com.alibaba.testable.core.model.TestableContext;
import com.alibaba.testable.processor.constant.ConstPool;
import com.alibaba.testable.processor.generator.PrivateAccessStatementGenerator;
import com.alibaba.testable.processor.model.TestableContext;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Name;

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.core.util;
package com.alibaba.testable.processor.util;
import java.util.List;

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package com.alibaba.testable.core.util;
package com.alibaba.testable.processor.util;
import org.junit.jupiter.api.Test;