目录

一、TestableMock概述

二、​快速Mock被测类的任意方法调用​

三、覆写任意类的方法调用

四、覆写任意类的new操作

五、在Mock方法中区分调用来源

六、注意点


一、TestableMock概述

TestableMock是阿里云效团队开发的一款快速Mock的单元测试框架,旨在"让Java没有难测的代码"。

TestableMock现在已不仅是一款轻量易上手的单元测试Mock工具,更是以简化Java单元测试为目标的综合辅助工具集,包含以下功能:

  • 快速Mock任意调用:使被测类的任意方法调用快速替换为Mock方法,实现"指哪换哪",解决传统Mock工具使用繁琐的问题;
  • 访问被测类私有成员:使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题;
  • 快速构造参数对象:生成任意复杂嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题;
  • 辅助测试void方法:利用Mock校验器对方法的内部逻辑进行检查,解决无返回值方法难以实施单元测试的问题;

二、​快速Mock被测类的任意方法调用​

在单元测试中,Mock方法的主要作用是替代某些需要外部依赖、执行过程耗时、执行结果随机或其他影响测试正常开展,却并不影响关键待测逻辑的调用。通常来说,某个调用需要被Mock,往往只与其自身特征有关,而与调用的来源无关。

基于上述特点,TestableMock设计了一种极简的Mock机制。与以往Mock工具以作为Mock的定义粒度,在每个测试用例里各自重复描述Mock行为的方式不同,TestableMock让每个业务类(被测类)关联一组可复用的Mock方法集合(使用Mock容器类承载),并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测类中的指定方法调用。

实际规则约定归纳起来只有两条:

  • Mock非构造方法,拷贝原方法定义到Mock容器类,加@MockMethod注解;
  • Mock构造方法,拷贝原方法定义到Mock容器类,返回值换成构造的类型,方法名随意,加@MockContructor注解;

具体使用方法如下。

【a】新建一个SpringBoot项目或者Maven项目,pom.xml加入Testable的依赖,以及Maven插件:

笔者选择使用0.6.8版本的testable

<properties><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.8</testable.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.alibaba.testable</groupId><artifactId>testable-all</artifactId><version>${testable.version}</version><scope>test</scope></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>${junit.version}</version><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.0.0-M5</version><configuration><argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine></configuration></plugin><!--            <plugin>--><!--                <groupId>com.alibaba.testable</groupId>--><!--                <artifactId>testable-maven-plugin</artifactId>--><!--                <version>${testable.version}</version>--><!--                <executions>--><!--                    <execution>--><!--                        <id>prepare</id>--><!--                        <goals>--><!--                            <goal>prepare</goal>--><!--                        </goals>--><!--                    </execution>--><!--                </executions>--><!--            </plugin>--><plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.6</version><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>prepare-package</phase><goals><goal>report</goal></goals><configuration><dataFile>target/jacoco.exec</dataFile><outputDirectory>target/jacoco-ut</outputDirectory></configuration></execution></executions></plugin></plugins></build>

【b】新建一些实体类,后续使用

package com.wsh.testable.mock.testablemockdemo;public interface Color {String getColor();}
package com.wsh.testable.mock.testablemockdemo;abstract public class Box {protected String data;abstract public void put(String something);public String get() {return data;}}
package com.wsh.testable.mock.testablemockdemo;public class BlackBox extends Box implements Color {public BlackBox(String data) {this.data = data;}public static BlackBox secretBox() {return new BlackBox("secret");}@Overridepublic void put(String something) {data = something;}@Overridepublic String getColor() {return "black";}}

【c】编写被测试类

注意包名:com.wsh.testable.mock.testablemockdemo;

package com.wsh.testable.mock.testablemockdemo;import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Collectors;/*** TestableMock: 基本的Mock功能*/
public class DemoMock {/*** method with new operation*/public String newFunc() {BlackBox component = new BlackBox("something");return component.get();}/*** method with member method invoke*/public String outerFunc(String s) throws Exception {return "{ \"res\": \"" + innerFunc(s) + staticFunc() + "\"}";}/*** method with common method invoke*/public String commonFunc() {return "anything".trim() + "__" + "anything".substring(1, 2) + "__" + "abc".startsWith("ab");}/*** method with static method invoke*/public BlackBox getBox() {return BlackBox.secretBox();}/*** two methods invoke same private method*/public String callerOne() {return callFromDifferentMethod();}public String callerTwo() {return callFromDifferentMethod();}private static String staticFunc() {return "_STATIC_TAIL";}private String innerFunc(String s) throws Exception {return Files.readAllLines(Paths.get("/a-not-exist-file")).stream().collect(Collectors.joining());}private String callFromDifferentMethod() {return "realOne";}}

