refactor docs

This commit is contained in:
金戟 2021-02-21 11:58:32 +08:00
parent 84df2a0fd8
commit 48d6ef328b
5 changed files with 52 additions and 33 deletions

View File

@ -45,7 +45,7 @@
| 参数(`N/A`为默认参数) | 类型 | 是否必须 | 默认值 | 作用 |
| --- | --- | --- | ---- | --- |
| N/A | Class | 是 | N/A | 指定使用的Mock容器类 |
| N/A | Class | 否 | NullType.class | 指定使用的Mock容器类 |
| treatAs | ClassType | 否 | ClassType.GuessByName | 指定当前类是测试类或被测类 |
| diagnose | LogLevel | 否 | N/A | (**deprecated**)指定Mock诊断日志级别 |

View File

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

View File

@ -46,4 +46,4 @@ public class DemoMockTest {
}
```
然后将`pom.xml`或`build.gradle`文件中的TestableMock依赖升级到`0.5`或以上版本即可。
然后将`pom.xml`或`build.gradle`文件中的TestableMock依赖升级到`0.5.0`或以上版本即可。

View File

@ -7,9 +7,9 @@
`TestableMock`会依次在以下两个位置寻找Mock容器
- 测试类中名为`Mock`的静态内部类(譬如原类型是`Demo`,测试类是`DemoTest`Mock容器类为`DemoTest.Mock`
- 同包路径下寻找名为`被测类+Mock`的外部类(譬如原类型是`Demo`,测试类是`DemoTest`Mock容器类为`DemoMock`
- 默认位置测试类中名为`Mock`的静态内部类(譬如原类型是`Demo`Mock容器类为`DemoTest.Mock`
- 同包路径下名为`被测类+Mock`的独立类(譬如原类型是`Demo`Mock容器类为`DemoMock`
倘若实际要使用的Mock容器类不在这两个位置就需要在测试类上使用`@MockWith`注释了。一般来说造成Mock容器类不在默认位置的原因可能有两种复用Mock容器、集中管理Mock容器。
当对一批功能近似的类型进行测试的时候由于需要进行Mock的外部调用基本一致可以将这些类型所需的所有Mock方法集中写在一个Mock容器类里然后让相关测试类共同引用这个公共Mock容器。详见[复用Mock类与方法](zh-cn/doc/mock-method-reusing.md)文档。
@ -33,11 +33,30 @@ src/
DemoServiceMock.java
```
此时需要在测试类上显式的指定相应的Mock容器类了,这种场景在实际情况中比较少见。
此时需要在测试类上显式的指定相应的Mock容器类,不过这种情况在实际中并不太常见。
### 2. 非标准位置的测试类
`TestableMock`在建立“被测类”、“测试类”、“Mock容器类”之间关联的过程中为了识别被测类的位置约定测试类应当与被测类在相同的包路径且名称为`被测类+Test`。当实际情况不符合这种约定的时候,就需要通过在**被测类**上添加`@MockWith`注解来显式的建立关联。例如:
从`TestableMock`的原理来说测试类的位置其实只是作为“被测类”与“Mock容器类”之间建立关联的参照物。当测试类的位置不是默认约定的`被测类+Test`时上述首选的Mock容器位置就不成立了。但此时次选Mock位置依然可用即如果Mock容器类的位置是`被测类+Mock`那么Mock置换就依然能够正常进行。
但此时测试类与Mock容器之间的关联丢失了因此需要为测试类使用`@MockWith`注解来显式的建立关联。例如:
```java
public class DemoService { // 被测类
...
}
public class DemoServiceMock { // Mock容器类
...
}
@MockWith(DemoServiceMock.class) // 测试类由于丢失与Mock容器的关联需要@MockWith注解
public class ServiceTest {
...
}
```
另一种相对少见的情况是Mock容器采用测试类的静态内部类但测试类由于某些原因无法置于约定位置或无法遵循约定命名。此时测试类与Mock类的关联能够自动建立但被测类无法找到自己的Mock容器因此需要在**被测类**上添加`@MockWith`注解来显式的建立关联。例如:
```java
@MockWith(ServiceTest.Mock.class)
@ -52,15 +71,27 @@ public class ServiceTest {
}
```
> 注意:
> 1. `@MockWith`注解通常是使用在测试类上,但这种情况下需要用在被测类上
> 2. `@MockWith`指向的目标始终是Mock容器类而不是测试类
> 3. `@MockWith`默认使用被注解类名字的尾缀判断当前类是被测类(名字非`Test`结尾)还是测试类(名字`Test`结尾),若遇到不符合此规则的类型,应使用注解的`treatAs`参数显式的指定(`ClassType.SourceClass`-被测类/`ClassType.TestClass`-测试类)
对于更特殊的一种情况即测试类与Mock容器类均不在约定位置的时候则需要同时在**测试类**与**被测类**上都使用`@MockWith`指向同一个Mock容器类来建立三者的关联。复杂的关联对代码阅读会造成一定不便在实际运用中应当尽量避免这种情况发生。
更进一步来说若出现测试类与Mock容器类均不在约定位置的时候则需要同时在**测试类**与**被测类**上都使用`@MockWith`指向同一个Mock容器类来建立三者的关联但这种场景在实际运用中很少会遇到。
> 特别说明:`@MockWith`默认使用被注解类名字的尾缀判断当前类是被测类(名字非`Test`结尾)还是测试类(名字`Test`结尾),若遇到不符合此规则的类型,应使用注解的`treatAs`参数显式的指定(`ClassType.SourceClass`-被测类/`ClassType.TestClass`-测试类)
### 3. 使用不包含Mock方法的Mock容器类
> Q为何当测试类在非约定位置时是在被测类上使用`@MockWith`,而不在测试类上指定被测类?
>
> A从原则上来说凡是能只改测试类就实现的肯定不应该为了测试而去动业务代码被测类
> 然而由于JavaAgent只能在类首次加载进内存的时候进行字节码处理实际情况无法保证被测的类一定在测试类之后加载可能在其他测试用例执行的时候就被提前加载进内存了等读取到测试类上的信息时可能已经无法对被测类进行Mock处理。因此对于测试类和被测类相互不知道对方位置的情况采用了两边都用`@MockWith`指定Mock容器类的折中设计。
### 3. 在一个测试类中测试多个被测类
这是非标准位置测试类的一种特殊情况,当一个测试类里同时测试了多个业务类(被测类),其名称要么只能与其中某个被测类有`+Test`的命名符合,要么不与其中任何一个被测类有命名相关性。
假设所有被测类的Mock容器均采用`被测类+Mock`约定命名(否则参考前一条规则,被测类也需要显式加`@MockWith`)。若该测试类本身命名不符合其中任何一个被测类+Test约定的情况需要为该测试类加一个无参数的`@MockWith`注解(即使用默认值,相当于`@MockWith(NullType.class)`),用于标识此类需参与`TestableMock`的预处理。
完整代码示例见`java-demo`和`kotlin-demo`示例项目中`OneToMultiSvcTest`测试类的用例。
> 由于当前版本里,测试类无法通过`@MockWith`与多个Mock容器关联对生效范围为`MockScope.ASSOCIATED`的Mock方法会遇到Mock无效的情况为已知BUG将在未来版本中修复。
### 4. 使用不包含Mock方法的Mock容器类
为了加快搜索Mock容器类的速度在扫描过程中`TestableMock`只会将自身定义有Mock方法包含`@MockMethod`或`@MockMockConstructor`注解的方法)以及明确被`@MockWith`指向的类识别为有效的Mock容器而不会去遍历其父类。
对于某些特殊情况譬如希望将实际Mock方法均定义在父类实际使用的子容器仅仅重载父类的某些特定方法此时即使Mock容器类的位置符合约定为了能够被识别依然应该在相应的测试类上增加对Mock容器类的`@MockWith`引用。
假如出于某些极特殊原因要使用无Mock方法的类型作为Mock容器譬如希望将实际Mock方法均定义在父类实际使用的子容器仅仅重载父类的某些特定方法此时即使Mock容器类的位置符合约定为了能够被识别依然应该在相应的测试类上增加对Mock容器类的`@MockWith`引用。

