TestableMock
是基于源码和字节码加强的Java单元测试辅助工具,包含如下功能:
访问被测类私有成员:使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题java
快速Mock任意调用:使被测类的任意方法调用快速替换为Mock方法,实现"指哪换哪",解决传统Mock工具使用繁琐的问题git
辅助测试void方法:利用Mock校验器对方法的内部逻辑进行检查,解决无返回值方法难以实施单元测试的问题web
访问私有成员字段和方法
现在关于私有方法是否应该作单元测试的争论正逐渐消停,开发者的广泛实践已经给出事实答案。经过公有方法间接测私有方法在不少状况下难以进行,开发者们更愿意经过修改方法可见性的办法来让本来私有的方法在测试用例中变得可测。微信
此外,在单元测试中时常会须要对被测对象进行特定的成员字段初始化,但有时因为被测类的构造方法限制,使得没法便捷的对这些字段进行赋值。那么,可否在不破坏被测类型封装的状况下,容许单元测试用例内的代码直接访问被测类的私有方法和成员字段呢?TestableMock提供了两种简单的解决方案。app
方法一:使用`@EnablePrivateAccess`注解
只需为测试类添加@EnablePrivateAccess
注解,便可在测试用例中得到如下加强能力:框架
调用被测类的私有方法(包括静态方法)dom
读取被测类的私有字段(包括静态字段)编辑器
修改被测类的私有字段(包括静态字段)工具
修改被测类的常量字段(使用final修饰的字段,包括静态字段)单元测试
访问和修改私有、常量成员时,IDE可能会提示语法有误,但编译器将可以正常运行测试。(使用编译期代码加强,目前仅实现了Java语言的适配)
效果见java-demo
示例项目DemoPrivateAccessTest
测试类中的用例。
方法二:使用`PrivateAccessor`工具类
若不但愿看到IDE的语法错误提醒,或是在非Java语言的JVM工程(譬如Kotlin语言)里,也能够借助PrivateAccessor
工具类来直接访问私有成员。
这个类提供了6个静态方法:
PrivateAccessor.get(被测对象, "私有字段名")
➜ 读取被测类的私有字段PrivateAccessor.set(被测对象, "私有字段名", 新的值)
➜ 修改被测类的私有字段(或常量字段)PrivateAccessor.invoke(被测对象, "私有方法名", 调用参数..)
➜ 调用被测类的私有方法PrivateAccessor.getStatic(被测类型, "私有静态字段名")
➜ 读取被测类的静态私有字段PrivateAccessor.setStatic(被测类型, "私有静态字段名", 新的值)
➜ 修改被测类的静态私有字段(或静态常量字段)PrivateAccessor.invokeStatic(被测类型, "私有静态方法名", 调用参数..)
➜ 调用被测类的静态私有方法
快速Mock被测类的任意方法调用
相比以往Mock工具以类为粒度的Mock方式,TestableMock
容许用户直接定义须要Mock的单个方法,并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测方法中的指定方法调用。
概括起来就两条:
Mock非构造方法,拷贝原方法定义到测试类,增长一个与调用者类型相同的参数,加
@MockMethod
注解Mock构造方法,拷贝原方法定义到测试类,返回值换成构造的类型,方法名随意,加
@MockContructor
注解
具体的Mock方法定义约定以下:
1. 覆写任意类的方法调用
在测试类里定义一个有@MockMethod
注解的普通方法,使它与需覆写的方法名称、参数、返回值类型彻底一致,而后在其参数列表首位再增长一个类型为该方法本来所属对象类型的参数。
此时被测类中全部对该需覆写方法的调用,将在单元测试运行时,将自动被替换为对上述自定义Mock方法的调用。
注意:当遇到待覆写方法有重名时,能够将需覆写的方法名写到@MockMethod
注解的targetMethod
参数里,这样Mock方法自身就能够随意命名了。
例如,被测类中有一处"anything".substring(1, 2)
调用,咱们但愿在运行测试的时候将它换成一个固定字符串,则只需在测试类定义以下方法:
// 原方法签名为`String substring(int, int)`
// 调用此方法的对象`"anything"`类型为`String`
// 则Mock方法签名在其参数列表首位增长一个类型为`String`的参数(名字随意)
// 此参数可用于得到当时的实际调用者的值和上下文
@MockMethod
private String substring(String self, int i, int j) {
return "sub_string";
}
下面这个例子展现了targetMethod
参数的用法,其效果与上述示例相同:
// 使用`targetMethod`指定需Mock的方法名
// 此方法自己如今能够随意命名,但方法参数依然须要遵循相同的匹配规则
@MockMethod(targetMethod = "substring")
private String use_any_mock_method_name(String self, int i, int j) {
return "sub_string";
}
完整代码示例见java-demo
和kotlin-demo
示例项目中的should_able_to_mock_common_method()
测试用例。(因为Kotlin对String类型进行了魔改,故Kotlin示例中将被测方法在BlackBox
类里加了一层封装)
2. 覆写被测类自身的成员方法
有时候,在对某些方法进行测试时,但愿将被测类自身的另一些成员方法Mock掉。
操做方法与前一种状况相同,Mock方法的第一个参数类型需与被测类相同,便可实现对被测类自身(不管是公有或私有)成员方法的覆写。
例如,被测类中有一个签名为String innerFunc(String)
的私有方法,咱们但愿在测试的时候将它替换掉,则只需在测试类定义以下方法:
// 被测类型是`DemoMock`
// 所以在定义Mock方法时,在目标方法参数首位加一个类型为`DemoMock`的参数(名字随意)
@MockMethod
private String innerFunc(DemoMock self, String text) {
return "mock_" + text;
}
3. 覆写任意类的静态方法
对于静态方法的Mock与普通方法相同。但须要注意的是,静态方法的Mock方法被调用时,传入的第一个参数实际值始终是null
。
例如,在被测类中调用了BlackBox
类型中的静态方法secretBox()
,改方法签名为BlackBox secretBox()
,则Mock方法以下:
// 目标静态方法定义在`BlackBox`类型中
// 在定义Mock方法时,在目标方法参数首位加一个类型为`BlackBox`的参数(名字随意)
// 此参数仅用于标识目标类型,实际传入值将始终为`null`
@MockMethod
private BlackBox secretBox(BlackBox ignore) {
return new BlackBox("not_secret_box");
}
完整代码示例见java-demo
和kotlin-demo
示例项目中的should_able_to_mock_static_method()
测试用例。
测试无返回值的方法
如何对void类型的方法进行测试一直是许多单元测试框架在悄悄回避的话题,因为以往的单元测试手段主要是对被测单元的返回结果进行校验,当遇到方法没有返回值时就会变得无从下手。
从功能的角度来讲,虽然void方法不返回任何值,但它的执行必定会对外界产生某些潜在影响,咱们将其称为方法的"反作用",好比:
初始化某些外部变量(私有成员变量或者全局静态变量)
在方法体内对外部对象实例进行赋值
输出了日志
调用了其余外部方法
… …
不返回任何值也不产生任何"反作用"的方法没有存在的意义。
这些"反作用"的本质概括来讲可分为两类:修改外部变量和调用外部方法。
经过TestableMock的私有字段访问和Mock校验器能够很方便的实现对"反作用"的结果检查。
1. 修改外部变量的void方法
例如,下面这个方法会根据输入修改私有成员变量hashCache
:
class Demo {
private Map<String, Integer> hashCache = mapOf();
public void updateCache(String domain, String key) {
String cacheKey = domain + "::" + key;
Integer num = hashCache.get(cacheKey);
hashCache.put(cacheKey, count == null ? initHash(key) : nextHash(num, key));
}
... // 其余方法省略
}
若要测试此方法,能够利用TestableMock直接读取私有成员变量的值,对结果进行校验:
@EnablePrivateAccess // 启用TestableMock的私有成员访问功能
class DemoTest {
private Demo demo = new Demo();
@Test
public void testSaveToCache() {
Integer firstVal = demo.initHash("hello"); // 访问私有方法
Integer nextVal = demo.nextHash(firstVal, "hello"); // 访问私有方法
demo.saveToCache("demo", "hello");
assertEquals(firstVal, demo.hashCache.get("demo::hello")); // 读取私有变量
demo.saveToCache("demo", "hello");
assertEquals(nextVal, demo.hashCache.get("demo::hello")); // 读取私有变量
}
}
2. 调用外部方法的void方法
例如,下面这个方法会根据输入打印信息到控制台:
class Demo {
public void recordAction(Action action) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss ");
String timeStamp = df.format(new Date());
System.out.println(timeStamp + "[" + action.getType() + "] " + action.getTarget());
}
}
若要测试此方法,能够利用TestableMock快速Mock掉System.out.println
方法。在Mock方法体里能够继续执行原调用(至关于并不影响原本方法功能,仅用于作调用记录),也能够直接留空(至关于去除了原方法的反作用)。
在执行完被测的void类型方法之后,用InvokeVerifier.verify()
校验传入的打印内容是否符合预期:
class DemoTest {
private Demo demo = new Demo();
// 拦截`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`被调用,且传入参数符合预期
verify("println").with(matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download"));
}
}
项目地址
开源地址:https://gitee.com/mirrors/TestableMock
关注肥朝公众号回复"mock",查看该mock工具介绍使用文档
本文分享自微信公众号 - 肥朝(feichao_java)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。