optimize docs

This commit is contained in:
金戟 2021-04-06 00:03:38 +08:00
parent c393d40d0d
commit 7c1f18de46
10 changed files with 206 additions and 136 deletions

View File

@ -1,4 +1,29 @@
How Mock Works
---
TO BE TRANSLATED
This document mainly introduces the design and implementation principles of the mock function in `TestableMock`.
Unlike common mock-tools which write mock definitions in each test case, `TestableMock` allows each business class to provide its own set of mock methods, describing what should be mocked during testing, and define the corresponding replacement logic (that is, each business class has its own independent "Test class" and independent "Mock class"). By adopting "convention is better than configuration" principle, `TestableMock` not only reduce redundant codes but also reduce the cost of mock learning.
This design is based on two basic assumptions:
1. In any business class, methods that need to be mocked in one test case are usually required to be mocked in other test cases. For these mocked methods usually requiring external dependencies that are not easy to test.
2. Each unit test only focuses on the logic inside the unit under test, and irrelevant calls outside the unit should be replaced with mocks. That is, the invocations that need to be mocked should all be in the code of the class under test.
Accordingly, the unit test scenarios that meet the above assumptions are simplified through conventions, and the remaining more complex use scenarios are supported through configuration.
The mechanism of `TestableMock` can be summarized in one sentence: <u>Using java agent to dynamically modify the bytecode, before the unit test is about to run, replace all invocations in the business class under test which match the mock method definition with invocations to the mock method itself.</u>.
The final effect is that no matter what service framework or object-container the code uses, no matter whether the object of mock target is injected by a framework, created by new operation, and whether the target method of mock is private or external, defined as global, local, static, inherited or overloaded, all can be mocked in a same and simple way, which make unit testing much easier.
> Notice: Mock's goal is the method invocation in the class under test. The code inside the test case will not be mocked, and the method definition itself has not changed, but the invocation code to those methods will be replaced.
Specifically, when the unit test is started, `TestableMock` will preprocess the classes loaded into memory and establish the association relationship between the "class under test", the "test class", and the "mock container class" respectively (can be one-to-one or many-to-one). On the one hand, this association is to correctly match the mock call and replace it before the test case is executed, and on the other hand, it is used to control the effective scope of the mock method.
For the class under test, replace the matched call with a call to the mock container method.
For the test class, insert the mock context initialization code at the beginning of each test case.
For the mock container class, add the `testableIns()` method to make the class become a singleton class, and insert codes to record the call at the beginning of each Mock method.
The above is the core logic of the entire mocking logic. For more implementation details, please refer to the source code. If you have any questions, suggestions, or improvement proposals, you are welcome to participate in the discussion and contribute through Github Issue and Pull Request 😃

View File

