写作目的

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的使用范式:

  1. 进入录制模式
  2. 编辑mock逻辑
  3. 进入测试模式
  4. 调用被测试方法

在录制模式中,方法的调用不会被真正执行,而是被记录,例子中的 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场景,使用频率较高。如有错误欢迎在评论区指正。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