Compare commits

...

18 Commits
0.7 ... v0.7.4

Author SHA1 Message Date
金戟
68e370b6e3 release v0.7.4 2022-02-02 19:41:53 +08:00
金戟
4ec08cfb6b fix #261, java.net.URL is loaded before OmniClassHandler 2022-02-02 16:50:52 +08:00
金戟
3120eba3af update release note 2022-01-10 20:55:39 +08:00
金戟
02f15177fc release v0.7.3 2022-01-09 22:45:14 +08:00
Fan Lin
a25f14c0ec Merge pull request #257 from zcbbpo/FIXED-INVOKEINTERFACE_1
Fixed invokeinterface issuse
2022-01-07 14:55:25 +08:00
jimcao
635cbf62fb completed 2022-01-06 15:13:32 +08:00
jimcao
17b31b39dd refine code 2022-01-06 13:13:09 +08:00
jimcao
0d43d93a29 temp update 2022-01-06 11:52:18 +08:00
jimcao
66db3cff7d temp 2022-01-05 09:49:49 +08:00
jimcao
3343a26de8 Add interface static test case 2022-01-05 09:12:38 +08:00
jimcao
de2e520d95 Fixed interface default method 2022-01-04 18:02:49 +08:00
金戟
59bcbfdf37 release v0.7.2 2021-12-17 14:39:20 +08:00
Fan Lin
cdad0d2446 Merge pull request #251 from zcbbpo/Fxied-static-instance-reference-issue
fixed static instance reference issue
2021-12-17 13:40:45 +08:00
jimcao
ba5102ffd9 fixed static instance reference issue 2021-12-16 17:26:38 +08:00
金戟
db4e02a201 release v0.7.1 2021-12-11 22:59:49 +08:00
金戟
fda7ee23c7 fix #248, should not look up package mapping if found mock class 2021-12-11 14:40:27 +08:00
金戟
140e54983b release v0.7.0 2021-11-07 11:07:39 +08:00
金戟
548d7ba21f add upgrade guide for 0.7.0 2021-11-07 11:06:37 +08:00
42 changed files with 717 additions and 188 deletions

View File