【d】编写测试类

注意包名:com.wsh.testable.mock.testablemockdemo;

package com.wsh.testable.mock.testablemockdemo;import com.alibaba.testable.core.annotation.MockConstructor;
import com.alibaba.testable.core.annotation.MockMethod;
import org.junit.jupiter.api.Test;import java.util.concurrent.Executors;import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
import static com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT;
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;
import static org.junit.jupiter.api.Assertions.assertEquals;/*** 演示基本的Mock功能*/
class DemoMockTest {private DemoMock demoMock = new DemoMock();/*** 承载Mock方法的容器,用于存放所有Mock方法,最简单的做法是在测试类里添加一个名称为Mock的静态内部类*/public static class Mock {@MockConstructorprivate BlackBox createBlackBox(String text) {return new BlackBox("mock_" + text);}@MockMethod(targetClass = DemoMock.class)private String innerFunc(String text) {return "mock_" + text;}@MockMethod(targetClass = DemoMock.class)private String staticFunc() {return "_MOCK_TAIL";}@MockMethod(targetClass = String.class)private String trim() {return "trim_string";}@MockMethod(targetClass = String.class, targetMethod = "substring")private String sub(int i, int j) {return "sub_string";}@MockMethod(targetClass = String.class)private boolean startsWith(String s) {return false;}@MockMethod(targetClass = BlackBox.class)private BlackBox secretBox() {return new BlackBox("not_secret_box");}@MockMethod(targetClass = DemoMock.class)private String callFromDifferentMethod() {if ("special_case".equals(MOCK_CONTEXT.get("case"))) {return "mock_special";}//在Mock方法中通过TestableTool.SOURCE_METHOD变量可以识别进入该Mock方法前的被测类方法名称switch (SOURCE_METHOD) {case "callerOne":return "mock_one";default:return "mock_others";}}}/*** 1. Mock构造方法* <p>* 解析:* a.Mock容器类中使用@MockConstructor注解Mock了BlackBox构造方法,* b.然后newFunc()方法在被测试类()中实现为:new BlackBox("something");* c.故这里demoMock.newFunc(),实际上调用的构造方法是Mock容器类里面的Mock的createBlackBox(String text)方法*/@Testvoid should_mock_new_object() {assertEquals("mock_something", demoMock.newFunc());//检查在执行被测方法newFunc()时,名称是createBlackBox的Mock方法是否有被调用过,且调用时收到的参数值是否为"something"verify("createBlackBox").with("something");}/*** 2. Mock成员方法* 解析:* a.demoMock.outerFunc("hello")方法在被测类DemoMock中分别调用了innerFunc(s) + staticFunc()两个方法* b.在Mock容器类中分别Mock了innerFunc(s) + staticFunc()这两个方法;* c.所以在真正运行单元测试的时候,实际上调用的是Mock里面innerFunc(String text)和staticFunc()方法*/@Testvoid should_mock_member_method() throws Exception {assertEquals("{ \"res\": \"mock_hello_MOCK_TAIL\"}", demoMock.outerFunc("hello"));//检查在执行被测方法outerFunc()时,名称是innerFunc的Mock方法是否有被调用过,且调用时收到的参数值是否为"hello"verify("innerFunc").with("hello");//检查在执行被测方法outerFunc()时,名称是staticFunc的Mock方法是否有被调用过verify("staticFunc").with();}/*** 3. Mock公共方法* 解析:* a.demoMock.commonFunc()方法在被测类DemoMock中分别调用了trim()、substring(1, 2)、startsWith("ab")三个方法* b.在Mock容器类中也分别Mock了String类的trim()、substring(1, 2)、startsWith("ab")三个方法* c.所以在真正运行单元测试的时候,实际上调用的是Mock里面trim()、sub(int i, int j)、startsWith(String s)方法*/@Testvoid should_mock_common_method() {assertEquals("trim_string__sub_string__false", demoMock.commonFunc());//检查在执行被测方法commonFunc()时,名称是trim的Mock方法是否有被调用过一次,并忽略对调用参数的检查verify("trim").withTimes(1);//检查在执行被测方法commonFunc()时,名称是sub的Mock方法是否有被调用过一次,并忽略对调用参数的检查verify("sub").withTimes(1);//检查在执行被测方法commonFunc()时,名称是startsWith的Mock方法是否有被调用过一次,并忽略对调用参数的检查verify("startsWith").withTimes(1);}/*** 4. Mock静态方法* 解析:* a. demoMock.getBox()原来的实现是调用了BlackBox.secretBox();* b. 在Mock容器类中Mock了BlackBox类的secretBox()方法* c. 所以运行时真正调用的是Mock容器类里面的Mock#secretBox()方法*/@Testvoid should_mock_static_method() {assertEquals("not_secret_box", demoMock.getBox().get());//检查在执行被测方法getBox()时,名称是secretBox的Mock方法是否有被调用过一次,并忽略对调用参数的检查verify("secretBox").withTimes(1);}/*** 5.SOURCE_METHOD:识别进入Mock方法前的被测类方法名称* 解析:* a. demoMock.callerOne()/demoMock.callerTwo()在被测试类中都调用了callFromDifferentMethod()方法* b. 在测试类Mock容器中Mock了callFromDifferentMethod()方法,并通过TestableTool.SOURCE_METHOD识别了进入Mock方法前的被测类方法名称*/@Testvoid should_get_source_method_name() throws Exception {// synchronous同步调用assertEquals("mock_one_mock_others", demoMock.callerOne() + "_" + demoMock.callerTwo());// asynchronous异步调用assertEquals("mock_one_mock_others",Executors.newSingleThreadExecutor().submit(() -> demoMock.callerOne() + "_" + demoMock.callerTwo()).get());//检查在执行被测方法callerOne()/callerTwo()时,名称是callFromDifferentMethod的Mock方法是否有被调用过四次,并忽略对调用参数的检查verify("callFromDifferentMethod").withTimes(4);}/*** 6. MOCK_CONTEXT:Mock额外的上下文参数* 解析:* a. demoMock.callerOne()在被测试类中调用了callFromDifferentMethod()方法* b. 在Mock容器中Mock了callFromDifferentMethod()方法,通过MOCK_CONTEXT标注了上下文参数为”special_case“* c. Mock#callFromDifferentMethod()方法中,如果是”special_case“,直接返回"mock_special"*/@Testvoid should_set_mock_context() throws Exception {MOCK_CONTEXT.put("case", "special_case");// synchronousassertEquals("mock_special", demoMock.callerOne());// asynchronousassertEquals("mock_special", Executors.newSingleThreadExecutor().submit(() -> demoMock.callerOne()).get());//检查在执行被测方法callerOne()时,名称是callFromDifferentMethod的Mock方法是否有被调用过二次,并忽略对调用参数的检查verify("callFromDifferentMethod").withTimes(2);}}

如上我们可以看到,我们为测试类添加一个关联的Mock类型,作为承载其Mock方法的容器,做法是在测试类里添加一个名称为Mock的静态内部类。并且我们使用@MockMethod注解定义了一些Mock方法,在每个单元测试中我都写了比较详细的解析步骤,应该还是比较容易理解的。

注意被测试类和测试类的包名尽量保持一致,Testable框架默认的规则是需要保持一致的,当然如果真不一致,可以参照官网进行一些调整也可以扫描出,日常开发没什么特殊情况,我们保持一致即可。

三、覆写任意类的方法调用

在Mock容器类中定义一个有@MockMethod注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,并在注解的targetClass参数指定该方法原本所属对象类型。

此时被测类中所有对该需覆写方法的调用,将在单元测试运行时,将自动被替换为对上述自定义Mock方法的调用。

例如,被测类中有一处"something".substring(0, 4)调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在Mock容器类定义如下方法:

// 原方法签名为`String substring(int, int)`
// 调用此方法的对象`"something"`类型为`String`
@MockMethod(targetClass = String.class)
private String substring(int i, int j) {return "sub_string";
}

当遇到待覆写方法有重名时,可以将需覆写的方法名写到@MockMethod注解的targetMethod参数里,这样Mock方法自身就可以随意命名了。如下:

// 使用`targetMethod`指定需Mock的方法名
// 此方法本身现在可以随意命名,但方法参数依然需要遵循相同的匹配规则
@MockMethod(targetClass = String.class, targetMethod = "substring")
private String use_any_mock_method_name(int i, int j) {return "sub_string";
}

有时,在Mock方法里会需要访问发起调用的原始对象中的成员变量,或是调用原始对象的其他方法。此时,可以将@MockMethod注解中的targetClass参数去除,然后在方法参数列表首位增加一个类型为该方法原本所属对象类型的参数

TestableMock约定,当@MockMethod注解的targetClass参数未定义时,Mock方法的首位参数即为目标方法所属类型,参数名称随意。通常为了便于代码阅读,建议将此参数统一命名为selfsrc。举例如下:

// Mock方法在参数列表首位增加一个类型为`String`的参数(名字随意)
// 此参数可用于获得当时的实际调用者的值和上下文
@MockMethod
private String substring(String self, int i, int j) {// 可以直接调用原方法,此时Mock方法仅用于记录调用,常见于对void方法的测试return self.substring(i, j);
}

Mock静态方法、成员方法的规则大体与上述类似,只是targetClass参数指定为需要重写的类类型。

四、覆写任意类的new操作

在Mock容器类里定义一个返回值类型为要被创建的对象类型,且方法参数与要Mock的构造函数参数完全一致的方法,名称随意,然后加上@MockContructor注解。

此时被测类中所有用new创建指定类的操作(并使用了与Mock方法参数一致的构造函数)将被替换为对该自定义方法的调用。

例如,在被测类中有一处new BlackBox("something")调用,希望在测试时将它换掉(通常是换成Mock对象,或换成使用测试参数创建的临时对象),则只需定义如下Mock方法:

// 要覆写的构造函数签名为`BlackBox(String)`
// Mock方法返回`BlackBox`类型对象,方法的名称随意起
@MockContructor
private BlackBox createBlackBox(String text) {return new BlackBox("mock_" + text);
}

五、在Mock方法中区分调用来源

在Mock方法中通过TestableTool.SOURCE_METHOD变量可以识别进入该Mock方法前的被测类方法名称;此外,还可以借助TestableTool.MOCK_CONTEXT变量为Mock方法注入“额外的上下文参数”,从而区分处理不同的调用场景。

例如,在测试用例中验证当被Mock方法返回不同结果时,对被测目标方法的影响:

@Test
public void testDemo() {MOCK_CONTEXT.put("case", "data-ready");assertEquals(true, demo());MOCK_CONTEXT.put("case", "has-error");assertEquals(false, demo());
}

在Mock方法中取出注入的参数,根据情况返回不同结果:

@MockMethod
private Data mockDemo() {switch((String)MOCK_CONTEXT.get("case")) {case "data-ready":return new Data();case "has-error":throw new NetworkException();default:return null;}
}

六、注意点

Mock只对被测类的代码有效

TestableMock的Issues列表中,最常见的一类问题是“Mock为什么没生效”,其中最多的一种情况是“在测试用例里直接调用了Mock的方法,发现没有替换”。这是因为Mock替换只会作用在被测类的代码里

测试类和Mock容器的命名约定

默认情况下,TestableMock假设测试类与被测类的包路径相同,且名称为被测类名+Test(通常采用MavenGradle构建的Java项目均符合这种惯例)。 同时约定测试类关联的Mock容器为在其内部且名为Mock的静态类,或相同包路径下名为被测类名+Mock的独立类

当测试类或Mock容器路径不符合此约定时,可使用@MockWith注解显式指定,详见使用MockWith注解。

本文大部分内容来源于TestableMock官方文档学习总结,感兴趣的小伙伴可以前往TestableMock官网学习更多功能的使用:TestableMock

单元测试框架TestableMock快速入门(一):快速Mock任意调用相关推荐

