mirror of
https://github.com/alibaba/testable-mock.git
synced 2026-08-29 18:58:15 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e88813518 | ||
|
|
88fddf2bcc | ||
|
|
ba2a823c19 | ||
|
|
4b282179f4 | ||
|
|
6454ae547e | ||
|
|
5b7382e9b0 | ||
|
|
dc904496fc | ||
|
|
6f74ba771c | ||
|
|
df63bc5c74 | ||
|
|
da3eca3d49 | ||
|
|
d816726f4f | ||
|
|
4b1b1c3126 | ||
|
|
649fdebda7 | ||
|
|
24e4b4f317 | ||
|
|
0b9a9c7ac6 | ||
|
|
e33c208b8d | ||
|
|
afecf4ddf5 | ||
|
|
ae134ac5ce | ||
|
|
36136e878e | ||
|
|
0ef13a8e95 | ||
|
|
989a52a048 | ||
|
|
419a10669d | ||
|
|
2ad671e1f9 | ||
|
|
f9fc1e6224 |
@@ -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
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.alibaba.testable.demo.model;
|
||||
|
||||
public interface Box {
|
||||
|
||||
void put(String something);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.alibaba.testable.demo.model
|
||||
|
||||
interface Box {
|
||||
|
||||
fun put(something: String)
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()`用例。
|
||||
10
docs/frequency-asked-questions.md
Normal file
10
docs/frequency-asked-questions.md
Normal 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
46
docs/known-issues.md
Normal 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:请将路径中的版本号替换成实际使用的版本号
|
||||
|
||||

|
||||
|
||||
**方法二**:不使用`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>
|
||||
```
|
||||
@@ -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
144
docs/usage.md
Normal 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()`测试用例。
|
||||
1
pom.xml
1
pom.xml
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.alibaba.testable.core.model;
|
||||
|
||||
/**
|
||||
* Type of mock method
|
||||
*
|
||||
* @author flin
|
||||
*/
|
||||
public enum MockType {
|
||||
|
||||
MEMBER_METHOD,
|
||||
STATIC_METHOD,
|
||||
CONSTRUCTOR
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
com.alibaba.testable.core.processor.EnablePrivateAccessProcessor
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
78
testable-processor/pom.xml
Normal file
78
testable-processor/pom.xml
Normal 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>
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.core.annotation;
|
||||
package com.alibaba.testable.processor.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
@@ -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.*;
|
||||
|
||||
/**
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
package com.alibaba.testable.processor.util;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
com.alibaba.testable.processor.EnablePrivateAccessProcessor
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
package com.alibaba.testable.processor.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
Reference in New Issue
Block a user