@ -1,7 +1,7 @@
Use TestableMock
---
`TestableMock` is an assist tool for Java unit testing based on source code and bytecode enhancement, including the following functions:
`TestableMock` is now not only a lightweight and easy-to-use unit testing mock tool, but also a comprehensive set of auxiliary tools aimed at **simplifying Java unit testing**, including the following functions:
- [Quickly mock arbitrary call](en-us/doc/use-mock.md): quickly replace any method invocation in the class under test with a mock method, solve the cumbersome use of traditional mock tools problem
- [Access private members of the class under test](en-us/doc/private-accessor.md): enable unit tests directly invoke or access private members of the class under test, solve the problems of private member initialization and private method testing
@ -93,3 +93,20 @@ See the [build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo
> ```
>
> See [issue-43](https://github.com/alibaba/testable-mock/issues/43) for a complete example.
> If the project is using `Spock` test framework, you need to specify the bytecode version generated by `Groovy` to be 1.6 or above, the method is as follows (please modify the properties value according to the actual JVM version used).
>
> For Maven project, add `<maven.compiler.source>` and `<maven.compiler.target>` properties inside the `pom.xml` file, e.g.
> ```xml
> <properties>
> <maven.compiler.source>1.6</maven.compiler.source>
> <maven.compiler.target>1.6</maven.compiler.target>
> </properties>
> ```
>
> For Gradle project, add a `sourceCompatibility` property inside the `build.gradle` file, e.g.
> ```groovy
> sourceCompatibility = '6'
> ```
>
> See project `demo/spock-demo` for a complete example.

View File

@ -3,52 +3,45 @@ Self-Help Troubleshooting
Compared with `Mockito` and other mock tools where developers have to manually inject mock classes, `TestableMock` uses method name and parameter type matching to automatically find invocations that require mock. While this mechanism brings convenience, it may also cause unexpected mock replacement.
To troubleshoot mock-related issues, just add the `@MockDiagnose` annotation to the mock class, and set the value `LogLevel.ENABLE`, so the detailed mock method replacement process will be printed when the test is run.
For this reason, `TestableMock` will automatically save the mock scanning log of the last test run in the project build directory. The default location is `target/testable-agent.log` (Maven project) or `build/testable-agent.log` (Gradle project).
```java
class DemoTest {
@MockDiagnose(LogLevel.ENABLE)
public static class Mock {
...
}
}
```
The output log example is as follows:
Examples of log content are as follows:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
[DIAGNOSE] Handling method <init>
[DIAGNOSE] Handling method newFunc
[DIAGNOSE] Line 19, mock method "createBlackBox" used
[DIAGNOSE] Handling method outerFunc
[DIAGNOSE] Line 27, mock method "innerFunc" used
[DIAGNOSE] Line 27, mock method "staticFunc" used
[DIAGNOSE] Handling method commonFunc
[DIAGNOSE] Line 34, mock method "trim" used
[DIAGNOSE] Line 34, mock method "sub" used
[DIAGNOSE] Line 34, mock method "startsWith" used
[INFO] Start at Mon Jan 00 00:00:00 CST 0000
... ...
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
[INFO] Found 6 test cases
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[INFO] Found 8 mock methods
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
[INFO] Found method <init>
[INFO] Found method newFunc
[INFO] Line 19, mock method "createBlackBox" used
[INFO] Found method outerFunc
[INFO] Line 27, mock method "innerFunc" used
[INFO] Line 27, mock method "staticFunc" used
[INFO] Found method commonFunc
[INFO] Line 34, mock method "trim" used
[INFO] Line 34, mock method "sub" used
[INFO] Line 34, mock method "startsWith" used
... ...
[INFO] Completed at Mon Jan 00 00:00:00 CST 0000
```
The log shows all the mocked invocation and corresponding code line numbers in the class under test.
- Self troubleshooting:
According to the targeted test classes, below are some simple clues for self-troubleshooting. Suppose the class under test is "com.demo.BizService", the test class is "com.demo.BizServiceTest", and the mock container class is "com.demo.BizServiceTest.Mock":
- If there is no output, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies
- If the log file is no generated, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies
- If only `com/demo/BizServiceTest$Mock` is found in the output, please check whether the mock class is created at correct place
- If both the test class and the mock class are found, but the class under test `com/demo/BizService` not appeared, please check whether the test class is in the same package of the class under test, and the name is "<ClassUnderTest>+Test", otherwise `@MockWith` annotation should be used
- If all the three classes are found, and `Found method xxx` are output, but there is no mock replacement happen at the expected code line, please check whether the mock method definition matches the target method
- If only the first line of `Handling mock class` is output, please check whether the mock class is created at correct place
- If `Handling mock class` and `Handling test class` are output, please check whether the test class is in the same package of the class under test, and the name is "<ClassUnderTest>+Test", otherwise `@MockWith` annotation should be used
- If `Handling source class` and `Handling method xxx` are output, but there is no mock replacement happen at the expected code line, please check whether the mock method definition matches the target method
For situations where expected mocking is not take effect, you could set the diagnosis level to `LogLevel.VERBOSE` for further investigation information.
For situations where expected mocking is not take effect, you could add a `@MockDiagnose` annotation to the mock class, and set the diagnosis level to `LogLevel.VERBOSE` for further investigation information.
```java
class DemoTest {
class BizServiceTest {
@MockDiagnose(LogLevel.VERBOSE)
public static class Mock {
...
@ -59,30 +52,30 @@ class DemoTest {
Executing the unit test again will print out the signatures of all mock methods, and the signatures of all invocations scanned in the class under test:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
[VERBOSE] Test case "should_mock_new_object"
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
[TIP] Test case "should_mock_new_object"
... ...
[VERBOSE] Test case "should_set_mock_context"
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[VERBOSE] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[VERBOSE] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[TIP] Test case "should_set_mock_context"
[INFO] Found 6 test cases
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[TIP] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[TIP] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
... ...
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
[DIAGNOSE] Handling method <init>
[VERBOSE] Line 13, constructing "java.lang.Object()"
[DIAGNOSE] Handling method newFunc
[VERBOSE] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[DIAGNOSE] Line 19, mock method "createBlackBox" used
[VERBOSE] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
[VERBOSE] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
[DIAGNOSE] Handling method outerFunc
[VERBOSE] Line 27, constructing "java.lang.StringBuilder()"
[VERBOSE] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
[VERBOSE] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[DIAGNOSE] Line 27, mock method "innerFunc" used
[TIP] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[INFO] Found 8 mock methods
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
[INFO] Found method <init>
[TIP] Line 13, constructing "java.lang.Object()"
[INFO] Found method newFunc
[TIP] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[INFO] Line 19, mock method "createBlackBox" used
[TIP] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
[TIP] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
[INFO] Found method outerFunc
[TIP] Line 27, constructing "java.lang.StringBuilder()"
[TIP] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
[TIP] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[INFO] Line 27, mock method "innerFunc" used
... ...
```
@ -92,3 +85,7 @@ The logs are formatted in follow pattern:
- `Mock method "<MockMethodName>" as "<Signature>"` Mock method found in test class (the first parameter that identify the mock target class is currently kept)
- `Line XX, constructing "<TypeName>" as "<Signature>"` Constructor invocation found in test under class
- `Line XX, invoking "<MethodName>" as "<Signature>"` Member method invocation found in test under class
> In order to clearly distinguish between the type of method return value and the type of invoker, the method signature recorded in the log uses a method definition structure similar to `Kotlin`.
Comparing the actual signature of the original call with the signature defined by the mock method, the problem is usually found quickly.

View File

@ -1,7 +1,9 @@
Fast Mocking
---
Compared with the class-granularity mocking practices of existing mock tools, `TestableMock` allows developers to directly define a single method and use it for mocking. With the principle of convention over configuration, mock method replacement will automatically happen when the specified method in the test class match an invocation in the class under test.
In unit testing, the main role of the mock method is to replace those methods with **need external dependencies**, **with time-consuming**, **has random results**, or other effects that affect the development of the test, but do not affect the key the logic to be tested. Generally speaking, a certain call needs to be mocked, which is usually only related to its own characteristics, and has nothing to do with the source of the invocation.
Based on the above information, `TestableMock` has designed a minimalist mock mechanism. Unlike the common mock tools that uses **class** as the definition granularity of mocking, and repeats the description of mock behavior in each test case, `TestableMock` allows each business class (class under test) to be associated with a set of reusable collection of mock methods (carried by the mock container class), following the principle of "convention over configuration", and mock method replacement will automatically happen when the specified method in the test class match an invocation in the class under test.
> In summary, there are two simple rules:
> - Mock non-constructive method, copy the original method definition to the mock class, add a `@MockMethod` annotation
@ -55,7 +57,7 @@ private String use_any_mock_method_name(int i, int j) {
Sometimes, the mock method need to access the member variables in the original object that initiated the invocation, or invoke other methods of the original object. At this point, you can remove the `targetClass` parameter in the `@MockMethod` annotation, and then add a extra parameter whose type is the original object type of the method to the first index of the method parameter list.
The `TestableMock` convention is that when the `targetClass` parameter value of the `@MockMethod` annotation is empty, the first parameter of the mock method is the type of the target method, and the parameter name is arbitrary. In order to facilitate code reading, it is recommended to name this parameter as `self` or `src`. Example as follows:
The `TestableMock` convention is that when the `targetClass` parameter of the `@MockMethod` annotation is not defined, the first parameter of the mock method is the type of the target method, and the parameter name is arbitrary. In order to facilitate code reading, it is recommended to name this parameter as `self` or `src`. Example as follows:
```java
// Adds a `String` type parameter to the first position the mock method parameter list (parameter name is arbitrary)
@ -67,7 +69,7 @@ private String substring(String self, int i, int j) {
}
```
For complete code examples, see the `should_mock_common_method()` test cases in the `java-demo` and `kotlin-demo` sample projects. (Because Kotlin has made magical changes to the String type, the method under test in the Kotlin example adds a layer of encapsulation to the `BlackBox` class)
For complete code examples, see the `should_mock_common_method()` test cases in the `java-demo` and `kotlin-demo` sample projects. (Because `Kotlin` has made magical changes to the String type, the method under test in the `Kotlin` example adds a layer of encapsulation to the `BlackBox` class)
### 1.2 Mock the member method of the class under test itself
@ -161,7 +163,17 @@ For complete code examples, see the `should_get_source_method_name()` and `shoul
### 3. Verify the sequence and parameters of the mock method being invoked
In test cases, you can use the `TestableTool.verify()` method, and cooperate with `with()`, `withInOrder()`, `without()`, `withTimes()` and other methods to verify the mock call situation.
In test cases, you can use the `InvokeVerifier.verify()` method, and cooperate with `with()`, `withInOrder()`, `without()`, `withTimes()` and other methods to verify the mock call situation.
For details, please refer to the [Check Mock Call](en-us/doc/matcher.md) document.
### 4. Special instructions
> **Naming Conventions for Test Classes and Mock Containers**
>
> By default, `TestableMock` assumes that the <u>package path of the test class and the class under test are the same, and the name is `<ClassUnderTest>+Test`</u> (usually Java project with `Maven` or `Gradle` conform to this convention).
> At the same time, it is agreed that the mock container associated with the test class is <u>in its internal static class named `Mock`</u>, or <u>an independent class named `<ClassUnderTest>+Mock` under the same package path</u>.
>
> When the test class or mock container path does not follow to this convention, you can use the `@MockWith` annotation to specify it explicitly. For details, see [Use MockWith Annotation](en-us/doc/use-mock-with.md).
For more implementation details of `TestableMock`, please refer to the [Design and Principle](en-us/doc/design-and-mechanism.md) document.

View File

@ -1,7 +1,7 @@
TestableMock简介
---
单元测试中的Mock方法通常是为了绕开那些依赖外部资源或无关功能的方法调用使得测试重点能够集中在需要验证和保障的代码逻辑上。某个调用需要被Mock往往只与其自身特征有关而与调用的来源无关。
单元测试中的Mock方法通常是为了绕开那些依赖外部资源或无关功能的方法调用使得测试重点能够集中在需要验证和保障的代码逻辑上。
在定义Mock方法时开发者真正关心的只有一件事"<u>这个调用在测试的时候要换成那个假的Mock方法</u>"。

View File

@ -8,9 +8,9 @@ TestableMock的设计和原理
这种设计基于两项基本假设:
1. 同一个测试类里一个测试用例里需要Mock掉的方法在其他测试用例里通常也都需要Mock。因为这些被Mock的方法往往访问了不便于测试的外部依赖。
2. 需要Mock的调用都来自被测类的代码。此假设是符合单元测试初衷的即单元测试只应该关注当前单元的内部行为单元外的逻辑应该被替换为Mock
2. 每个单元测试只关注被测单元内部的逻辑单元外的无关调用应该被替换为Mock。即需要被Mock的调用应该都在被测类的代码中。
据此通过约定来简化符合假设的单元测试场景,通过配置来支持其余复杂的使用场景。
据此通过约定来简化符合以上假设的单元测试场景,通过配置来支持其余复杂的使用场景。
`TestableMock`的原理可以用一句话概括:<u>利用JavaAgent动态修改字节码把被测的业务类中与所有与Mock方法定义匹配的调用在单元测试运行时替换成对Mock方法的调用</u>

View File

@ -8,8 +8,9 @@
| 参数 | 描述 | 可用值和示例(`N/A`表示无需赋值) |
| ---- | ---- | ---- |
| logLevel | 修改全局日志级别 | 可用值为:`mute`(禁止打印警告) / `debug`(打印调试信息) / `verbose`(打印非常详细的调试信息) |
| dumpPath | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 例如 `/tmp/bytecode`(需要此目录事先存在) |
| pkgPrefix | 限定`TestableMock`仅对部分包生效 | 使用`,`分隔的包路径前缀列表,例如 `com.demo.svc,com.demo.dao` |
| logFile | 指定TestableAgent日志文件位置 | 相对项目根目录的位置,例如:`target/testable/agent.log`,特殊值`null`表示禁用日志文件 |
| dumpPath | 将修改过后的字节码保存到本地指定目录(用于排查问题) | 相对项目根目录的位置,例如:`target/bytecode` |
| pkgPrefix | 限定`TestableMock`仅对部分包生效 | 使用`,`分隔的包路径前缀列表,例如:`com.demo.svc,com.demo.dao` |
| mockScope | 修改默认的Mock生效范围详见[Mock生效范围](zh-cn/doc/scope-of-mock) | 可用值为:`global`(全局生效) / `associated`(只对关联的测试用例生效) |
| useThreadPool | 启用基于`TransmittableThreadLocal`的Mock上下文存储用于包含线程池的测试用例 | `N/A` |

View File

@ -1,7 +1,7 @@
使用TestableMock
---
`TestableMock`是基于源码和字节码增强的Java单元测试辅助工具,包含以下功能:
`TestableMock`现在已不仅是一款轻量易上手的单元测试Mock工具更是以**简化Java单元测试**为目标的综合辅助工具集,包含以下功能:
- [快速Mock任意调用](zh-cn/doc/use-mock.md)使被测类的任意方法调用快速替换为Mock方法实现"指哪换哪"解决传统Mock工具使用繁琐的问题
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
@ -93,3 +93,20 @@ test {
> ```
>
> 完整示例参考[issue-43](https://github.com/alibaba/testable-mock/issues/43)
> 若项目使用`Spock`测试框架,需指定`Groovy`编译生成的JVM 1.6或以上版本字节码方法如下请根据实际使用的JVM版本修改属性值
>
> Maven项目在`pom.xml`中添加`<maven.compiler.source>`和`<maven.compiler.target>`属性,例如:
> ```xml
> <properties>
> <maven.compiler.source>1.6</maven.compiler.source>
> <maven.compiler.target>1.6</maven.compiler.target>
> </properties>
> ```
>
> Gradle项目在`build.gradle`中添加`sourceCompatibility`属性,例如:
> ```groovy
> sourceCompatibility = '6'
> ```
>
> 完整代码可参考`demo/spock-demo`示例项目。

View File

@ -3,51 +3,45 @@
相比`Mockito`等由开发者手工放置Mock类的做法`TestableMock`使用方法名和参数类型匹配自动寻找需Mock的调用。这种机制在带来方便的同时也容易导致对“Mock究竟有没生效”的疑问。
若要排查Mock相关的问题只需在相应的**Mock容器类**上添加`@MockDiagnose`注解,并配置参数值为`LogLevel.ENABLE`在运行测试时就会打印出详细的Mock方法替换过程。例如
为此,`TestableMock`会在项目构建目录下自动保存最后一次测试运行过程的Mock扫描日志。默认位置为`target/testable-agent.log`Maven项目或`build/testable-agent.log`Gradle项目
```java
class DemoTest {
@MockDiagnose(LogLevel.ENABLE)
public static class Mock {
...
}
}
```
输出日志示例如下:
日志内容示例如下:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
[DIAGNOSE] Handling method <init>
[DIAGNOSE] Handling method newFunc
[DIAGNOSE] Line 19, mock method "createBlackBox" used
[DIAGNOSE] Handling method outerFunc
[DIAGNOSE] Line 27, mock method "innerFunc" used
[DIAGNOSE] Line 27, mock method "staticFunc" used
[DIAGNOSE] Handling method commonFunc
[DIAGNOSE] Line 34, mock method "trim" used
[DIAGNOSE] Line 34, mock method "sub" used
[DIAGNOSE] Line 34, mock method "startsWith" used
[INFO] Start at Mon Jan 00 00:00:00 CST 0000
... ...
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
[INFO] Found 6 test cases
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[INFO] Found 8 mock methods
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
[INFO] Found method <init>
[INFO] Found method newFunc
[INFO] Line 19, mock method "createBlackBox" used
[INFO] Found method outerFunc
[INFO] Line 27, mock method "innerFunc" used
[INFO] Line 27, mock method "staticFunc" used
[INFO] Found method commonFunc
[INFO] Line 34, mock method "trim" used
[INFO] Line 34, mock method "sub" used
[INFO] Line 34, mock method "startsWith" used
... ...
[INFO] Completed at Mon Jan 00 00:00:00 CST 0000
```
其中`Line XX, mock method "XXX" used`日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
简单排查方法:
依据需排查的测试类,进行针对性排查。假设被测类为"com.demo.BizService",测试类为"com.demo.BizServiceTest"Mock容器类为"com.demo.BizServiceTest.Mock"
- 若没有任何输出,请检查`pom.xml`或`build.gradle`配置是否正确引入了`TestableMock`依赖
- 若只输出了`Handling mock class`请检查Mock容器类的名称和位置是否符合规范
- 若只输出了`Handling mock class`和`Handling test class`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test",或者是否正确的使用了`@MockWith`注解
- 若输出了`Handling source class`以及`Handling method xxx`但预期的代码行位置没有发生Mock替换请继续检查Mock方法定义是否未与目标方法匹配
- 若该日志文件未生成,请检查`pom.xml`或`build.gradle`配置是否正确引入了`TestableMock`依赖
- 若日志中只能找到`com/demo/BizServiceTest$Mock`,没有找到被测类和测试类请检查Mock容器类的名称和位置是否符合规范
- 若日志中找到了Mock类和测试类但没有找到被测类`com/demo/BizService`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test",或者是否正确的使用了`@MockWith`注解
- 若日志中三个类都已经找到,且有`Found method xxx`但预期的代码行位置没有发生Mock替换请继续检查Mock方法定义是否未与目标方法匹配
对于上述的最后一种情况预期Mock未生效可将日志级别提升到`LogLevel.VERBOSE`做进一步排查。例如:
对于上述的最后一种情况预期Mock未生效使用`@MockDiagnose`注解相应Mock容器类的日志级别提升到`LogLevel.VERBOSE`做进一步排查。例如:
```java
class DemoTest {
class BizServiceTest {
@MockDiagnose(LogLevel.VERBOSE)
public static class Mock {
...
@ -55,39 +49,43 @@ class DemoTest {
}
```
再次执行单元测试,此时将会打印出所有Mock方法的签名定义,以及被测类中扫描到所有调用的实际方法签名:
再次执行单元测试,此时日志将会包含所有该Mock类中的方法签名定义,以及被测类中扫描到所有调用的实际方法签名:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
[VERBOSE] Test case "should_mock_new_object"
[INFO] Found test class com/alibaba/testable/demo/basic/DemoMockTest
[TIP] Test case "should_mock_new_object"
... ...
[VERBOSE] Test case "should_set_mock_context"
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[VERBOSE] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[VERBOSE] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[TIP] Test case "should_set_mock_context"
[INFO] Found 6 test cases
[INFO] Found mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[TIP] Mock constructor "createBlackBox" as "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[TIP] Mock method "innerFunc" as "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
... ...
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
[DIAGNOSE] Handling method <init>
[VERBOSE] Line 13, constructing "java.lang.Object()"
[DIAGNOSE] Handling method newFunc
[VERBOSE] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[DIAGNOSE] Line 19, mock method "createBlackBox" used
[VERBOSE] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
[VERBOSE] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
[DIAGNOSE] Handling method outerFunc
[VERBOSE] Line 27, constructing "java.lang.StringBuilder()"
[VERBOSE] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
[VERBOSE] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[DIAGNOSE] Line 27, mock method "innerFunc" used
[TIP] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[INFO] Found 8 mock methods
[INFO] Found source class com/alibaba/testable/demo/basic/DemoMock
[INFO] Found method <init>
[TIP] Line 13, constructing "java.lang.Object()"
[INFO] Found method newFunc
[TIP] Line 19, constructing "com.alibaba.demo.basic.model.mock.BlackBox(java.lang.String)"
[INFO] Line 19, mock method "createBlackBox" used
[TIP] Line 19, invoking "com.alibaba.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.demo.basic.model.mock.BlackBox"
[TIP] Line 20, invoking "com.alibaba.demo.basic.model.mock.BlackBox::get() : java.lang.String"
[INFO] Found method outerFunc
[TIP] Line 27, constructing "java.lang.StringBuilder()"
[TIP] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
[TIP] Line 27, invoking "com.alibaba.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[INFO] Line 27, mock method "innerFunc" used
... ...
```
输出日志结构参考如下:
- `Mock constructor "<Mock方法名>" as "<方法签名>" for "<类型>"`测试类中扫描到的**Mock构造方法**及其签名
- `Mock method "<Mock方法名>" as "<方法签名>"`测试类中扫描到的**普通Mock方法**及其签名
- `Mock constructor "<Mock方法名>" as "<方法签名>" for "<类型>"`Mock类中扫描到的**Mock构造方法**及其签名
- `Mock method "<Mock方法名>" as "<方法签名>"`Mock类中扫描到的**普通Mock方法**及其签名
- `Line XX, constructing "<类型>" as "<方法签名>"` 在被测类中扫描掉的**构造方法调用**及其签名
- `Line XX, invoking "<方法名>" as "<方法签名>"` 在被测类中扫描到的**成员方法调用**及其签名
> 为了便于清楚的区分返回值类型和调用目标类型,日志中记录的方法签名采用了类似`Kotlin`的方法定义结构。
对比原调用的实际签名和Mock方法定义的签名通常很快就能够找出问题所在。

View File

@ -1,13 +1,15 @@
快速Mock被测类的任意方法调用
---
相比以往Mock工具以类为粒度的Mock方式`TestableMock`允许用户直接定义需要Mock的单个方法并遵循约定优于配置的原则按照规则自动在测试运行时替换被测方法中的指定方法调用
在单元测试中Mock方法的主要作用是替代某些**需要外部依赖**、**执行过程耗时**、**执行结果随机**或其他影响测试正常开展却并不影响关键待测逻辑的调用。通常来说某个调用需要被Mock往往只与其自身特征有关而与调用的来源无关
> 规则归纳起来就两条:
基于上述特点,`TestableMock`设计了一种极简的Mock机制。与以往Mock工具以**类**作为Mock的定义粒度在每个测试用例里各自重复描述Mock行为的方式不同`TestableMock`让每个业务类被测类关联一组可复用的Mock方法集合使用Mock容器类承载并遵循约定优于配置的原则按照规则自动在测试运行时替换被测类中的指定方法调用。
> 实际规则约定归纳起来只有两条:
> - Mock非构造方法拷贝原方法定义到Mock容器类加`@MockMethod`注解
> - Mock构造方法拷贝原方法定义到Mock容器类返回值换成构造的类型方法名随意加`@MockContructor`注解
具体的Mock方法定义约定如下。
具体使用方法如下。
### 0. 前置步骤准备Mock容器
@ -29,11 +31,11 @@ public class DemoTest {
此时被测类中所有对该需覆写方法的调用将在单元测试运行时将自动被替换为对上述自定义Mock方法的调用。
例如,被测类中有一处`"anything".substring(1, 2)`调用我们希望在运行测试的时候将它换成一个固定字符串则只需在Mock容器类定义如下方法
例如,被测类中有一处`"something".substring(0, 4)`调用我们希望在运行测试的时候将它换成一个固定字符串则只需在Mock容器类定义如下方法
```java
// 原方法签名为`String substring(int, int)`
// 调用此方法的对象`"anything"`类型为`String`
// 调用此方法的对象`"something"`类型为`String`
@MockMethod(targetClass = String.class)
private String substring(int i, int j) {
return "sub_string";
@ -55,7 +57,7 @@ private String use_any_mock_method_name(int i, int j) {
有时在Mock方法里会需要访问发起调用的原始对象中的成员变量或是调用原始对象的其他方法。此时可以将`@MockMethod`注解中的`targetClass`参数去除,然后在方法参数列表首位增加一个类型为该方法原本所属对象类型的参数。
`TestableMock`约定,当`@MockMethod`注解的`targetClass`参数值为空Mock方法的首位参数即为目标方法所属类型参数名称随意。通常为了便于代码阅读建议将此参数统一命名为`self`或`src`。举例如下:
`TestableMock`约定,当`@MockMethod`注解的`targetClass`参数未定义Mock方法的首位参数即为目标方法所属类型参数名称随意。通常为了便于代码阅读建议将此参数统一命名为`self`或`src`。举例如下:
```java
// Mock方法在参数列表首位增加一个类型为`String`的参数(名字随意)
@ -67,7 +69,7 @@ private String substring(String self, int i, int j) {
}
```
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_mock_common_method()`测试用例。(由于Kotlin对String类型进行了魔改故Kotlin示例中将被测方法在`BlackBox`类里加了一层封装)
完整代码示例见`java-demo`和`kotlin-demo`示例项目中的`should_mock_common_method()`测试用例。(由于`Kotlin`对String类型进行了魔改`Kotlin`示例中将被测方法在`BlackBox`类里加了一层封装)
### 1.2 覆写被测类自身的成员方法
@ -161,16 +163,15 @@ private Data mockDemo() {
### 3. 验证Mock方法被调用的顺序和参数
在测试用例中可用通过`TestableTool.verify()`方法,配合`with()`、`withInOrder()`、`without()`、`withTimes()`等方法实现对Mock调用情况的验证。
在测试用例中可用通过`InvokeVerifier.verify()`方法,配合`with()`、`withInOrder()`、`without()`、`withTimes()`等方法实现对Mock调用情况的验证。
详见[校验Mock调用](zh-cn/doc/matcher.md)文档。
### 4. 特别说明
> **Mock只对被测类的代码有效**
>
> 在`TestableMock`的[Issues](https://github.com/alibaba/testable-mock/issues)列表中最常见的一类问题是“Mock为什么没生效”其中最多的一种情况是“在测试用例里直接调用了Mock的方法发现没有替换”。这是因为<u>Mock替换只会作用在**被测类**的代码里</u>哦(~ ̄▽ ̄)。知道大家是想快速验证一下`TestableMock`的功能不过测试用例的代码真滴无需被Mock这心意我们领了👻
> 在`TestableMock`的[Issues](https://github.com/alibaba/testable-mock/issues)列表中最常见的一类问题是“Mock为什么没生效”其中最多的一种情况是“在测试用例里直接调用了Mock的方法发现没有替换”。这是因为<u>Mock替换只会作用在**被测类**的代码里</u>。知道大家是想快速验证一下`TestableMock`的功能这心意我们领了👻不过测试用例的代码真的无需被Mock哦(~ ̄▽ ̄)
>
> 除去这种情况若Mock未生效请参考[自助问题排查](zh-cn/doc/troubleshooting.md)提供的方法对比<u>Mock方法签名</u><u>目标位置的调用方法签名</u>。若依然无法定位原因欢迎提交Issues告诉我们。
@ -180,3 +181,5 @@ private Data mockDemo() {
> 同时约定测试类关联的Mock容器为<u>在其内部且名为`Mock`的静态类</u>,或<u>相同包路径下名为`被测类名+Mock`的独立类</u>
>
> 当测试类或Mock容器路径不符合此约定时可使用`@MockWith`注解显式指定,详见[使用MockWith注解](zh-cn/doc/use-mock-with.md)。
关于`TestableMock`的更多实现细节可参考[设计和原理](zh-cn/doc/design-and-mechanism.md)文档。