add 0.5 zh-cn docs

This commit is contained in:
金戟 2021-02-20 09:03:44 +08:00
parent f660ccb88f
commit df73dac54f
17 changed files with 434 additions and 107 deletions

View File

@ -2,16 +2,16 @@
换种思路写Mock让单元测试更简单。
无需初始化,不挑测试框架甭管要换的是私有方法、静态方法、构造方法还是其他任何类的任何方法也甭管要换的对象是怎么创建的。写好Mock定义加个`@MockMethod`注解,一切统统搞定。
无需初始化,不挑服务框架甭管要换的是私有方法、静态方法、构造方法还是其他任何类的任何方法也甭管要换的对象是怎么创建的。写好Mock定义加个`@MockMethod`注解,一切统统搞定。
- 文档https://alibaba.github.io/testable-mock/
- 国内文档镜像http://freyrlin.gitee.io/testable-mock/
阅读[这里](https://mp.weixin.qq.com/s/KyU6Eu7mDkZU8FspfSqfMw)了解更多故事。
> 特别说明
> 1. 如遇到"Attempt to access non-static member in mock method"错误,参见[常见问题](https://alibaba.github.io/testable-mock/#/zh-cn/doc/frequently-asked-questions)第8条
> 2. 如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码我们将在24小时内回复并处理
<font size="5">**0.5版本已发布**</font>,从`0.4.x`升级到`0.5.x`版本请参考[0.5版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-to-v05)
如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码我们将在24小时内回复并处理
-----
@ -19,9 +19,8 @@
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
- `0.4` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
- `0.5` 实现以"独立Mock类"为单元的Mock方法复用机制让测试类之间可以方便复用相同的Mock方法
- `0.6` 实现将Mock方法的默认生效范围缩小至当前被测类避免Mock方法在测试类之间相互影响
- `0.5` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
- `0.6` 实现第四项单元测试增强能力"[快速入参构造器](https://alibaba.github.io/testable-mock/#/zh-cn/doc/parameter-constructor)"
- `1.0` 功能稳定,一个崭新的开始
## 目录结构

View File

@ -3,4 +3,4 @@ Parameter constructor
No matter how intricate the parameter structure required by the method under test is, even there is no suitable construction method, or even there are private internal class objects... Call `TestableMock`, the parameter object will be handed to you immediately~
This feature is planned to be released in the `0.5.x` version.
This feature is planned to be released in the `0.6` version.

View File

@ -5,7 +5,7 @@ TestableMock简介
在定义Mock方法时开发者真正关心的只有一件事"<u>这个调用在测试的时候要换成那个假的Mock方法</u>"。
然而当下主流的Mock框架在实现Mock功能时需要开发者操心的事情实在太多Mock框架如何初始化、与所用的单元测试框架是否兼容、要被Mock的方法是不是私有的、是不是静态的、被Mock对象是new出来的还是注入的、怎样把被测对象送回被测类里...这些非关键的额外工作极大分散了使用Mock工具应有的乐趣。
然而当下主流的Mock框架在实现Mock功能时需要开发者操心的事情实在太多Mock框架如何初始化、与所用的服务框架是否兼容、要被Mock的方法是不是私有的、是不是静态的、被Mock对象是new出来的还是注入的、怎样把被测对象送回被测类里...这些非关键的额外工作极大分散了使用Mock工具应有的乐趣。
于是,我们开发了`TestableMock`**一款特立独行的轻量Mock工具**。

View File

@ -0,0 +1,61 @@
注解参数清单
---
基于轻量的原则,`TestableMock`为开发者提供了尽可能精炼、易用的注解组合,以下参数信息可供开发参考。
#### @EnablePrivateAccess
启用对被测类的<u>私有成员访问编译期增强</u><u>私有目标存在性的编译期校验</u>功能。
- 作用于:测试类
| 参数 | 类型 | 是否必须 | 默认值 | 作用 |
| --- | --- | --- | ---- | --- |
| srcClass | Class | 否 | N/A | 当测试类命名不符合约定时,指定实际被测类 |
| verifyTargetOnCompile | boolean | 否 | true | 是否启用私有目标的编译期存在性校验 |
#### @MockMethod
将当前方法标识为待匹配的Mock成员方法。
- 作用于Mock容器类中的方法
| 参数 | 类型 | 是否必须 | 默认值 | 作用 |
| --- | --- | --- | ---- | --- |
| targetClass | Class | 否 | N/A | 指定Mock目标的调用者类型 |
| targetMethod | String | 否 | N/A | 指定Mock目标的方法名 |
| scope | MockScope | 否 | MockScope.GLOBAL | 指定Mock的生效范围 |
#### @MockConstructor
将当前方法标识为待匹配的Mock构造方法。
- 作用于Mock容器类中的方法
| 参数 | 类型 | 是否必须 | 默认值 | 作用 |
| --- | --- | --- | ---- | --- |
| scope | MockScope | 否 | MockScope.GLOBAL | 指定Mock的生效范围 |
#### @MockWith
显式指定当前类型关联的Mock容器类。
- 作用于:测试类、被测类
| 参数(`N/A`为默认参数) | 类型 | 是否必须 | 默认值 | 作用 |
| --- | --- | --- | ---- | --- |
| N/A | Class | 是 | N/A | 指定使用的Mock容器类 |
| treatAs | ClassType | 否 | ClassType.GuessByName | 指定当前类是测试类或被测类 |
| diagnose | LogLevel | 否 | N/A | (**deprecated**)指定Mock诊断日志级别 |
#### @MockDiagnose
启用或禁止Mock相关的诊断信息输出。
- 作用于Mock容器类
| 参数(`N/A`为默认参数) | 类型 | 是否必须 | 默认值 | 作用 |
| --- | --- | --- | ---- | --- |
| N/A | LogLevel | 是 | N/A | 指定当前Mock容器关联测试用例的诊断日志级别 |

View File

@ -29,44 +29,30 @@
> 由于JVM存在泛型擦除机制对于Java项目也可以直接使用`Object`类型替代泛型参数见Java版`DemoTemplateTest`测试类中被注释掉的"第二种写法"示例。
#### 5. 在Kotlin项目对`String`类中的方法进行Mock不生效
#### 5. 如何Mock没有测试类的代码
在规范的单元测试中,通常不推荐做跨单元(跨类)的测试用例,即在`A`类型的测试中应当只关注自己类的代码逻辑,若其中调用了`B`类型的某些复杂方法则应该Mock掉让这些逻辑在`B`类型的单元测试里验证。这也是`TestableMock`设计时遵循的一条顶层逻辑。不过在实际的单元测试中,从实用性出发,其实经常出现一次把多个单元的方法放在一个单元测试里串起来测的情况。这样一来,在使用`TestableMock`的时候就可能会遇到“要Mock的代码没有测试类”的情况。
对应的解决方法是,在测试目录的相同包路径下定义一个名称是`被测类+Mock`的类型在其中定义Mock方法或者在**被测类**上使用`@MockWith`注解然后就近定义一个Mock容器类比如直接在被测类里增加一个内部静态类
#### 6. 为何当测试类名不为“被测类+Test”时要在被测类上使用`@MockWith`,而不在测试类上直接指定被测类?
从原则上来说,凡是能只改测试类就实现的,肯定不应该为了测试而去动业务代码(被测类)。
然而由于JavaAgent只能在类首次加载进内存的时候对类进行处理实际情况并不能保证被测的类一定是在测试类之后才加载可能在其他测试用例执行的时候就被提前加载进内存了等读取到测试类上的信息时已经无法对被测类进行Mock处理。因此对于测试类和被测类相互不知道对方位置的情况采用了两边都用`@MockWith`指定Mock容器类的折中设计。
#### 7. 在Kotlin项目对`String`类中的方法进行Mock不生效
Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.String`。但在构建生成自字节码的时候又会被替换为Java的`java.lang.String`类因此无论将Mock目标写为`kotlin.String`或`java.lang.String`均无法正常匹配到原始的被调用方法。
实际场景中需要对`String`类中的方法进行Mock的场景很少`TestableMock`暂未对这种情况做特别处理。
#### 6. 当被Mock的方法被其它测试类**间接调用**时依然有效吗?
同样有效Mock的作用范围是整个测试运行过程。
例如测试类`AaaTest`中Mock了`Aaa`类的某些私有方法(或者某些外部方法调用);在另一个测试类`BbbTest`中测试`Bbb`类时,某些方法间接用到了`Aaa`类被Mock过的方法或调用此时实际调用的同样会是`AaaTest`类中定义的Mock方法。
#### 7. `TestableMock`能否用于Android项目的测试
#### 8. `TestableMock`能否用于Android项目的测试
结合[Roboelectric](https://github.com/robolectric/robolectric)测试框架可使用。
Android系统的`Dalvik`和`ART`虚拟机采用了与标准JVM不同的字节码体系会影响`TestableMock`的正常工作。`Roboelectric`框架能在普通JVM虚拟机上运行Android单元测试其速度比通过Android虚拟机运行单元测试快非常多绝大多数Android App的单元测试都在使用`Roboelectric`框架。
#### 8. 使用Mock时候遇到"Attempt to access non-static member in mock method"错误?
当前`TestableMock`的设计不允许在Mock方法中访问测试类的非`static`成员因为Mock方法自身会在运行期被动态修改为`static`类型。然而有些Java语句包括构造块譬如`new ArrayList<String>() {{ append("data"); }}`)、匿名函数(譬如`list.stream().map(i -> i.get)`等等会在编译过程中生成额外的成员方法调用导致Mock方法执行报错。
最简单的解决办法是将Mock方法本身也声明为`static`类型(这样动态生成的调用也会是`static`的,避免了以上错误),例如原方法定义为:
```java
@MockMethod
private int getXxx(Demo self) {}
```
将其修改为:
```java
@MockMethod
private static int getXxx(Demo self) {}
```
在下一个大迭代版本(**即`0.5`版本**将会在保持当前Mock体验的前提下对Mock的实现机制进行修改不再需要修改Mock方法为静态方法从而彻底解决此类报错问题。
#### 9. 在IntelliJ运行测试报"Command Line is too Long. Shorten command line for ..."错误?
这个问题是由于系统ClassPath包含太多路径所致与是否使用`TestableMock`无关。但需要注意的是IntelliJ提供了两种辅助解决机制`JAR manifest`和`classpath file`,若测试中使用了`TestableMock`,请选择`JAR manifest`。

View File

@ -0,0 +1,45 @@
全局运行参数
---
`TestableMock`的许多功能采用了基于JavaAgent的运行时字节码修改实现。在JavaAgent启动时可以通过额外的全局参数来调整`TestableMock`的执行过程。
### 可用参数清单
| 参数 | 描述 | 可用值和示例(`N/A`表示无需赋值) |
| ---- | ---- | ---- |
| logLevel | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
| dumpPath | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 例如 `/tmp/bytecode`(需要此目录事先存在) |
| pkgPrefix | 限定`TestableMock`仅对部分包生效 | 使用`,`分隔的包路径前缀列表,例如 `com.demo.svc,com.demo.dao` |
| mockScope | 修改默认的Mock生效范围详见[Mock生效范围](zh-cn/doc/scope-of-mock) | 可用值为:`global`(全局生效) / `associated`(只对关联的测试用例生效) |
| useThreadPool | 启用基于`TransmittableThreadLocal`的Mock上下文存储用于包含线程池的测试用例 | `N/A` |
### 参数的连接
若参数有值,参数名和值之间用`=`符合连接。例如:
`useThreadPool`、`logLevel=debug`、`dumpPath=/tmp/debug`
多个参数之间使用`&`符号连接,例如:
`useThreadPool&logLevel=debug`、`logLevel=debug&dumpPath=/tmp/debug`
### 添加运行参数
与其他基于JavaAgent的工具相似`TestableMock`通过在单元测试引入`testable-agent`包的末尾加上一个`=`符号,然后连接额外参数来传递用户的自定义参数。
对于Maven项目可将参数追加到`maven-surefire-plugin`参数`testable-agent`包尾部,紧接着`.jar`的位置。例如:
```xml
<configuration>
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar=mockScope=associated&amp;pkgPrefix=com.demo.</argLine>
</configuration>
```
> 注意:在`xml`文件中,连接参数的`&`符号需要写为`&amp;`
对于Gradle项目同样是直接将参数追加到引入`testable-agent`的配置末尾。例如:
```groovy
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}=mockScope=associated&pkgPrefix=com.demo."
```

View File

@ -0,0 +1,63 @@
复用Mock类与方法
---
Don't Repeat Yourself是软件开发过程当中的一项重要原则即“DRY原则”在编写测试代码时有些通用的基础功能调用语句常常出现在许多相似的业务类里若测试中需要Mock这些调用就要在各个测试类中重复提供同样的Mock方法。而通过Mock方法的复用机制能够很好的避免编写臃肿重复Mock代码的麻烦。
TestableMock支持两种粒度的Mock复用方式<u>复用Mock类</u><u>复用Mock方法</u>
## 复用Mock类
如果有两个或以上测试类需要Mock的方法近乎相同那么采用类级别的Mock复用就是最省心的一种方式。
进行类级别的Mock复用只需将Mock容器定义为独立的类然后在要使用它的测试类上通过`@MockWith`进行引用。例如:
```java
@MockWith(ServiceMock.class)
public class AaaServiceTest {
...
}
@MockWith(ServiceMock.class)
public class BbbServiceTest {
...
}
public class ServiceMock {
...
}
```
这样在`AaaServiceTest`和`BbbServiceTest`类中的测试用例在执行时,都会用`ServiceMock`容器类中定义的Mock方法进行调用匹配和Mock替换。
## 复用Mock方法
实际场景中相比一次性复用整个Mock类的情况更常见的是对部分高频Mock方法进行复用。
Mock方法的复用可以通过Mock容器类的继承来实现父类中定义的所有Mock方法都会在子类中自然存在例如
```java
public class AaaServiceTest {
public static class Mock extends BasicMock {
...
}
...
}
public class BbbServiceTest {
public static class Mock extends BasicMock {
...
}
...
}
public class BasicMock {
@MockMethod(targetClass = UserDao.class)
protected String getById(int id) {
...
}
}
```
则名为`getById`的Mock方法在`AaaServiceTest`和`BbbServiceTest`的测试用例执行时都会生效。

View File

@ -3,4 +3,4 @@
不论被测方法所需的参数结构多么错综复杂、甚至没有合适的构造方法、甚至需要私有内部类对象... 呼唤TestableMock马上递给您~
计划在`0.5.x`版本中推出。
计划在`0.6`版本中推出。

View File

@ -9,17 +9,17 @@
只需为测试类添加`@EnablePrivateAccess`注解,即可在测试用例中获得以下增强能力:
- 调用被测类的私有方法(包括静态方法)
- 读取被测类的私有字段(包括静态字段)
- 修改被测类的私有字段(包括静态字段)
- 修改被测类的常量字段使用final修饰的字段包括静态字段
- 调用**被测类**的私有方法(包括静态方法)
- 读取**被测类**的私有字段(包括静态字段)
- 修改**被测类**的私有字段(包括静态字段)
- 修改**被测类**的常量字段使用final修饰的字段包括静态字段
访问和修改私有、常量成员时IDE可能会提示语法有误但编译器将能够正常运行测试。使用编译期代码增强目前仅实现了Java语言的适配
效果见`java-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
> 此功能默认假设测试类与被测类同包,且名称为`被测类+Test`。当不符合此约定时,可在测试类的`@EnablePrivateAccess`注解上使用`srcClass`参数指定实际的被测类。例如:
>
>
> ```java
> @EnablePrivateAccess(srcClass = DemoServiceImpl.class)
> class DemoServiceTest() { ... }
@ -27,25 +27,24 @@
### 方法二:使用`PrivateAccessor`工具类
若不希望看到IDE的语法错误提醒或是在非Java语言的JVM工程譬如Kotlin语言可以借助`PrivateAccessor`工具类来直接访问私有成员。
若不希望看到IDE的语法错误提醒或是在非Java语言的JVM工程譬如Kotlin语言可以借助`PrivateAccessor`工具类来直接访问私有成员。
这个类提供了6个静态方法:
这个类提供了7个静态方法:
- `PrivateAccessor.get(被测对象, "私有字段名")` ➜ 读取被测类的私有字段
- `PrivateAccessor.set(被测对象, "私有字段名", 新的值)` ➜ 修改被测类的私有字段(或常量字段)
- `PrivateAccessor.invoke(被测对象, "私有方法名", 调用参数..)` ➜ 调用被测类的私有方法
- `PrivateAccessor.getStatic(被测类型, "私有静态字段名")` ➜ 读取被测类的**静态**私有字段
- `PrivateAccessor.setStatic(被测类型, "私有静态字段名", 新的值)` ➜ 修改被测类的**静态**私有字段(或**静态**常量字段)
- `PrivateAccessor.invokeStatic(被测类型, "私有静态方法名", 调用参数..)` ➜ 调用被测类的**静态**私有方法
- `PrivateAccessor.get(任意对象, "私有字段名")` ➜ 读取任意类的私有字段
- `PrivateAccessor.set(任意对象, "私有字段名", 新的值)` ➜ 修改任意类的私有字段(或常量字段)
- `PrivateAccessor.invoke(任意对象, "私有方法名", 调用参数...)` ➜ 调用任意类的私有方法
- `PrivateAccessor.getStatic(任意类型, "私有静态字段名")` ➜ 读取任意类的**静态**私有字段
- `PrivateAccessor.setStatic(任意类型, "私有静态字段名", 新的值)` ➜ 修改任意类的**静态**私有字段(或**静态**常量字段)
- `PrivateAccessor.invokeStatic(任意类型, "私有静态方法名", 调用参数...)` ➜ 调用任意类的**静态**私有方法
- `PrivateAccessor.construct(任意类型, 构造方法参数...)` ➜ 调用任意类的私有构造方法
使用`PrivateAccessor`工具类并不需要测试类具有`@EnablePrivateAccess`注解,但加上此注解将开启被测类私有成员的编译期校验功能,通常建议搭配使用
> 使用`PrivateAccessor`工具类并不需要测试类具有`@EnablePrivateAccess`注解,但加上此注解将开启被测类私有成员的编译期校验功能。
详见`java-demo`和`kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。
### 私有成员编译期校验
以上两种方式本质上都是利用JVM的反射机制实现了私有成员访问JVM编译器不会检查反射目标的存在性。当代码重构时如果对源类型中的私有方法名称、参数进行了修改可能导致在单元测试运行时出现较不直观的异常错误。为此`TestableMock`对访问的私有目标提供了额外的编译期校验。
上述两种私有成员访问功能的本质都是利用了JVM的反射机制但JVM编译器不会检查反射目标的存在性当代码重构时如果对源类型中的私有方法名称、参数进行了修改就会导致错误要在单元测试运行时才能被发现。为此`@EnablePrivateAccess`注解的另一项功能是为**被测类**的私有成员访问进行额外的编译期校验。
编译期校验功能通过`@EnablePrivateAccess`注解开启,对使用`方法一`访问的私有成员的情况默认生效,而对通过`方法二`访问私有成员的情况默认关闭(若要启用,给测试类加上`@EnablePrivateAccess`注解即可)。
> `@EnablePrivateAccess`注解的编译期校验功能可以手工关闭,只需将注释的`verifyTargetOnCompile`参数设为`false`。
**注意**:当私有成员校验功能开启时,`PrivateAccessor`工具类将只能用于访问**被测类**的私有成员,这会有助于限制将`PrivateAccessor`工具类用于与当前测试无关的“越权”操作。如果确实需要访问其他类的私有成员,可将`@EnablePrivateAccess`注解移除,或将注解的`verifyTargetOnCompile`参数设为`false`,手工关闭校验功能。

View File

@ -1,5 +1,12 @@
# Release Note
## 0.5.0
- 分离测试类与Mock类实现Mock类和Mock方法的复用机制
- 支持测试类与被测类在不同包路径的情况下实施Mock
- 支持将Mock方法生效范围缩小为所属测试类的测试用例
- 使用`TransmittableThreadLocal`替换基于线程堆栈的Mock上下文识别机制
- 增加专用于输出诊断信息的`@MockDiagnose`注解
## 0.4.12
- 支持`VERBOSE`级别的Mocking过程日志增强错误自助排查能力
- 支持使用`verifyTargetOnCompile`参数禁用编译期私有目标校验功能

View File

@ -0,0 +1,27 @@
Mock的生效范围
---
在`@MockMethod`和`@MockConstructor`注解上都有一个`scope`参数,其可选值有两种
- `MockScope.GLOBAL`该Mock方法将全局生效
- `MockScope.ASSOCIATED`该Mock方法仅对Mock容器关联测试类中的测试用例生效
举例来说,`AaaService`和`BbbService`是两个需要被测试的类,在`BbbService`的代码里有个`recordTicket()`调用依赖外部系统。因此在进行单元测试时,开发者在`BbbService`关联的Mock容器里使用`@MockMethod`注解定义了这个调用的替代方法。此时若该Mock方法的`scope`值为`MockScope.GLOBAL`,则不论是在`AaaServiceTest`测试类还是在`BbbServiceTest`测试类的测试用例只要直接或间接的执行到这行调用都会被置换为调用Mock方法。若该Mock方法的`scope`值为`MockScope.ASSOCIATED`则Mock只对`BbbServiceTest`类中的测试用例生效,而`AaaServiceTest`类中的测试用例在运行过程中执行到了`BbbService`类的相关代码,将会执行`recordTicket()`的原本调用。
对于常规项目而言单元测试里需要被Mock的调用都是由于其中包含了不需要或不便于测试的逻辑譬如“依赖外部系统”、“包含随机结果”、“执行非常耗时”等等这类调用在整个单元测试的生命周期里都应该被Mock方法置换不论调用的发起者是谁。因此`TestableMock`默认所有Mock方法都是全局生效的即`scope`默认值为`MockScope.GLOBAL`。
在一些大型项目中会有“下层模块编写单元测试上层模块编写端到端集成测试两者混合在一起运行”的情况这时候大部分Mock方法都应该使用`MockScope.ASSOCIATED`作为生效范围。针对这种情况,`TestableMock`支持通过`mockScope`运行参数来修改默认的Mock方法生效范围详见[全局运行参数](zh-cn/doc/javaagent-args.md)文档。
> 特别说明。若要Mock静态块里的调用Mock方法的`scope`必须为`MockScope.GLOBAL`因为静态块中的代码在程序初始化时就会执行不属于任何测试用例。典型场景是在使用JNI开发的项目中Mock系统库的加载方法。
> ```java
> static {
> System.loadLibrary("native-lib");
> }
> ```
> 若默认的`scope`参数不是`MockScope.GLOBAL`则相应Mock方法应当显式的声明`scope`值,例如:
> ```java
> @MockMethod(targetClass = System.class, scope = MockScope.GLOBAL)
> private void loadLibrary(String libname) {
> System.err.println("loadLibrary " + libname);
> }
> ```

View File

@ -77,18 +77,20 @@ class Demo {
class DemoTest {
private Demo demo = new Demo();
// 拦截`System.out.println`调用
@MockMethod
public void println(PrintStream ps, String msg) {
// 执行原调用
ps.println(msg);
public static class Mock {
// 拦截System.out.println调用
@MockMethod
public void println(PrintStream ps, String msg) {
// 执行原调用
ps.println(msg);
}
}
@Test
public void testRecordAction() {
Action action = new Action("click", ":download");
demo.recordAction();
// 验证Mock方法`println`被调用,且传入参数符合预期
// 验证Mock方法println被调用且传入参数格式符合预期
verify("println").with(matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download"));
}
}

View File

@ -1,14 +1,16 @@
自助问题排查
---
相比`Mockito`等由开发者手工放置Mock类的做法`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也有可能发生预料之外的Mock替换
相比`Mockito`等由开发者手工放置Mock类的做法`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也容易导致对“Mock究竟有没生效”的疑问
若要排查Mock相关的问题只需在测试类上添加`@MockWith`注解,并配置参数`diagnose`值为`MockDiagnose.ENABLE`在运行测试时就会打印出详细的Mock方法替换过程。
若要排查Mock相关的问题只需在相应的**Mock容器类**上添加`@MockDiagnose`注解,并配置参数值为`LogLevel.ENABLE`在运行测试时就会打印出详细的Mock方法替换过程。例如:
```java
@MockWith(diagnose = MockDiagnose.ENABLE)
class DemoTest {
...
@MockDiagnose(LogLevel.ENABLE)
public static class Mock {
...
}
}
```
@ -16,6 +18,8 @@ class DemoTest {
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/DemoMockTest$Mock
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
[DIAGNOSE] Handling method <init>
@ -31,34 +35,38 @@ class DemoTest {
... ...
```
日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
其中`Line XX, mock method "XXX" used`日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
简单排查方法:
- 若没有任何输出,请检查`pom.xml`或`build.gradle`配置是否正确引入了TestableMock依赖
- 若只输出了`Handling test class`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test"`0.4.x`版本要求)
- 若输出了`Handling source class`以及`Handling method xxx`但预期的代码行位置没有发生Mock替换请检查Mock方法定义是否未与目标方法匹配
- 若没有任何输出,请检查`pom.xml`或`build.gradle`配置是否正确引入了`TestableMock`依赖
- 若只输出了`Handling mock class`请检查Mock容器类的名称和位置是否符合规范
- 若只输出了`Handling mock class`和`Handling test class`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test",或者是否正确的使用了`@MockWith`注解
- 若输出了`Handling source class`以及`Handling method xxx`但预期的代码行位置没有发生Mock替换请继续检查Mock方法定义是否未与目标方法匹配
对于预期Mock未生效的情况如需进一步排查可将日志级别提升到`MockDiagnose.VERBOSE`。
对于上述的最后一种情况预期Mock未生效可将日志级别提升到`LogLevel.VERBOSE`做进一步排查。例如:
```java
@MockWith(diagnose = MockDiagnose.VERBOSE)
class DemoTest {
...
@MockDiagnose(LogLevel.VERBOSE)
public static class Mock {
...
}
}
```
再次执行单元测试将会打印出所有Mock方法的运行期签名以及被测类中扫描到所有调用的运行期签名
再次执行单元测试,此时将会打印出所有Mock方法的运行期签名以及被测类中扫描到所有调用的运行期签名
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
[VERBOSE] Test case "should_able_to_mock_new_object"
... ...
[VERBOSE] Test case "should_able_to_set_mock_context"
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/DemoMockTest$Mock
[VERBOSE] Mock constructor "createBlackBox" as "(Ljava/lang/String;)V" for "com/alibaba/testable/demo/model/BlackBox"
[VERBOSE] Mock method "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
[VERBOSE] Mock method "staticFunc" as "()Ljava/lang/String;"
[VERBOSE] Mock method "trim" as "()Ljava/lang/String;"
[VERBOSE] Mock method "sub" as "(II)Ljava/lang/String;"
[VERBOSE] Mock method "startsWith" as "(Ljava/lang/String;)Z"
[VERBOSE] Mock method "secretBox" as "()Lcom/alibaba/testable/demo/model/BlackBox;"
... ...
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
@ -74,19 +82,16 @@ class DemoTest {
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
[DIAGNOSE] Line 27, mock method "innerFunc" used
[VERBOSE] Line 27, invoking "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "staticFunc" as "()Ljava/lang/String;"
[DIAGNOSE] Line 27, mock method "staticFunc" used
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "toString" as "()Ljava/lang/String;"
... ...
```
输出日志结构参考如下:
- `Mock constructor "<Mock方法名>" as "<方法签名>" for "<类型>"` 在测试类中扫描到的**Mock构造方法**及其运行时签名
- `Mock method "<Mock方法名>" as "<方法签名>"` 在测试类中扫描到的**普通Mock方法**及其运行时签名打印时暂未自动排除用于指定Mock目标类的首位参数
- `Line XX, constructing "<类型>" as "<方法签名>"` 在被测类中扫描掉的**构造方法调用**及其运行时签名
- `Line XX, invoking "<方法名>" as "<方法签名>"` 在被测类中扫描到的**成员方法调用**及其运行时签名
- `Mock constructor "<Mock方法名>" as "<方法签名>" for "<类型>"` 在测试类中扫描到的**Mock构造方法**及其运行期签名
- `Mock method "<Mock方法名>" as "<方法签名>"` 在测试类中扫描到的**普通Mock方法**及其运行期签名
- `Line XX, constructing "<类型>" as "<方法签名>"` 在被测类中扫描掉的**构造方法调用**及其运行期签名
- `Line XX, invoking "<方法名>" as "<方法签名>"` 在被测类中扫描到的**成员方法调用**及其运行期签名
"运行期签名"是目标方法参数和返回值类型的在字节码中的表示形式其结构相比方法的原始Java签名更紧凑精炼通过对比相应代码行调用时的方法签名与Mock方法的实际签名通常能够快速定位出Mock未匹配的原因。
> 在`0.4.x`版本使用测试类添加`@MockWith`注解的`diagnose`参数来启用诊断信息的方法在`0.5`版本中依然可用,但将在未来版本中移除,请优先使用`@MockDiagnose`注解替代。

View File

@ -0,0 +1,49 @@
升级到0.5版本
---
经过近一个月的设计和开发TestableMock的`0.5`版本终于和大家见面了。相比`0.4`版本,新版解决了此前遗留的三大历史问题:
1. <s>**Mock方法无法调用其他非静态方法**</s>。新版中的Mock方法与普通方法不再有任何差别可以访问任意外部方法和成员变量。
2. <s>**Mock方法总是作用于整个测试生命周期**</s>。从现在开始Mock方法支持将生效范围限定为**所属测试类里的测试用例**不用担心跨类测试调用被意外Mock掉了。
3. <s>**需手工清理MOCK_CONTEXT且只支持类粒度的并行测试**</s>。现在每个测试用例拥有了独立的`MOCK_CONTEXT`变量,无需清理也不会串号,而且可以放心使用**任意粒度**的并行单元测试啦。
在使用方式上,`0.5`版本延续`TestableMock`简洁轻量的原则同时为了更好的实现Mock方法复用新版本的Mock类与测试类之间有了明确的边界。从`0.4`版本升级到`0.5`时唯一需要的改变是将测试类中的所有Mock方法使用一个`public static class Mock { }`包裹起来。
例如,原先有如下测试类定义:
```java
public class DemoMockTest {
@MockMethod(targetClass = DemoMock.class)
private String innerFunc(String text) {
return "hello_" + text;
}
@Test
void should_able_to_mock_member_method() throws Exception {
assertEquals("hello_world", demoMock.outerFunc());
verify("innerFunc").with("world");
}
}
```
升级为`0.5`版本后将所有Mock方法此例中只有`innerFunc`这一个方法)移到一个名称为`Mock`的静态内部类中,相当于增加两行代码:
```java
public class DemoMockTest {
public static class Mock { // 增加此行
@MockMethod(targetClass = DemoMock.class)
private String innerFunc(String text) {
return "hello_" + text;
}
} // 增加此行
@Test
void should_able_to_mock_member_method() throws Exception {
assertEquals("hello_world", demoMock.outerFunc());
verify("innerFunc").with("world");
}
}
```
然后将`pom.xml`或`build.gradle`文件中的TestableMock依赖升级到`0.5`或以上版本即可。

View File

@ -0,0 +1,66 @@
使用MockWith注解
---
`@MockWith`注释的功能是为测试显式指定Mock容器通常用于测试类或Mock容器类没有在标准约定位置的情况以下列举几种典型使用场景。
### 1. 非标准位置的Mock容器类
`TestableMock`会依次在以下两个位置寻找Mock容器
- 测试类中名为`Mock`的静态内部类(譬如原类型是`Demo`,测试类是`DemoTest`Mock容器类为`DemoTest.Mock`
- 同包路径下寻找名为`被测类+Mock`的外部类(譬如原类型是`Demo`,测试类是`DemoTest`Mock容器类为`DemoMock`
倘若实际要使用的Mock容器类不在这两个位置就需要在测试类上使用`@MockWith`注释了。一般来说造成Mock容器类不在默认位置的原因可能有两种复用Mock容器、集中管理Mock容器。
当对一批功能近似的类型进行测试的时候由于需要进行Mock的外部调用基本一致可以将这些类型所需的所有Mock方法集中写在一个Mock容器类里然后让相关测试类共同引用这个公共Mock容器。详见[复用Mock类与方法](zh-cn/doc/mock-method-reusing.md)文档。
另一种情况是开发者希望将Mock方法的定义与测试类本身分开以便进行集中管理或规避某些扫描工具的路径规则譬如形成下面这种目录结构
```
src/
main/
com/
demo/
service/
DemoService.java
test/
com/
demo/
service/
DemoServiceTest.java
mock/
service/
DemoServiceMock.java
```
此时就需要在测试类上显式的指定相应的Mock容器类了这种场景在实际情况中比较少见。
### 2. 非标准位置的测试类
`TestableMock`在建立“被测类”、“测试类”、“Mock容器类”之间关联的过程中为了识别被测类的位置约定测试类应当与被测类在相同的包路径且名称为`被测类+Test`。当实际情况不符合这种约定的时候,就需要通过在**被测类**上添加`@MockWith`注解来显式的建立关联。例如:
```java
@MockWith(ServiceTest.Mock.class)
public class DemoService {
...
}
public class ServiceTest {
public static class Mock extends BasicMock {
...
}
}
```
> 注意:
> 1. `@MockWith`注解通常是使用在测试类上,但这种情况下需要用在被测类上
> 2. `@MockWith`指向的目标始终是Mock容器类而不是测试类
> 3. `@MockWith`默认使用被注解类名字的尾缀判断当前类是被测类(名字非`Test`结尾)还是测试类(名字`Test`结尾),若遇到不符合此规则的类型,应使用注解的`treatAs`参数显式的指定(`ClassType.SourceClass`-被测类/`ClassType.TestClass`-测试类)
更进一步来说若出现测试类与Mock容器类均不在约定位置的时候则需要同时在**测试类**与**被测类**上都使用`@MockWith`指向同一个Mock容器类来建立三者的关联但这种场景在实际运用中很少会遇到。
### 3. 使用不包含Mock方法的Mock容器类
为了加快搜索Mock容器类的速度在扫描过程中`TestableMock`只会将自身定义有Mock方法包含`@MockMethod`或`@MockMockConstructor`注解的方法)以及明确被`@MockWith`指向的类识别为有效的Mock容器而不会去遍历其父类。
对于某些特殊情况譬如希望将实际Mock方法均定义在父类实际使用的子容器仅仅重载父类的某些特定方法此时即使Mock容器类的位置符合约定为了能够被识别依然应该在相应的测试类上增加对Mock容器类的`@MockWith`引用。

View File

@ -4,14 +4,28 @@
相比以往Mock工具以类为粒度的Mock方式`TestableMock`允许用户直接定义需要Mock的单个方法并遵循约定优于配置的原则按照规则自动在测试运行时替换被测方法中的指定方法调用。
> 归纳起来就两条:
> - Mock非构造方法拷贝原方法定义到测试类,加`@MockMethod`注解
> - Mock构造方法拷贝原方法定义到测试类,返回值换成构造的类型,方法名随意,加`@MockContructor`注解
> - Mock非构造方法拷贝原方法定义到Mock容器类,加`@MockMethod`注解
> - Mock构造方法拷贝原方法定义到Mock容器类,返回值换成构造的类型,方法名随意,加`@MockContructor`注解
具体的Mock方法定义约定如下
具体的Mock方法定义约定如下。
#### 0. 前置步骤准备Mock容器
首先为测试类添加一个关联的Mock类型作为承载其Mock方法的容器最简单的做法是在测试类里添加一个名称为`Mock`的静态内部类。例如:
```java
public class DemoTest {
public static class Mock {
// 放置Mock方法的地方
}
}
```
#### 1. 覆写任意类的方法调用
在测试类里定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,并在注解的`targetClass`参数指定该方法原本所属对象类型。
Mock类中定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,并在注解的`targetClass`参数指定该方法原本所属对象类型。
此时被测类中所有对该需覆写方法的调用将在单元测试运行时将自动被替换为对上述自定义Mock方法的调用。
@ -111,7 +125,7 @@ private BlackBox createBlackBox(String text) {
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_mock_new_object()`测试用例。
#### 5. 识别当前测试用例和调用来源
#### 5. 在Mock方法中区分调用来源
在Mock方法中通过`TestableTool.SOURCE_METHOD`变量可以识别**进入该Mock方法前的被测类方法名称**;此外,还可以借助`TestableTool.MOCK_CONTEXT`变量为Mock方法注入“**额外的上下文参数**”,从而区分处理不同的调用场景。
@ -124,7 +138,6 @@ public void testDemo() {
assertEquals(true, demo());
MOCK_CONTEXT.put("case", "has-error");
assertEquals(false, demo());
MOCK_CONTEXT.clear();
}
```
@ -144,12 +157,6 @@ private Data mockDemo() {
}
```
注意,由于`TestableMock`并不依赖(也不希望依赖)任何特定测试框架,因而无法自动识别单个测试用例的结束位置,这使得设置到`TestableTool.MOCK_CONTEXT`变量的参数可能会在同测试类中跨测试用例存在。建议总是在使用后及时使用`MOCK_CONTEXT.clear()`清空上下文也可将这行语句添加到单元测试框架特定的测试用例结束的统一位置比如JUnit 5的`@AfterEach`方法。
在当前版本中,此变量在运行期的效果类似于一个在测试类中的普通`Map`类型成员对象但请尽量使用此变量而非自定义对象传递附加的Mock参数以便在将来升级至`v0.5`版本时获得更好的兼容性。
> `TestableTool.MOCK_CONTEXT`变量的值是在测试类内共享的,当单元测试并行运行时,建议请选择`parallel`类型为`classes`
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_able_to_get_source_method_name()`和`should_able_to_get_test_case_name()`测试用例。
#### 6. 验证Mock方法被调用的顺序和参数
@ -160,10 +167,15 @@ private Data mockDemo() {
#### 特别说明
> **0.4.x 版本的Mock约定**
> - 测试类与被测类的包路径应相同,且名称为`被测类名+Test`(通常采用`Maven`或`Gradle`构建的Java项目均符合这种惯例
> - Mock方法即包含`@MockMethod`或`@MockContructor`注解的方法)会在运行期被自动修改为`static`方法请勿在Mock方法的定义中访问任何非静态成员。
> **Mock只对被测类的代码有效**
>
> 这两项约束会在`0.5`版本中去除
> 在`TestableMock`的[Issues](https://github.com/alibaba/testable-mock/issues)列表中最常见的一类问题是“Mock为什么没生效”其中最多的一种情况是“在测试用例里直接调用了Mock的方法发现没有替换”。这是因为<u>Mock替换只会作用在**被测类**的代码里</u>哦(~ ̄▽ ̄)~。知道大家是想快速验证一下`TestableMock`的功能不过测试用例的代码真滴无需被Mock这心意我们领了👻
>
> 当Mock方法内容较复杂包含Lambda语句、构造块、匿名类等编译器会在构建期生成额外的非静态临时方法导致"Bad type in operand stack"错误。如果有遇到此类错误请将Mock方法显式加上`static`修饰即可解决。这个问题会在`0.5`版本中彻底解决。
> 除去这种情况若Mock未生效请参考[自助问题排查](zh-cn/doc/troubleshooting.md)提供的方法对比<u>Mock方法签名</u><u>目标位置的调用方法签名</u>。若依然无法定位原因欢迎提交Issues告诉我们。
> **测试类和Mock容器的命名约定**
>
> 默认情况下,`TestableMock`假设测试类与被测类的<u>包路径相同,且名称为`被测类名+Test`</u>(通常采用`Maven`或`Gradle`构建的Java项目均符合这种惯例
> 同时约定测试类关联的Mock容器为<u>在其内部且名为`Mock`的静态类</u>,或<u>相同包路径下名为`被测类名+Mock`的独立类</u>
>
> 当测试类或Mock容器路径不符合此约定时可使用`@MockWith`注解显式指定,详见[使用MockWith注解](zh-cn/doc/use-mock-with.md)。

View File

@ -7,12 +7,18 @@
- 使用指南
- [校验Mock调用](zh-cn/doc/invoke-matcher.md)
- [Mock的生效范围](zh-cn/doc/scope-of-mock.md)
- [复用Mock类与方法](zh-cn/doc/mock-method-reusing.md)
- [使用MockWith注解](zh-cn/doc/use-mock-with.md)
- [常见使用问题](zh-cn/doc/frequently-asked-questions.md)
- [在IDE运行单元测试](zh-cn/doc/use-in-ide.md)
- [自助问题排查](zh-cn/doc/troubleshooting.md)
- [使用Maven插件](zh-cn/doc/use-maven-plugin.md)
- 技术参考
- [升级到0.5版本](zh-cn/doc/upgrade-to-v05.md)
- [全局运行参数](zh-cn/doc/javaagent-args.md)
- [注解参数清单](zh-cn/doc/annotations.md)
- [主流Mock工具对比](zh-cn/doc/comparation.md)
- [版本更新](zh-cn/doc/release-note.md)
- [关于我们](zh-cn/doc/about-us.md)