@@ -9,7 +9,7 @@
阅读[这里](https://mp.weixin.qq.com/s/KyU6Eu7mDkZU8FspfSqfMw)了解更多故事。
<font size="5">**0.6版本已发布**</font>,从`0.5.x`升级到`0.6.x`版本请参考[版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-guide)
<font size="5">**0.7版本已发布**</font>,从`0.6.x`升级到`0.7.x`版本请参考[版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-guide)
-----
@@ -31,7 +31,9 @@
|-- tool ➜ 项目开发过程中的工具脚本
|-- demo
| |-- java-demo ➜ Java语言的示例代码
| `-- kotlin-demo ➜ Kotlin语言的示例代码
| |-- kotlin-demo ➜ Kotlin语言的示例代码
| |-- android-demo ➜ Android项目的示例代码
| `-- spock-demo ➜ Spock测试框架的示例代码
`-- docs ➜ 项目使用文档
```

View File

@@ -9,11 +9,7 @@ Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
## Roadmap
`TestableMock` is still under heavy development, the following version plans may be adjusted during the iteration
- `v0.6` it's the current version, refer to the [issue](https://github.com/alibaba/testable-mock/issues) list for the work in progress
- `v0.7` better anti-refactoring support of mocking, allow quick mock all method in specified class
- `v1.0` all functions are stable, a brand-new start
`TestableMock` is still under heavy development, if you get sucked somehow, just go ahead raise an [issue](https://github.com/alibaba/testable-mock/issues) for it.
## Directory Structure
@@ -27,7 +23,9 @@ Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
|-- tool ➜ Scripts for project maintain
|-- demo
| |-- java-demo ➜ Java code example
| `-- kotlin-demo ➜ Kotlin code example
| |-- kotlin-demo ➜ Kotlin code example
| |-- android-demo ➜ Android app code example
| `-- spock-demo ➜ Spock framwork code example
`-- docs ➜ Source code of usage document
```

View File

@@ -49,7 +49,7 @@ dependencies {
testImplementation 'androidx.test:runner:1.4.0-alpha05'
testImplementation 'junit:junit:4.+'
testImplementation 'org.robolectric:robolectric:4.5.1'
testImplementation 'com.alibaba.testable:testable-all:0.6.10'
testImplementation 'com.alibaba.testable:testable-all:0.7.4'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

View File

@@ -13,8 +13,8 @@ repositories {
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
testImplementation('com.alibaba.testable:testable-all:0.6.10')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.10')
testImplementation('com.alibaba.testable:testable-all:0.7.4')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.7.4')
}
tasks.withType(JavaCompile) {

View File

@@ -12,7 +12,7 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>5.6.2</junit.version>
<testable.version>0.6.10</testable.version>
<testable.version>0.7.4</testable.version>
</properties>
<dependencies>

View File

@@ -0,0 +1,9 @@
package com.alibaba.demo.basic.issue;
public abstract class AbstractServiceA {
public String put(Object input) {
return "object";
}
}

View File

@@ -0,0 +1,9 @@
package com.alibaba.demo.basic.issue;
public class ServiceA extends AbstractServiceA {
public String get(Object input) {
return super.put(input);
}
}

View File

@@ -0,0 +1,42 @@
package com.alibaba.demo.lambda;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* @author jim
*/
public interface BaseDemo {
default void blackHole(Object... ignore) {}
default void consumesRun(Runnable r) {
r.run();
}
default void consumesFunction0(Runnable f) {
f.run();
}
default <T> void consumesFunction1(Consumer<T> f) {
f.accept(null);
}
default <T, R> void consumesFunction2(Function<T, R> f) {
f.apply(null);
}
default <T1, T2, R> void consumesFunction3(BiFunction<T1, T2, R> f) {
f.apply(null, null);
}
default <T> void consumesSupplier(Supplier<T> supplier) {
supplier.get();
}
default <T, R> void consumesTwoFunction2(Function<T, R> f1, Function<T, R> f2) {
f1.apply(null);
f2.apply(null);
}
}

View File

@@ -0,0 +1,121 @@
package com.alibaba.demo.lambda;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author jim
*/
@SuppressWarnings("SimplifyStreamApiCallChains")
public class CollectionListCodeDemo implements BaseDemo {
public void list() {
List<User> userList = getUsers();
double d = userList.stream()
.map(v -> v)
.map(v -> Function.<User>identity().apply(v))
.map(User::getId)
.mapToDouble(Double::valueOf)
.reduce(0, Double::sum);
//.reduce(0, (v1, v2) -> v1 + v2);
blackHole(d);
blackHole(userList.stream()
.map(User::getId)
.mapToLong(Long::valueOf)
.reduce(0, Long::sum));
blackHole(userList.stream()
.map(User::getId)
.mapToInt(Long::intValue)
.reduce(0, Integer::sum));
Map<Long, User> map = userList.stream()
.filter(User::isActive)
.collect(Collectors.toMap(User::getId, Function.identity(), (v1, v2) -> v1));
blackHole(map);
List<User> l1 = userList.stream()
.collect(Collectors.toList())
.stream()
.collect(Collectors.toSet())
.stream()
.collect(Collectors.groupingBy(User::getName))
.entrySet()
.stream()
.filter(v -> v.getValue().stream().anyMatch(User::isActive))
.map(Map.Entry::getValue)
.flatMap(Collection::parallelStream)
.collect(Collectors.groupingBy(User::getId))
.values()
.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
blackHole(l1);
userList.stream()
.map(User::isActive)
.reduce(Boolean::logicalAnd)
.ifPresent(this::blackHole);
}
private List<User> getUsers() {
List<User> userList = new ArrayList<>();
for (int i=0; i<10; i++) {
userList.add(getUser(i));
}
return userList;
}
private User getUser(int index) {
User u1 = new User();
u1.id = (long)index;
u1.name = "u" + index;
u1.age = index;
u1.active = true;
return u1;
}
private static class User {
private Long id;
private String name;
private int age;
private boolean active;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
}

View File

@@ -0,0 +1,86 @@
package com.alibaba.demo.lambda;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* @author jim
*/
public class InvokeInterfaceDemo implements BaseDemo {
public void collectionInterfaceDefaultOrStatic() {
blackHole(collectionStreamTest());
blackHole(arraysStreamTest());
}
public void interfaceDefault() {
ILambda l = new LambdaFoo();
consumesRun(l::run);
consumesFunction1(l::function1);
}
public void interfaceStatic() {
consumesRun(ILambda::staticRun);
consumesFunction1(ILambda::staticFunction1);
}
public Object collectionStreamTest() {
List<List<String>> testList = new ArrayList<>();
List<String> fooList = new ArrayList<>();
fooList.add("123");
fooList.add("456");
testList.add(fooList);
return testList.stream()
//.flatMap(v -> v.stream())
.flatMap(Collection::stream)
.map(Double::valueOf)
.map(BigDecimal::new)
.reduce(BigDecimal::add);
}
public Object arraysStreamTest() {
List<String[]> zz = new ArrayList<>();
zz.add(new String[]{"1", "2", "3"});
return zz.stream()
.flatMap(Arrays::stream)
.map(Double::valueOf)
.map(BigDecimal::new)
.reduce(BigDecimal::add);
}
public void objectStaticMethodReference() {
List<Boolean> f = new ArrayList<>();
f.add(false);
f.add(false);
f.add(false);
blackHole(f.stream()
.reduce(Boolean::logicalAnd)
.get()
);
}
public interface ILambda {
default void run() {
}
default void function1(String s) {
}
static void staticRun() {
}
static void staticFunction1(String s) {
}
}
public static class LambdaFoo implements ILambda {
}
}

View File

@@ -9,7 +9,7 @@ import java.util.function.Supplier;
* @author jimca
*/
@SuppressWarnings({"WrapperTypeMayBePrimitive", "ResultOfMethodCallIgnored", "MismatchedReadAndWriteOfArray", "unused"})
public class ExternalLambdaDemo {
public class InvokeVirtualDemo {
public void string1() {
String s = "";
@@ -197,8 +197,8 @@ public class ExternalLambdaDemo {
}
public void function3() {
ExternalLambdaDemo externalLambdaDemo = new ExternalLambdaDemo();
consumesFunction3(externalLambdaDemo::f3);
InvokeVirtualDemo invokeVirtualDemo = new InvokeVirtualDemo();
consumesFunction3(invokeVirtualDemo::f3);
}
public Boolean f3(String s1, Long l) {

View File

@@ -0,0 +1,40 @@
package com.alibaba.demo.lambda;
import java.math.BigDecimal;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author jim
*/
public class StaticInstanceReference implements BaseDemo {
private static final StaticClassA STATIC_CLASS_A = new StaticClassA();
public void staticMethodReference() {
consumesRun(STATIC_CLASS_A::doIt);
consumesFunction1(STATIC_CLASS_A::function1);
consumesFunction2(STATIC_CLASS_A::function2);
consumesFunction3(STATIC_CLASS_A::function3);
}
public static class StaticClassA {
public void doIt() {
}
public void function1(String s) {
}
public Integer function2(String s) {
return 1;
}
public Integer function3(String s, Double d) {
return 1;
}
}
}

View File

@@ -0,0 +1,23 @@
package com.alibaba.demo.basic.issue;
import com.alibaba.testable.core.annotation.MockInvoke;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ServiceATest {
private ServiceA sa = new ServiceA();
public static class Mock {
@MockInvoke(targetClass = AbstractServiceA.class, targetMethod = "put")
public String put(Object input) {
return "mocked";
}
}
@Test
void get() {
assertEquals("mocked", sa.get(123));
}
}

View File

@@ -0,0 +1,28 @@
package com.alibaba.demo.lambda;
import com.alibaba.testable.core.annotation.MockDiagnose;
import com.alibaba.testable.core.annotation.MockInvoke;
import com.alibaba.testable.core.model.LogLevel;
import org.junit.jupiter.api.Test;
/**
* @author jim
*/
public class CollectionListCodeDemoTest {
private final CollectionListCodeDemo instance = new CollectionListCodeDemo();
//@MockDiagnose(LogLevel.VERBOSE)
public static class Mock {
@MockInvoke(targetClass = String.class, targetMethod = "contains")
public boolean mockContains(CharSequence s) {
return false;
}
}
@Test
public void listTest() {
instance.list();
}
}

View File

@@ -0,0 +1,72 @@
package com.alibaba.demo.lambda;
import com.alibaba.testable.core.annotation.MockDiagnose;
import com.alibaba.testable.core.annotation.MockInvoke;
import com.alibaba.testable.core.model.LogLevel;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
import static com.alibaba.testable.core.matcher.InvocationVerifier.verifyInvoked;
/**
* @author jim
*/
public class InvokeInterfaceDemoTest {
private final InvokeInterfaceDemo instance = new InvokeInterfaceDemo();
//@MockDiagnose(LogLevel.VERBOSE)
public static class Mock {
@MockInvoke(targetClass = InvokeInterfaceDemo.ILambda.class, targetMethod = "run")
private void mockILambdaRun() {
}
@MockInvoke(targetClass = InvokeInterfaceDemo.ILambda.class, targetMethod = "function1")
private void mockILambdaFunction1(String s) {
}
@SuppressWarnings("unchecked")
@MockInvoke(targetClass = Collection.class, targetMethod = "stream")
<E> Stream<E> mockStream() {
List<E> fooList = new ArrayList<>();
fooList.add((E) "123");
fooList.add((E) "456");
return fooList.stream();
}
@MockInvoke(targetClass = Boolean.class, targetMethod = "logicalAnd")
public static boolean mockLogicalAnd(boolean a, boolean b) {
return false;
}
}
@Test
public void shouldMockCollectionStream() {
instance.collectionInterfaceDefaultOrStatic();
verifyInvoked("mockStream").withTimes(1);
}
@Test
public void shouldMockInterfaceDefault() {
instance.interfaceDefault();
verifyInvoked("mockILambdaRun").withTimes(1);
verifyInvoked("mockILambdaFunction1").withTimes(1);
}
@Test
public void shouldMockObjectStaticMethodReference() {
instance.objectStaticMethodReference();
verifyInvoked("mockLogicalAnd").withTimes(2);
}
@Test
public void shouldMockInterfaceStatic() {
instance.interfaceStatic();
}
}

View File

@@ -8,8 +8,8 @@ import static com.alibaba.testable.core.matcher.InvocationVerifier.verifyInvoked
/**
* @author zcbbpo
*/
public class ExternalLambdaDemoTest {
private final ExternalLambdaDemo lambdaDemo = new ExternalLambdaDemo();
public class InvokeVirtualDemoTest {
private final InvokeVirtualDemo lambdaDemo = new InvokeVirtualDemo();
@SuppressWarnings("unused")
public static class Mock {
@@ -34,7 +34,7 @@ public class ExternalLambdaDemoTest {
return "";
}
@MockInvoke(targetClass = ExternalLambdaDemo.class, targetMethod = "f3")
@MockInvoke(targetClass = InvokeVirtualDemo.class, targetMethod = "f3")
public Boolean mockF3(String s1, Long l) {
return true;
}

View File

@@ -0,0 +1,30 @@
package com.alibaba.demo.lambda;
import com.alibaba.testable.core.annotation.MockDiagnose;
import com.alibaba.testable.core.annotation.MockInvoke;
import com.alibaba.testable.core.model.LogLevel;
import org.junit.jupiter.api.Test;
import static com.alibaba.testable.core.matcher.InvocationVerifier.verifyInvoked;
/**
* @author jim
*/
public class StaticInstanceReferenceTest {
private final StaticInstanceReference instance = new StaticInstanceReference();
//@MockDiagnose(LogLevel.VERBOSE)
public static class Mock {
@MockInvoke(targetClass = StaticInstanceReference.StaticClassA.class, targetMethod = "doIt")
private void mockDoIt() {
}
}
@Test
public void shouldMockDoIt() {
instance.staticMethodReference();
verifyInvoked("mockDoIt").withTimes(1);
}
}

View File

@@ -17,8 +17,8 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
testImplementation("com.alibaba.testable:testable-all:0.6.10")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.6.10")
testImplementation("com.alibaba.testable:testable-all:0.7.4")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.7.4")
}
tasks.withType<KotlinCompile> {

View File

@@ -14,7 +14,7 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>5.6.2</junit.version>
<testable.version>0.6.10</testable.version>
<testable.version>0.7.4</testable.version>
</properties>
<dependencies>

View File

@@ -14,8 +14,8 @@ repositories {
dependencies {
testImplementation 'org.codehaus.groovy:groovy-all:3.0.7'
testImplementation 'org.spockframework:spock-core:2.0-M5-groovy-3.0'
testImplementation('com.alibaba.testable:testable-all:0.6.10')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.10')
testImplementation('com.alibaba.testable:testable-all:0.7.4')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.7.4')
}
tasks.withType(JavaCompile) {

View File

@@ -12,7 +12,7 @@
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<testable.version>0.6.10</testable.version>
<testable.version>0.7.4</testable.version>
</properties>
<dependencyManagement>

View File

@@ -1,5 +1,17 @@
# Release Note
## 0.7.4
- fix an `OmniConstructor.newInstance()` fail issue caused by `java.net.URL` class cannot be instrumented
## 0.7.3
- fix an issue cause mock failed with referring to method defined in interface
## 0.7.2
- fix an issue cause mock failed with static method reference
## 0.7.1
- fix an issue cause mock class unrecognized after package-mapping
## 0.7.0
- fix an improper exception issue when mock method with `associated` scope invoked
- rename type `InvokeVerifier`/`InvockeMatcher` to `InvocationVerifier`/`InvocationMatcher`

View File

@@ -16,7 +16,7 @@ It is recommended to add a `property` field that identifies the TestableMock ver
```xml
<properties>
<testable.version>0.6.10</testable.version>
<testable.version>0.7.4</testable.version>
</properties>
```
@@ -63,8 +63,8 @@ Add dependence of `TestableMock` in `build.gradle` file:
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.6.10')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.10')
testImplementation('com.alibaba.testable:testable-all:0.7.4')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.7.4')
}
```

View File

@@ -1,6 +1,15 @@
Upgrade Guide
---
#### Upgrade to 0.7
In version `0.7` mock related annotations and methods are renamed, in order to avoid naming conflict and misunderstanding.
1. The `verify()` used for check mock method invocation status has been renamed to `verifyInvoked()`, as there are several static methods also named `verify()` in `com.sun` package.
2. The annotation `@MockMethod` and `@MockConstructor` used for mark mock replacement method has been renamed to `@MockInvoke` and `@MockNew`, which is more suitable for its real usage.
The usage of those annotations and method has no change for below and above version `0.7.0`, so during the upgrading only rename is required.
### Upgrade to v0.6
In version `0.6``TestableMock` provided a new [quick complicated class construction](en-us/doc/omni-constructor.md) capability. Meanwhile, it includes a modification that is incompatible with the `0.5` version: class `PrivateAccessor` have been moved from `com.alibaba.testable.core.accessor` package to `com.alibaba.testable.core.tool` package.

View File

@@ -0,0 +1,2 @@
Use IntelliJ Plugin
---

View File

@@ -11,6 +11,7 @@
- [Reusing Mock Methods](en-us/doc/mock-method-reusing.md)
- [Use MockWith Annotation](en-us/doc/use-mock-with.md)
- [Use Package Mapping](en-us/doc/use-package-mapping.md)
- [Use IntelliJ Plugin](en-us/doc/use-intellij-plugin.md)
- [Frequently Asked Questions](en-us/doc/frequently-asked-questions.md)
- [Mock Invocation In Thread Pool](zh-cn/doc/with-thread-pool.md)
- [Use TestableMock In IDE](en-us/doc/use-in-ide.md)

View File

@@ -1,5 +1,17 @@
# Release Note
## 0.7.4
- 修复`java.net.URL`类无法被instrument导致使用`OmniConstructor`构造报错的问题issue-261
## 0.7.3
- 修复接口方法引用导致Mock报错的问题issue-254
## 0.7.2
- 修复静态方法引用导致Mock报错的问题issue-250
## 0.7.1
- 修复包路径映射时后识别不到Mock类的问题issue-248
## 0.7.0
- 修复当`scope``associated`的Mock方法被`null`对象调用且上下文与测试用例未关联时抛错不合理的问题issue-163
- 类型`InvokeVerifier``InvockeMatcher`更名为`InvocationVerifier``InvocationMatcher`

View File

@@ -16,7 +16,7 @@
```xml
<properties>
<testable.version>0.6.10</testable.version>
<testable.version>0.7.4</testable.version>
</properties>
```
@@ -63,8 +63,8 @@
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.6.10')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.10')
testImplementation('com.alibaba.testable:testable-all:0.7.4')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.7.4')
}
```
@@ -110,4 +110,6 @@ test {
> sourceCompatibility = '6' // 或7/8/9/...
> ```
>
> **注意**:由于`Spock`采用基于字符串解析的特殊执行方式,`TestableTool`工具类在`Spock`框架中无法使用。
>
> 完整代码可参考`demo/spock-demo`示例项目。

View File

@@ -1,6 +1,15 @@
版本升级说明
---
#### 升级到0.7.x版本
`0.7`版本重命名了Mock相关的注解和方法以避免命名冲突和由于名字带来的理解歧义包括两部分变化。
1. 用于验证Mock方法调用的`verify()`方法与包括`com.sun`包在内的多个JVM内置静态方法重名在IDE自动添加`import`时会有误选的情况,该方法在新版本中更名为`verifyInvoked()`,同时所属的类型更名为`InvocationVerifier`
2. 注解`@MockMethod``@MockConstructor`的实际作用是对方法调用和New操作进行Mock而其名称容易使人误解为是对方法和构造器本身进行了Mock在新版本中这两个注解分别已被分别更名为`@MockInvoke``@MockNew`
这些注解和方法的使用与之前版本并无差异,从`0.6.x`版本升级到`0.7.0`及以上版本时,请全局替换相关名称即可。
### 升级到0.6.x版本
`0.6`版本中,`TestableMock`提供了[快速构造复杂参数对象](zh-cn/doc/omni-constructor.md)的能力,同时包含一处与`0.5`版本不兼容的修改,`PrivateAccessor`类型的包路径从`com.alibaba.testable.core.accessor`移到了`com.alibaba.testable.core.tool`

View File

@@ -0,0 +1,2 @@
使用IntelliJ插件
---

View File

@@ -11,6 +11,7 @@
- [复用Mock类与方法](zh-cn/doc/mock-method-reusing.md)
- [使用MockWith注解](zh-cn/doc/use-mock-with.md)
- [使用包路径映射](zh-cn/doc/use-package-mapping.md)
- [使用IntelliJ插件](zh-cn/doc/use-intellij-plugin.md)
- [常见使用问题](zh-cn/doc/frequently-asked-questions.md)
- [Mock线程池内的调用](zh-cn/doc/with-thread-pool.md)
- [在IDE运行单元测试](zh-cn/doc/use-in-ide.md)

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.6.10</version>
<version>0.7.4</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-agent</artifactId>

View File

@@ -1,20 +1,16 @@
package com.alibaba.testable.agent.handler;
import com.alibaba.testable.agent.model.BasicType;
import com.alibaba.testable.agent.model.BsmArg;
import com.alibaba.testable.agent.model.MethodInfo;
import com.alibaba.testable.agent.model.TravelStatus;
import com.alibaba.testable.agent.util.BytecodeUtil;
import com.alibaba.testable.agent.util.ClassUtil;
import com.alibaba.testable.agent.util.MethodUtil;
import com.alibaba.testable.core.util.LogUtil;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -64,12 +60,12 @@ public class SourceClassHandler extends BaseClassHandler {
resolveMethodReference(cn);
for (MethodNode m : cn.methods) {
transformMethod(m, memberInjectMethods, newOperatorInjectMethods, cn);
transformMethod(m, memberInjectMethods, newOperatorInjectMethods);
}
}
private void transformMethod(MethodNode mn, Set<MethodInfo> memberInjectMethods,
Set<MethodInfo> newOperatorInjectMethods, ClassNode cn) {
Set<MethodInfo> newOperatorInjectMethods) {
LogUtil.verbose(" Found method %s", mn.name);
if (mn.name.startsWith("$")) {
// skip methods e.g. "$jacocoInit"
@@ -341,85 +337,46 @@ public class SourceClassHandler extends BaseClassHandler {
return Opcodes.INVOKEVIRTUAL == opcode && ClassUtil.isCompanionClassName(ownerClass);
}
private void setFinalValue(Field ownerField, Object obj, Object value) throws Exception {
ownerField.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(ownerField, ownerField.getModifiers() & ~Modifier.FINAL);
ownerField.set(obj, value);
}
private List<Handle> fetchInvokeDynamicHandle(MethodNode mn) {
List<Handle> handleList = new ArrayList<Handle>();
private List<BsmArg> fetchInvokeDynamicHandle(MethodNode mn) {
List<BsmArg> handleList = new ArrayList<BsmArg>();
for (AbstractInsnNode instruction : mn.instructions) {
if (instruction.getOpcode() == Opcodes.INVOKEDYNAMIC) {
InvokeDynamicInsnNode invokeDynamicInsnNode = (InvokeDynamicInsnNode) instruction;
handleList.add((Handle) invokeDynamicInsnNode.bsmArgs[1]);
//handleList.add((Handle) invokeDynamicInsnNode.bsmArgs[1]);
BsmArg bsmArg = new BsmArg(invokeDynamicInsnNode.bsmArgs);
handleList.add(bsmArg);
}
}
return handleList;
}
private void resolveMethodReference(ClassNode cn) {
List<Handle> invokeDynamicList = new ArrayList<Handle>();
List<BsmArg> invokeDynamicList = new ArrayList<BsmArg>();
for (MethodNode method : cn.methods) {
List<Handle> handleList = fetchInvokeDynamicHandle(method);
List<BsmArg> handleList = fetchInvokeDynamicHandle(method);
invokeDynamicList.addAll(handleList);
}
// process for method reference
for (Handle handle : invokeDynamicList) {
for (BsmArg bsmArg : invokeDynamicList) {
// the jdk auto generation method
if (handle.getName().startsWith("lambda$")) {
if (bsmArg.getHandle().getName().startsWith("lambda$")) {
continue;
}
int tag = handle.getTag();
int tag = bsmArg.getHandle().getTag();
if (tag == Opcodes.H_NEWINVOKESPECIAL) {
// lambda new method reference
continue;
}
// external mean:
// public void foo() {
// String s = "";
// consumes(s::contains);
//}
boolean external = tag == Opcodes.H_INVOKEVIRTUAL;
boolean isStatic = bsmArg.isStatic();
Handle handle = bsmArg.getHandle();
Type handleDesc = bsmArg.getHandleDesc();
boolean isStatic = tag == Opcodes.H_INVOKESTATIC || external;
String desc = handle.getDesc();
String parameters = desc.substring(desc.indexOf("(") + 1, desc.lastIndexOf(")"));
String returnType = desc.substring(desc.indexOf(")") + 1);
String[] parameterArray = parameters.split(";");
int len = parameterArray.length;
for (String s : parameterArray) {
if (s.isEmpty()) {
len--;
}
}
len = external ? len + 1 : len;
String[] refineParameterArray = new String[len];
int index = external ? 1 : 0;
if (external) {
// The type should was reference type
refineParameterArray[0] = "L" + handle.getOwner();
}
for (String s : parameterArray) {
if (!s.isEmpty()) {
refineParameterArray[index] = s;
index++;
}
}
String externalDesc = buildDesc(refineParameterArray, returnType);
String lambdaName = String.format("Lambda$_%s_%d", handle.getName(), atomicInteger.incrementAndGet());
MethodVisitor mv = cn.visitMethod(isStatic ? ACC_PUBLIC + ACC_STATIC : ACC_PUBLIC, lambdaName, external ? externalDesc : desc, null, null);
String lambdaName = String.format("_Lambda$_%s_%d", handle.getName(), atomicInteger.incrementAndGet());
MethodVisitor mv = cn.visitMethod(isStatic ? ACC_PUBLIC + ACC_STATIC : ACC_PUBLIC, lambdaName, handleDesc.getDescriptor(), null, null);
mv.visitCode();
Label l0 = new Label();
@@ -428,101 +385,52 @@ public class SourceClassHandler extends BaseClassHandler {
// add this
mv.visitVarInsn(ALOAD, 0);
}
for (int i = 0; i < refineParameterArray.length; i++) {
String arg = refineParameterArray[i];
mv.visitVarInsn(getLoadType(arg), isStatic ? i : i + 1);
Type[] argumentTypes = handleDesc.getArgumentTypes();
int nextVar = isStatic ? 0 : 1;
for (Type argumentType : argumentTypes) {
String arg = argumentType.getDescriptor();
mv.visitVarInsn(getLoadType(arg), nextVar);
nextVar = isLongByte(argumentType) ? nextVar + 2 : nextVar + 1;
}
mv.visitMethodInsn(isStatic ? INVOKESTATIC : INVOKEVIRTUAL, handle.getOwner(), handle.getName(), desc, false);
if (tag == H_INVOKEINTERFACE) {
mv.visitMethodInsn(INVOKEINTERFACE, handle.getOwner(), handle.getName(), bsmArg.getOriginalHandleDesc(), handle.isInterface());
} else {
mv.visitMethodInsn(Opcodes.H_INVOKESTATIC == tag ? INVOKESTATIC : INVOKEVIRTUAL, handle.getOwner(), handle.getName(), bsmArg.getOriginalHandleDesc(), handle.isInterface());
}
mv.visitInsn(getReturnType(returnType));
mv.visitInsn(getReturnType(handleDesc.getReturnType().getDescriptor()));
Label l1 = new Label();
mv.visitLabel(l1);
// static function was not required add this to first parameter
if (isStatic) {
for (int i = 0; i < refineParameterArray.length; i++) {
String localVar = refineParameterArray[i];
if (!isPrimitive(localVar)) {
// primitive type and reference type difference
localVar = localVar.endsWith(";") ? localVar : localVar + ";";
}
if (localVar.isEmpty()) {
continue;
}
// add local var
mv.visitLocalVariable(String.format("o%d", i), localVar, null, l0, l1, i);
}
visitLocalVariableByArguments(mv, 0, argumentTypes, l0, l1);
} else {
mv.visitLocalVariable("this", "L" + handle.getOwner() + ";", null, l0, l1, 0);
for (int i = 0; i < refineParameterArray.length; i++) {
String localVar = refineParameterArray[i];
if (!isPrimitive(localVar) && !isPrimitiveArray(localVar)) {
localVar = localVar.endsWith(";") ? localVar : localVar + ";";
}
if (localVar.isEmpty()) {
continue;
}
mv.visitLocalVariable(String.format("o%d", i), localVar, null, l0, l1, i + 1);
}
visitLocalVariableByArguments(mv, 1, argumentTypes, l0, l1);
}
// auto compute max
mv.visitMaxs(-1, -1);
mv.visitEnd();
try {
// modify handle to the generation method
setFinalValue(handle.getClass().getDeclaredField("name"), handle, lambdaName);
// mark: should merge the below two if.
if (!handle.getOwner().equals(cn.name) && isStatic) {
setFinalValue(handle.getClass().getDeclaredField("owner"), handle, cn.name);
}
if (external) {
setFinalValue(handle.getClass().getDeclaredField("owner"), handle, cn.name);
setFinalValue(handle.getClass().getDeclaredField("descriptor"), handle, externalDesc);
setFinalValue(handle.getClass().getDeclaredField("tag"), handle, H_INVOKESTATIC);
}
} catch (Exception ignore) {
}
bsmArg.complete(cn.name, lambdaName);
}
}
private String buildDesc(String[] refineParameterArray, String returnType) {
StringBuilder sb = new StringBuilder();
sb.append("(");
for (String s : refineParameterArray) {
sb.append(s);
if (!isPrimitive(s)) {
sb.append(";");
}
private void visitLocalVariableByArguments(MethodVisitor mv, final int initVar, Type[] argumentTypes, Label l0, Label l1) {
int nextLocalVar = initVar;
for (int i = 0; i < argumentTypes.length; i++) {
Type argumentType = argumentTypes[i];
String localVar = argumentType.getDescriptor();
mv.visitLocalVariable(String.format("o%d", i), localVar, null, l0, l1, nextLocalVar);
nextLocalVar = isLongByte(argumentType) ? nextLocalVar + 2 : nextLocalVar + 1;
}
sb.append(")");
sb.append(returnType);
return sb.toString();
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean isPrimitive(String type) {
if (type.endsWith(";")) {
type = type.substring(0, type.length() - 1);
}
return BasicType.basicType(type.charAt(0)).isPrimitive();
}
private boolean isPrimitiveArray(String type) {
if (!type.startsWith("[")) {
return false;
}
if (type.endsWith(";")) {
type = type.substring(0, type.length() - 1);
}
type = type.replace("[", "");
return BasicType.basicType(type.charAt(0)).isPrimitive();
private boolean isLongByte(Type argumentType) {
return double.class.getName().equals(argumentType.getClassName()) || long.class.getName().equals(argumentType.getClassName());
}
private int getReturnType(String returnType) {

View File

@@ -0,0 +1,84 @@
package com.alibaba.testable.agent.model;
import com.alibaba.testable.agent.util.StringUtil;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
/**
* @author jim
*/
public class BsmArg {
private Type handleDesc;
private Type methodDesc;
private Handle handle;
private final Object[] bsmArgs;
private final String originalHandleDesc;
public BsmArg(Object[] bsmArgs) {
this.bsmArgs = bsmArgs;
handle = (Handle) bsmArgs[1];
methodDesc = (Type) bsmArgs[2];
handleDesc = Type.getType(handle.getDesc());
originalHandleDesc = handle.getDesc();
// H_INVOKEVIRTUAL: String s = "";consumes(s::contains);
// H_INVOKEINTERFACE: list.stream().flatMap(Collection::stream)
if (handle.getTag() == Opcodes.H_INVOKEVIRTUAL || handle.getTag() == Opcodes.H_INVOKEINTERFACE) {
Type[] argumentTypes = handleDesc.getArgumentTypes();
Type thisArgument = Type.getType("L" + handle.getOwner() + ";");
String handleDescString = refineHandle(thisArgument, argumentTypes);
handle = new Handle(handle.getTag(), handle.getOwner(), handle.getName(), handleDescString, handle.isInterface());
handleDesc = Type.getType(handleDescString);
if (/*handle.getTag() == Opcodes.H_INVOKEVIRTUAL || */methodDesc.getArgumentTypes().length == handleDesc.getArgumentTypes().length - 1) {
Type[] methodArguments = methodDesc.getArgumentTypes();
String methodDescString = refineHandle(thisArgument, methodArguments);
methodDesc = Type.getType(methodDescString);
}
}
}
private String refineHandle(Type thisArgument, Type[] argumentTypes) {
Type[] handleArguments = new Type[argumentTypes.length + 1];
String[] handleArgs = new String[argumentTypes.length + 1];
handleArguments[0] = thisArgument;
System.arraycopy(argumentTypes, 0, handleArguments, 1, argumentTypes.length);
for (int i = 0; i < handleArguments.length; i++) {
handleArgs[i] = handleArguments[i].getDescriptor();
}
return "(" + StringUtil.join("", handleArgs) + ")" + handleDesc.getReturnType().getDescriptor();
}
public Type getHandleDesc() {
return handleDesc;
}
public Type getMethodDesc() {
return methodDesc;
}
public Handle getHandle() {
return handle;
}
public String getOriginalHandleDesc() {
return originalHandleDesc;
}
public boolean isStatic() {
int tag = handle.getTag();
return tag == Opcodes.H_INVOKESTATIC || tag == Opcodes.H_INVOKEVIRTUAL || tag == Opcodes.H_INVOKEINTERFACE;
}
public void complete(String owner, String methodName) {
bsmArgs[1] = new Handle(isStatic()? Opcodes.H_INVOKESTATIC : Opcodes.H_INVOKEVIRTUAL, owner, methodName, handleDesc.getDescriptor(), false);
}
}

View File

@@ -63,15 +63,14 @@ public class TestableClassTransformer implements ClassFileTransformer {
}
private byte[] transformMock(byte[] bytes, ClassNode cn) {
String className = (GlobalConfig.getMockPackageMapping() == null) ? cn.name : mapPackage(cn.name);
try {
if (mockClassParser.isMockClass(cn)) {
// it's a mock class
bytes = new MockClassHandler(className).getBytes(bytes);
bytes = new MockClassHandler(cn.name).getBytes(bytes);
BytecodeUtil.dumpByte(cn, GlobalConfig.getDumpPath(), bytes);
return bytes;
}
String mockClass = foundMockForSourceClass(className);
String mockClass = foundMockForSourceClass(cn.name);
if (mockClass != null) {
// it's a source class with testable enabled
List<MethodInfo> injectMethods = mockClassParser.getTestableMockMethods(mockClass);
@@ -92,7 +91,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
LogUtil.error("Invalid mock method %s::%s - %s", e.getClassName(), e.getMethodName(), e.getMessage());
System.exit(0);
} catch (Throwable t) {
LogUtil.warn("Failed to transform class " + className);
LogUtil.warn("Failed to transform class " + cn.name);
LogUtil.warn(t.toString());
LogUtil.warn(ThreadUtil.getFirstRelatedStackLine(t));
} finally {
@@ -102,18 +101,8 @@ public class TestableClassTransformer implements ClassFileTransformer {
return bytes;
}
private String mapPackage(String name) {
String dotSeparatedName = ClassUtil.toDotSeparatedName(name);
for (String prefix : GlobalConfig.getMockPackageMapping().keySet()) {
if (dotSeparatedName.startsWith(prefix)) {
return ClassUtil.toSlashSeparatedName(GlobalConfig.getMockPackageMapping().get(prefix))
+ name.substring(prefix.length());
}
}
return name;
}
private String foundMockForSourceClass(String className) {
private String foundMockForSourceClass(String name) {
String className = (GlobalConfig.getMockPackageMapping() == null) ? name : mapPackage(name);
String mockClass = lookForMockWithAnnotationAsSourceClass(className);
if (mockClass != null) {
return mockClass;
@@ -125,6 +114,17 @@ public class TestableClassTransformer implements ClassFileTransformer {
return foundMockForInnerSourceClass(className);
}
private String mapPackage(String name) {
String dotSeparatedName = ClassUtil.toDotSeparatedName(name);
for (String prefix : GlobalConfig.getMockPackageMapping().keySet()) {
if (dotSeparatedName.startsWith(prefix)) {
return ClassUtil.toSlashSeparatedName(GlobalConfig.getMockPackageMapping().get(prefix))
+ name.substring(prefix.length());
}
}
return name;
}
private String foundMockForInnerSourceClass(String className) {
return (className.contains(DOLLAR) && !className.endsWith(KOTLIN_POSTFIX_COMPANION)) ?
foundMockForTestClass(ClassUtil.getTestClassName(className.substring(0, className.indexOf(DOLLAR)))) : null;

View File

@@ -19,4 +19,15 @@ public class StringUtil {
return sb.toString();
}
public static String join(String delimiter, String[] s) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < s.length; i++) {
stringBuilder.append(s[i]);
if (i != s.length - 1) {
stringBuilder.append(delimiter);
}
}
return stringBuilder.toString();
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.6.10</version>
<version>0.7.4</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-all</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.6.10</version>
<version>0.7.4</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-core</artifactId>

View File

@@ -227,6 +227,10 @@ public class OmniConstructor {
private static Object createInstance(Class<?> clazz, Set<Class<?>> classPool)
throws InstantiationException, IllegalAccessException, InvocationTargetException {
Object ins = createSpecialClass(clazz);
if (ins != null) {
return ins;
}
Constructor<?> constructor = getBestConstructor(clazz);
if (constructor == null) {
throw new ClassConstructionException("Fail to invoke constructor of " + clazz.getName());
@@ -244,6 +248,18 @@ public class OmniConstructor {
}
}
private static Object createSpecialClass(Class<?> clazz) throws InstantiationException, IllegalAccessException, InvocationTargetException {
try {
// java.net.URL is loaded before OmniClassHandler, cannot be instrumented
if (clazz.getName().equals("java.net.URL")) {
return clazz.getDeclaredConstructor(String.class).newInstance("https://");
}
} catch (NoSuchMethodException e) {
return null;
}
return null;
}
private static Constructor<?> getBestConstructor(Class<?> clazz) {
Constructor<?> bestConstructor = null;
int minimalExceptionCount = 999;

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.6.10</version>
<version>0.7.4</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-maven-plugin</artifactId>

View File

@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.6.10</version>
<version>0.7.4</version>
<packaging>pom</packaging>
<name>testable-parent</name>
<description>Unit test enhancement toolkit</description>
@@ -42,7 +42,7 @@
<plugin.gpg.version>1.6</plugin.gpg.version>
<plugin.staging.version>1.6.8</plugin.staging.version>
<plugin.maven.version>3.6.0</plugin.maven.version>
<testable.version>0.6.10</testable.version>
<testable.version>0.7.4</testable.version>
</properties>
<profiles>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.6.10</version>
<version>0.7.4</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-processor</artifactId>