  1. python qt快速入门_PyQt5快速入门(一)

    PyQt5快速入门(一) 前言 为什么选择PyQt5作为GUI框架? API与Qt一致, 学会PyQt后再使用qt很简单 开发迅速, 可视化操作,使用designer快速拖拽布局进行调试 可以将文件打 ...

  2. python的快速入门-Python快速入门,你想要的就在这里了!

    原标题:Python快速入门,你想要的就在这里了! 学习Python您是否会面临以下问题?"网上充斥着大量的学习资源.书籍.视频教程和博客,但是大部分都是讲解基础知识,不够深入:也有的比较晦 ...

  3. python快速入门 pdf-Python快速入门 (第3版) PDF 下载

    相关截图: 资料简介: 这是一本Python快速入门书,基于Python 3.6编写.本书分为4部分,*部分讲解Python的基础知识,对Python进行概要的介绍:第二部分介绍Python编程的重点 ...

  4. Sers微服务快速入门-02.快速接入

    微服务给我们的第一映像是架构复杂,部署起来麻烦.其实并非如此,不同的架构选型必然带来不同的优点和缺点,没有一劳永逸的方法,配置简单是因为适用的场景面窄.在项目或产品的实际开发中往往随着时间的推进需要实 ...

  5. java redis快速入门_快速入门Redis系列(3)——Redis的JavaAPI操作(附带练习)

