AgitarOne Mockingbird常用api
本文目录
写作目的
Mockingbird 是 agitarOne 提供的强大 mock 框架,能够轻松模拟公有/私有方法的返回值或异常,甚至支持构造函数模拟。相比主流 mock 工具 Mockito,其功能更全面且限制更少。但由于 agitarOne 是国外收费产品,中文资料较为匮乏,这给单元测试开发带来了一定挑战。本文将结合实际使用经验,重点介绍高频 API 的具体用法,并分享实战中遇到的典型问题及解决方案。
基本使用方法
被测试类
public void Sample{
private SampleService sampleService = new SampleService();
public String getId(){
return sampleService.getId();
}
}
测试代码
import static com.agitar.lib.mockingbird.Mockingbird.*;
public class SampleManualAgitarTest extends AgitarTestCase{
public void test_getId(){
Sample sample = new Sample();
SampleService sampleService = (SampleService) getPrivateField(sample, "sampleService");
// 进入录制模式
enterRecordingMode();
// mock 方法返回值
setReturnValue(sampleService.getId(), "1");
// 进入测试模式
enterTestMode();
// 调用被测试方法
sample.getId();
}
}
从上述例子中可以看到Mockingbird的使用范式:
- 进入录制模式
- 编辑mock逻辑
- 进入测试模式
- 调用被测试方法
在录制模式中,方法的调用不会被真正执行,而是被记录,例子中的 sampleService.getId()并没有被真正调用。进入测试模式后,当方法体运行到sampleService.getId()时,方法返回给定的mock值,从而完成对方法返回值的控制。
API 介绍
setReturnValue
该api用于控制方法返回值,可以控制公有方法、私有方法、静态方法的返回值。一般情况下,mock方法返回值时,方法参数值需要和实际调用的相同(同一个值或同一个对象),然而,即使不能保证上述条件,该方法也能做到返回值控制。
场景一: 公有方法返回值mock
/**
* 控制公有方法返回值,mock一次
* @param methodReturnValue 方法调用结果
* @param exceptedValue 方法期待结果
**/
public static void setReturnValue(int methodReturnValue, int exceptedValue);
/**
* 控制公有方法返回值,支持mock多次
* @param methodReturnValue 方法调用结果
* @param exceptedValue 方法期待结果
* @param mockTimes mock次数
**/
public static void setReturnValue(int methodReturnValue, int exceptedValue, int mockTimes);
/**
* 控制公有方法返回值,支持static方法,支持控制mock次数
* @param isStaticMethod 是否是static方法
* @param methodReturnValue 方法调用结果
* @param exceptedValue 方法期待结果
* @param mockTimes mock次数
**/
public static void setReturnValue(boolean isStaticMethod, int methodReturnValue, int exceptedValue, int mockTimes);
特别注意,对于第二个参数,要求方法调用时的参数和实际测试是的参数是同一个值或对象,否则会mock失败。举例说明如下:
被测试类
public void Sample{
private SampleService sampleService = new SampleService();
public String getId(){
return sampleService.getId("Sample");
}
}
测试代码
import static com.agitar.lib.mockingbird.Mockingbird.*;
public class SampleManualAgitarTest extends AgitarTestCase{
public void test_getId(){
Sample sample = new Sample();
SampleService sampleService= (SampleService) getPrivateField(sample, "sampleService");
// 测试1
enterRecordingMode();
// mock 方法返回值,参数要求和实际调用时相同, mock成功
setReturnValue(sampleService.getId("Sample"), "1");
enterTestMode();
sample.getId();
// 测试2
enterRecordingMode();
// mock 方法返回值,参数和实际调用时不同, mock失败
setReturnValue(sampleService.getId("xxx"), "1");
// 进入测试模式
enterTestMode();
// 调用被测试方法
sample.getId();
}
}
上述case中,由于在mock阶段方法参数和实际调用时不同,回导致mock失败。
上述仅列举部分方法,其他重载方法对methodReturnValue参数和exceptedValue参数做重载,支持不同类型返回值的情况,这里不再一一例举。
注意mock次数的控制,进入测试模式(Mockingbird.enterTestMode())后,方法的每一次调用都会使用一次mock次数,当mock次数用完了,下一次的方法调用将不受控制。
私有方法返回值mock
/**
* 控制公有方法返回值,支持static方法,支持控制mock次数
* @param isStaticMethod 是否是static方法
* @param methodInvokeObj 方法调用对象,static方法是类对象
* @param methodName 方法名称
* @param methodParamsAndType 方法参数和返回值,写法需要特别注意
* @param exceptedValue 期待返回值
* @param mockTimes mock次数
**/
public static void setReturnValue(boolean isStaticMethod, Object methodInvokeObj, String methodName, String methodParamsAndType, Object exceptedValue, int mockTimes);
该api功能非常强大,对于有返回值的方法,不区分方法是公有还是私有,也不必关注方法调用的参数,可直接控制指定方法的返回值。对于无返回值的方法,可利用其实际方法不执行的效果,解决实际执行会导致异常的场景。
参数 methodParamsAndType 的格式为:参数按顺序依次排布,并用英文逗号分隔,括号包裹各个参数全限定名,方法返回值的全限定名在括号之后。该参数写法不正确,会导致mock失败。
举例如下:
| 方法签名 | methodParamsAndType 写法 |
|---|---|
| int getId() | ()int |
| Integer getId(String tag) | (java.lang.Integer)java.lang.String |
| void init() | ()void |
| boolean isConnected(String serviceName, String tag) | (java.lang.String,java.lang.String)boolean |
setException
该api用于控制方法抛出异常,和setReturnValue方法相同,不再赘述。需要注意,只能抛出方法声明了的异常,其他异常的抛出无法实现。
setNomalReturnForVoid
方法签名:
public static void setNormalReturnForVoid();
使用场景:
当某个void方法执行中抛出异常,导致该方法后续的代码无法执行到时,使用该api使其不抛出异常,从而覆盖到后续代码。
使用方法:
首先先调用目标方法,然后调用该API。
例子:
被测试类
package com.test;
public void Sample{
public String getUUId(){
checkEnvironment();
return "3f2504e0-4f89-11d3-9a0c-0305e82c3301";
}
private void checkEnvironment(){
throw new RuntimeException();
}
}
测试代码
import static com.agitar.lib.mockingbird.Mockingbird.*;
public class SampleManualAgitarTest extends AgitarTestCase{
public void test_getUUId(){
Sample sample = new Sample();
enterRecordingMode();
// 先调用原始方法
callPrivateMethod("com.test.Sample", "checkEnvironment", new Class[]{}, sample , new Object[]{});
// 调用API
setNormalReturnForVoid();
enterTestMode();
sample.getUUId();
}
}
上述例子中,checkEnvironment() 方法会抛出 异常,导致后续代码不能执行到。在录制模式中,先调用该方法,再调用setNormalReturnForVoid API,让该方法正常返回。
setExceptionForVoid
方法签名:
/ **
* 设置void方法抛出指定Throwalbe 对象
* @param throwable 指定的throwable 对象
**/
public static void setExceptionForVoid(Throwable throwalbe);
使用场景:
当某个void方法执行中,需要抛出异常才能执行到某些分支,而不控制其行为不抛出异常时,可使用该api使其抛出异常,从而覆盖到分支代码。
使用方法:
首先先调用目标方法,然后调用该API。
例子:
被测试类
package com.test;
public void Sample{
public String getUUId(){
try{
checkEnvironment();
return "3f2504e0-4f89-11d3-9a0c-0305e82c3301";
} catch(RemoteException e){
return "";
}
}
private void checkEnvironment throws RemoteException(){
return;
}
}
测试代码
import static com.agitar.lib.mockingbird.Mockingbird.*;
public class SampleManualAgitarTest extends AgitarTestCase{
public void test_getUUId(){
Sample sample = new Sample();
enterRecordingMode();
// 先调用原始方法
callPrivateMethod("com.test.Sample", "checkEnvironment", new Class[]{}, sample , new Object[]{});
// 调用API
setExceptionForVoid(new RemoteException());
enterTestMode();
sample.getUUId();
}
}
上述例子中,checkEnvironment() 不会抛出异常,导致分支代码不能执行到。在录制模式中,先调用该方法,再调用setExceptionForVoid这个API,让该方法抛出指定异常,从而覆盖到分支代码。
repleaceObjectForRecording
方法签名
/ **
* 替换被测试代码中new出来的对象
* @param clazz 类对象,对于不可见类(内部类),可以通过Class.forName(String) api 获取到。
* @param constructorSignature 构造函数签名,格式有特定要求,构造函数名均为<init>,构造函数参数的全限定名按顺序排列并用英文逗号分隔后,用括号包裹放在构造函数名后面,例如默认构造函数:<init>(), 带参数的构造函数:<init>(java.lang.String,int)
* @param instance 替换后的对象
**/
public static void repleaceObjectForRecording(Class clazz, String constructorSignature, Object instance);
使用场景:
在被测试方法中new了一个对象,外部无法拿到该对象,导致对该对象的方法无法mock时,可以使用该api,将此对象替换为指定对象,从而实现对象行为的mock。
例子:
被测试类
package com.test;
public void Sample{
public String getUUId(){
UUidGenerator generator = new UUidGenerator("");
if(generator.isReady()){
return generator.getUUID();
}else{
return "3f2504e0-4f89-11d3-9a0c-0305e82c3301";
}
}
}
测试代码
import static com.agitar.lib.mockingbird.Mockingbird.*;
public class SampleManualAgitarTest extends AgitarTestCase{
public void test_getUUId(){
Sample sample = new Sample();
UUidGenerator generator = (UUidGenerator)getProxyObject(UUidGenerator.class);
// 测试 1
enterRecordingMode();
repleaceObjectForRecording(UUidGenerator.class, "<init>(java.lang.String)", generator);
setReturnValue(generator.isReady(), true);
enterTestMode();
sample.getUUId();
// 测试2
enterRecordingMode();
repleaceObjectForRecording(UUidGenerator.class, "<init>(java.lang.String)", generator);
setReturnValue(generator.isReady(), false);
enterTestMode();
sample.getUUId();
}
}
常见问题
为什么相同的测试代码首次执行和二次执行路径不同?(某些case本地通过但CI报错)
这种现象常见于批量测试场景。测试框架为提高效率可能启用多线程并发执行,导致测试顺序无法保证。首次执行时关键类变量值为A,代码走第一条路径;而第二次执行时该变量可能已被其他先执行的测试修改为B,导致转入第二条路径。
解决方案:在测试用例中明确初始化关键类变量,确保每次执行状态一致。
测试用例执行了但未覆盖目标方法?
请确认是否仍处于录制模式(未调用Mockingbird.enterTestMode())。
Android环境中,各种post到Handler中的方法如何确保执行?
post方法是异步执行的,当单元测试主线程退出时,不能确保post线程中的代码执行完,会出现多次执行,覆盖的代码行数不同的问题。
使用AgitarOne的Intercepter可以满足需要,用法见下方例子。
被测试类
package com.test;
public void Sample{
Handler handler = new Handler();
public String getUUId(){
handler.post(() -> {
return "xxxx-xxxxxxxx-xxxxxxxx-xxxx";
});
}
}
测试代码
import static com.agitar.lib.mockingbird.Mockingbird.*;
public class SampleManualAgitarTest extends AgitarTestCase{
static class Post_Intercepter implements MethodIntercepter {
@Override
public String getDeclaringClass() {
return Sample.class.getName();
}
@Override
public String getSignature() {
return "post(java.lang.Runnable)";
}
@Override
public Object invoke(Object methodInvokeObj, Object[] methodParams) throws Throwable {
Runnable runnable = (Runnable)methodParams[0];
runnable.run();
return null;
}
}
public void test_getUUId(){
Sample sample = new Sample();
sample.getUUId();
}
}
原理就是将原来的异步执行的方式,在post时直接执行到,保证测试代码执行完才结束。
除了Handler的post或postDelay方法,如Timer的schedule方法,或者异步线程的执行等,均可通过上述方式实现代码的执行。
结尾
上述API能满足多数mock场景,使用频率较高。如有错误欢迎在评论区指正。
更多推荐


所有评论(0)