diff --git a/docs/en-us/doc/private-accessor.md b/docs/en-us/doc/private-accessor.md index 9cc2914..2024734 100644 --- a/docs/en-us/doc/private-accessor.md +++ b/docs/en-us/doc/private-accessor.md @@ -18,6 +18,13 @@ When accessing and modifying private and constant members, the IDE may prompt so For the effect, see the use case in the test class of the `java-demo` sample project `DemoPrivateAccessTest`. (Using compile-time code enhancement, currently only the adaptation of the Java language is implemented) +> This function assumes that the test class is in the same package as the class under test, and the name is `+Test`. When this convention is not met, you can use the `srcClass` parameter on the `@EnablePrivateAccess` annotation to specify the actual class under test. E.g: +> +> ```java +> @EnablePrivateAccess(srcClass = DemoServiceImpl.class) +> class DemoServiceTest() { ... } +> ``` + ### Solution 2: Use the `PrivateAccessor` tool class If you don't want to see the IDE's syntax error reminder, or in a non-Java language JVM project (such as Kotlin language), you can also use the `PrivateAccessor` tool class to directly access private members. @@ -31,4 +38,14 @@ This class provides 6 static methods: - `PrivateAccessor.setStatic(, "", )` ➜ modify the **static** private field (or **static** constant field) of the class under test - `PrivateAccessor.invokeStatic(, "", ..)` ➜ call the **static** private method of the class under test +Using the `PrivateAccessor` class does not require the test class to have `@EnablePrivateAccess` annotation, but adding this annotation will enable the compile-time verification for the private members of the class under test, and it is usually recommended to use together. + For details, see the use cases in the test classes of the `java-demo` and `kotlin-demo` sample projects `DemoPrivateAccessTest`. + +### Compile-time verification of private members + +Both of the above two methods essentially use JVM reflection mechanism to achieve private member access, and the JVM compiler does not check the existence of the reflection target. Thus, if the private method names or parameters are modified duration future refactor, it may cause unintuitive errors when the unit test is running. To this end, `TestableMock` provides additional compile-time checks for the private targets accessed. + +The compile-time verification function is enabled by the `@EnablePrivateAccess` annotation, which takes effect by default for the case of private members accessed using `Solution 1`, and is disabled by default for the case of accessing private members via `Solution 2` (To enable it, give the test class an `@EnablePrivateAccess` annotation). + +> The compile-time verification function of `@EnablePrivateAccess` can be turned off manually, just set the `verifyTargetOnCompile` parameter of the annotation to `false`. diff --git a/docs/en-us/doc/troubleshooting.md b/docs/en-us/doc/troubleshooting.md index 1022934..97beb5a 100644 --- a/docs/en-us/doc/troubleshooting.md +++ b/docs/en-us/doc/troubleshooting.md @@ -16,25 +16,19 @@ The output log example is as follows: ```text [DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest +[DIAGNOSE] Found 8 mock methods [DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock -[DIAGNOSE] Found 7 mock methods [DIAGNOSE] Handling method [DIAGNOSE] Handling method newFunc -[DIAGNOSE] Line 14, mock method createBlackBox used +[DIAGNOSE] Line 19, mock method "createBlackBox" used [DIAGNOSE] Handling method outerFunc -[DIAGNOSE] Line 22, mock method innerFunc used +[DIAGNOSE] Line 27, mock method "innerFunc" used +[DIAGNOSE] Line 27, mock method "staticFunc" used [DIAGNOSE] Handling method commonFunc -[DIAGNOSE] Line 29, mock method trim used -[DIAGNOSE] Line 29, mock method sub used -[DIAGNOSE] Line 29, mock method startsWith used -[DIAGNOSE] Handling method getBox -[DIAGNOSE] Line 36, mock method secretBox used -[DIAGNOSE] Handling method callerOne -[DIAGNOSE] Line 43, mock method callFromDifferentMethod used -[DIAGNOSE] Handling method callerTwo -[DIAGNOSE] Line 47, mock method callFromDifferentMethod used -[DIAGNOSE] Handling method innerFunc -[DIAGNOSE] Handling method callFromDifferentMethod +[DIAGNOSE] Line 34, mock method "trim" used +[DIAGNOSE] Line 34, mock method "sub" used +[DIAGNOSE] Line 34, mock method "startsWith" used +... ... ``` The log shows all the mocked invocation and corresponding code line numbers in the class under test. @@ -44,3 +38,55 @@ The log shows all the mocked invocation and corresponding code line numbers in t - If there is no output, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies - If only the first line of `Handling test class` is output, please check whether the test class is in the same package of the class under test, and the name is "+Test" (required for `0.4.x` version) - 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 `MockDiagnose.VERBOSE` for further investigation information. + +```java +@MockWith(diagnose = MockDiagnose.VERBOSE) +class DemoTest { + ... +} +``` + +Executing the unit test again will print out the runtime-signatures of all mock methods, and the runtime-signatures of all invocations scanned in the class under test: + +```text +[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest +[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 +[DIAGNOSE] Handling method +[VERBOSE] Line 13, constructing "java/lang/Object" as "()V" +[DIAGNOSE] Handling method newFunc +[VERBOSE] Line 19, constructing "com/alibaba/testable/demo/model/BlackBox" as "(Ljava/lang/String;)V" +[DIAGNOSE] Line 19, mock method "createBlackBox" used +[VERBOSE] Line 19, invoking "createBlackBox" as "(Ljava/lang/String;)Lcom/alibaba/testable/demo/model/BlackBox;" +[VERBOSE] Line 20, invoking "get" as "()Ljava/lang/String;" +[DIAGNOSE] Handling method outerFunc +[VERBOSE] Line 27, constructing "java/lang/StringBuilder" as "()V" +[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;" +... ... +``` + +The logs are formatted in follow pattern: + +- `Mock constructor "" as "" for ""` Mock constructor found in test class +- `Mock method "" as ""` Mock method found in test class (the first parameter that identify the mock target class is currently kept) +- `Line XX, constructing "" as ""` Constructor invocation found in test under class +- `Line XX, invoking "" as ""` Member method invocation found in test under class diff --git a/docs/zh-cn/doc/private-accessor.md b/docs/zh-cn/doc/private-accessor.md index 9362a68..9b83332 100644 --- a/docs/zh-cn/doc/private-accessor.md +++ b/docs/zh-cn/doc/private-accessor.md @@ -18,6 +18,13 @@ 效果见`java-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。 +> 此功能默认假设测试类与被测类同包,且名称为`被测类+Test`。当不符合此约定时,可在测试类的`@EnablePrivateAccess`注解上使用`srcClass`参数指定实际的被测类。例如: +> +> ```java +> @EnablePrivateAccess(srcClass = DemoServiceImpl.class) +> class DemoServiceTest() { ... } +> ``` + ### 方法二:使用`PrivateAccessor`工具类 若不希望看到IDE的语法错误提醒,或是在非Java语言的JVM工程(譬如Kotlin语言)里,也可以借助`PrivateAccessor`工具类来直接访问私有成员。 @@ -31,4 +38,14 @@ - `PrivateAccessor.setStatic(被测类型, "私有静态字段名", 新的值)` ➜ 修改被测类的**静态**私有字段(或**静态**常量字段) - `PrivateAccessor.invokeStatic(被测类型, "私有静态方法名", 调用参数..)` ➜ 调用被测类的**静态**私有方法 +使用`PrivateAccessor`工具类并不需要测试类具有`@EnablePrivateAccess`注解,但加上此注解将开启被测类私有成员的编译期校验功能,通常建议搭配使用。 + 详见`java-demo`和`kotlin-demo`示例项目`DemoPrivateAccessTest`测试类中的用例。 + +### 私有成员编译期校验 + +以上两种方式本质上都是利用JVM的反射机制实现了私有成员访问,JVM编译器不会检查反射目标的存在性。当代码重构时,如果对源类型中的私有方法名称、参数进行了修改,可能导致在单元测试运行时出现较不直观的异常错误。为此,`TestableMock`对访问的私有目标提供了额外的编译期校验。 + +编译期校验功能通过`@EnablePrivateAccess`注解开启,对使用`方法一`访问的私有成员的情况默认生效,而对通过`方法二`访问私有成员的情况默认关闭(若要启用,给测试类加上`@EnablePrivateAccess`注解即可)。 + +> `@EnablePrivateAccess`注解的编译期校验功能可以手工关闭,只需将注释的`verifyTargetOnCompile`参数设为`false`。 diff --git a/docs/zh-cn/doc/troubleshooting.md b/docs/zh-cn/doc/troubleshooting.md index 721f2f7..3855716 100644 --- a/docs/zh-cn/doc/troubleshooting.md +++ b/docs/zh-cn/doc/troubleshooting.md @@ -16,25 +16,19 @@ class DemoTest { ```text [DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest +[DIAGNOSE] Found 8 mock methods [DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock -[DIAGNOSE] Found 7 mock methods [DIAGNOSE] Handling method [DIAGNOSE] Handling method newFunc -[DIAGNOSE] Line 14, mock method createBlackBox used +[DIAGNOSE] Line 19, mock method "createBlackBox" used [DIAGNOSE] Handling method outerFunc -[DIAGNOSE] Line 22, mock method innerFunc used +[DIAGNOSE] Line 27, mock method "innerFunc" used +[DIAGNOSE] Line 27, mock method "staticFunc" used [DIAGNOSE] Handling method commonFunc -[DIAGNOSE] Line 29, mock method trim used -[DIAGNOSE] Line 29, mock method sub used -[DIAGNOSE] Line 29, mock method startsWith used -[DIAGNOSE] Handling method getBox -[DIAGNOSE] Line 36, mock method secretBox used -[DIAGNOSE] Handling method callerOne -[DIAGNOSE] Line 43, mock method callFromDifferentMethod used -[DIAGNOSE] Handling method callerTwo -[DIAGNOSE] Line 47, mock method callFromDifferentMethod used -[DIAGNOSE] Handling method innerFunc -[DIAGNOSE] Handling method callFromDifferentMethod +[DIAGNOSE] Line 34, mock method "trim" used +[DIAGNOSE] Line 34, mock method "sub" used +[DIAGNOSE] Line 34, mock method "startsWith" used +... ... ``` 该日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。 @@ -42,5 +36,57 @@ class DemoTest { 简单排查方法: - 若没有任何输出,请检查`pom.xml`或`build.gradle`配置是否正确引入了TestableMock依赖 -- 若只输出了第一行`Handling test class`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test"(`0.4.x`版本要求) +- 若只输出了`Handling test class`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test"(`0.4.x`版本要求) - 若输出了`Handling source class`以及`Handling method xxx`,但预期的代码行位置没有发生Mock替换,请检查Mock方法定义是否未与目标方法匹配 + +对于预期Mock未生效的情况,如需进一步排查,可将日志级别提升到`MockDiagnose.VERBOSE`。 + +```java +@MockWith(diagnose = MockDiagnose.VERBOSE) +class DemoTest { + ... +} +``` + +再次执行单元测试,将会打印出所有Mock方法的运行期签名,以及被测类中扫描到所有调用的运行期签名: + +```text +[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest +[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 +[DIAGNOSE] Handling method +[VERBOSE] Line 13, constructing "java/lang/Object" as "()V" +[DIAGNOSE] Handling method newFunc +[VERBOSE] Line 19, constructing "com/alibaba/testable/demo/model/BlackBox" as "(Ljava/lang/String;)V" +[DIAGNOSE] Line 19, mock method "createBlackBox" used +[VERBOSE] Line 19, invoking "createBlackBox" as "(Ljava/lang/String;)Lcom/alibaba/testable/demo/model/BlackBox;" +[VERBOSE] Line 20, invoking "get" as "()Ljava/lang/String;" +[DIAGNOSE] Handling method outerFunc +[VERBOSE] Line 27, constructing "java/lang/StringBuilder" as "()V" +[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 "" as "<方法签名>" for "<类型>"` 在测试类中扫描到的**Mock构造方法**及其运行时签名 +- `Mock method "" as "<方法签名>"` 在测试类中扫描到的**普通Mock方法**及其运行时签名(打印时暂未自动排除用于指定Mock目标类的首位参数) +- `Line XX, constructing "<类型>" as "<方法签名>"` 在被测类中扫描掉的**构造方法调用**及其运行时签名 +- `Line XX, invoking "<方法名>" as "<方法签名>"` 在被测类中扫描到的**成员方法调用**及其运行时签名