View File

@ -25,11 +25,11 @@ public class DemoTest {
#### 1. 覆写任意类的方法调用
在Mock类中定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,并在注解的`targetClass`参数指定该方法原本所属对象类型。
在Mock容器类中定义一个有`@MockMethod`注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,并在注解的`targetClass`参数指定该方法原本所属对象类型。
此时被测类中所有对该需覆写方法的调用将在单元测试运行时将自动被替换为对上述自定义Mock方法的调用。
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在测试类定义如下方法:
例如,被测类中有一处`"anything".substring(1, 2)`调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在Mock容器类定义如下方法:
```java
// 原方法签名为`String substring(int, int)`
@ -75,7 +75,7 @@ private String substring(String self, int i, int j) {
做法与前一种情况完全相同,只需将`targetClass`参数赋值为被测类,即可实现对被测类自身(不论是公有或私有)成员方法的覆写。
例如,被测类中有一个签名为`String innerFunc(String)`的私有方法,我们希望在测试的时候将它替换掉,则只需在测试类定义如下方法:
例如,被测类中有一个签名为`String innerFunc(String)`的私有方法,我们希望在测试的时候将它替换掉,则只需在Mock容器类定义如下方法:
```java
// 被测类型是`DemoMock`
@ -108,7 +108,7 @@ private BlackBox secretBox() {
#### 4. 覆写任意类的new操作
测试类里定义一个返回值类型为要被创建的对象类型且方法参数与要Mock的构造函数参数完全一致的方法名称随意然后加上`@MockContructor`注解。
Mock容器类里定义一个返回值类型为要被创建的对象类型且方法参数与要Mock的构造函数参数完全一致的方法名称随意然后加上`@MockContructor`注解。
此时被测类中所有用`new`创建指定类的操作并使用了与Mock方法参数一致的构造函数将被替换为对该自定义方法的调用。