    作为快速入门Redis系列的第三篇博客,本篇为大家带来的是Redis的JavaAPI操作. 码字不易,先赞后看! Redis的JavaAPI操作 看完了上一篇博客,相信大家对于Redis的数据类型有了 ...

  6. java 快速入门_Java快速入门-01-基础篇

    Java快速入门-01-基础篇 如果基础不好或者想学的很细,请参看:菜鸟教程-JAVA 本笔记适合快速学习,文章后面也会包含一些常见面试问题,记住快捷键操作,一些内容我就不转载了,直接附上链接,嘻嘻 ...

  7. RabbitMQ快速入门 | 帮助快速上手

    ♨️本篇文章记录的为RabbitMQ知识中快速入门相关内容,适合在学Java的小白,帮助新手快速上手,也适合复习中,面试中的大佬

  8. python快速入门 pdf-Python 快速入门 PDF 第3版

    给大家带来的一篇关于Python入门相关的电子书资源,介绍了关于Python.Python入门方面的内容,本书是由人民邮电出版社出版,格式为PDF,资源大小9.73 MB,娜奥米·塞德编写,目前豆瓣. ...

  9. SQL快速入门 ( MySQL快速入门, MySQL参考, MySQL快速回顾 )

    SQL 先说点废话,很久没发文了,整理了下自己当时入门 SQL 的笔记,无论用于入门,回顾,参考查询,应该都是有一定价值的,可以按照目录各取所需.SQL数据库有很多,MySQL是一种,本文基本都是SQ ...

  10. python的快速入门-Python快速入门

    Python语言本身就是一门简单的语言,入门非常容易. 机器学习中用到的Python(语法)相对就更简单了. 本文从机器学习的视角来看需要具备的Python基础. 我们的目标是-- 没有蛀牙... 针 ...

最新文章

  1. eclipse版本、代号
  2. JS创建多个下载任务
  3. java nginx https_docker nginx 配置ssl,实现https
  4. 视频质量检测中的TP、FP、Reacll、Precision
  5. JSP的三个编译指令-page,include详解
  6. 1.11_shell_sort_希尔排序
  7. HTML之文本相关标签
  8. 在 ReactNative 的 App 中,集成 Bugly 你会遇到的一些坑
  9. 麻瓜编程python web_麻瓜编程 Python Web开发工程师教程 完整版
  10. MATLAB批量添加图例
  11. Bandicam录制视频
  12. Android Studio 之万恶 gradle
  13. 微软中国CEO梁念坚 : Windows Phone有四大优点
  14. python怎么判断字符串中包含特殊符号
  15. 矫正ubuntu系统时间
  16. excel就绪筛选模式_Excel自动筛选器显示筛选器模式
  17. Windows 8应用商店应用如何与Android和iPad对抗?
  18. 工业互联网与制造控制生产网络学习总结
  19. MT6739的Android9.0 Camera kernel 驱动
  20. Linux系统删除文件夹下所有文件

热门文章

  1. html go语言,Go 语言基础语法
  2. 算法:回溯十四 Restore IP Addresses数字字符串还原为IP地址(2种解法)
  3. 问题:Warning: Attempt to present UINavigationController whose view is not in the window hierarchy
  4. PaddleSeg用于人像分割
  5. Java反射机制--反射概述
  6. 2020 携程 面经
  7. pwm 正弦波_增强型PWM抑制功能对于直列式电机控制的五大优势
  8. 凸优化第九章无约束优化 9.4最速下降方法
  9. 大白话解析模拟退火算法、遗传算法
  10. OpenCV-Python教程(5、初级滤波内容)