目前的Android单元测试,很多都基于Roboletric框架,回避了Instrumentation test必须启动虚拟机或者真机的麻烦,执行效率大大提高。这里不讨论测试框架的选择问题,网络上有很多关于此类的资料。同时,现在几乎所有的App都会进行网络数据通信,Retrofit2就是其中非常方便的一个网络框架,遵循Restful接口设计。如此,再进行Android单元测试时,就必然需要绕过Retrofit的真实网络请求,mock出不同的response来进行本地逻辑测试。


retrofit官方出过单元测试的方法和介绍,详见参考文献4,介绍的非常细致。但是该方法是基于Instrumentation的,如果基于Robolectric框架,对于异步的请求就会出现问题,在stackoverflow上面有关于异步问题的描述,也给出了一个解决方法,但是需要对源码进行改动,所以不完美。本文将针对Robolectric+Retrofit2的单元测试过程中异步问题如何解决,提出一种更完美的解决方法。有理解不当的,后者更好的方案,欢迎大家提出指正。


一般使用retrofit2的时候,会出现一下代码片段

publicvoidtestMethod(){OkHttpClientclient=newOkHttpClient();Retrofitretrofit=newRetrofit.Builder().baseUrl(BASE_URL).addConverterFactory(JacksonConverterFactory.create()).client(client).build();service=retrofit.create(xxxService.class);Call<xxxService>call=service.getxxx();call.enqueue(newCallback<xxx>(){@OverridepublicvoidonResponse(Call<xxx>call,Response<xxxResponse>response){//Dealwiththesuccessfulcase}@OverridepublicvoidonFailure(Call<xxxResponse>call,Throwablet){//Dealwiththefailurecase}});}


单元测试会测试testMethod方法,触发后根据不同的response,校验对应的逻辑处理,如上面的“// Deal with the successful case” 和 “// Deal with the failure case”。为了达到这个目的,需要实现一下两点:1)当触发该方法时,不会走真实的网络;2)可以mock不同的response进行测试


第一点可以借助MockWebServer来实现,具体的实现方法可以参考文献4,这里不展开了,重点看下第二点。在文献4中的sample#1,通过一个json文件,清晰简单的表明了测试的目的,所以我们也希望用这种方式。但是当实现后测试却发现,上面赋值给call.enqueue的Callback,无论是onResponse还是onFailure都不会被调用。后来在stackoverflow上面发现了文献3,再结合自己的测试,发现根本的原因在于call.enqueue是异步的。当单元测试已经结束时,enqueue的异步处理还没有结束,所以Callback根本没有被调用。那么网络是否执行了呢?通过打开OkhttpClient的log可以看到,MockWebServer的request和response都出现了,说明网络请求已经模拟执行了。产生这个问题跟Robolectric框架的实现有一定的关系,更进一步的具体原因,有兴趣大家可以进一步研究,也许会发现新的思路。


知道是由于异步导致的,那解决的思路就简单了,通过mock手段,将异步执行变成同步执行。那么如何mock呢,我们可以通过retrofit的源码来查看。

通过Retrofit的create方法可以获取service,先来看看create这个方法的实现

public<T>Tcreate(finalClass<T>service){Utils.validateServiceInterface(service);if(validateEagerly){eagerlyValidateMethods(service);}return(T)Proxy.newProxyInstance(service.getClassLoader(),newClass<?>[]{service},newInvocationHandler(){privatefinalPlatformplatform=Platform.get();@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object...args)throwsThrowable{//IfthemethodisamethodfromObjectthendefertonormalinvocation.if(method.getDeclaringClass()==Object.class){returnmethod.invoke(this,args);}if(platform.isDefaultMethod(method)){returnplatform.invokeDefaultMethod(method,service,proxy,args);}ServiceMethodserviceMethod=loadServiceMethod(method);OkHttpCallokHttpCall=newOkHttpCall<>(serviceMethod,args);returnserviceMethod.callAdapter.adapt(okHttpCall);}});}


从代码可以看出,通过service.getxxx()来获得Call<xxxService>的时候,实际获得的是OkHttpCall。那么call.enqueue实际调用的也是OkHttpCall的enqueue方法,其源码如下:

@Overridepublicvoidenqueue(finalCallback<T>callback){if(callback==null)thrownewNullPointerException("callback==null");okhttp3.Callcall;Throwablefailure;synchronized(this){if(executed)thrownewIllegalStateException("Alreadyexecuted.");executed=true;call=rawCall;failure=creationFailure;if(call==null&&failure==null){try{call=rawCall=createRawCall();}catch(Throwablet){failure=creationFailure=t;}}}if(failure!=null){callback.onFailure(this,failure);return;}if(canceled){call.cancel();}call.enqueue(newokhttp3.Callback(){@OverridepublicvoidonResponse(okhttp3.Callcall,okhttp3.ResponserawResponse)throwsIOException{Response<T>response;try{response=parseResponse(rawResponse);}catch(Throwablee){callFailure(e);return;}callSuccess(response);}@OverridepublicvoidonFailure(okhttp3.Callcall,IOExceptione){try{callback.onFailure(OkHttpCall.this,e);}catch(Throwablet){t.printStackTrace();}}privatevoidcallFailure(Throwablee){try{callback.onFailure(OkHttpCall.this,e);}catch(Throwablet){t.printStackTrace();}}privatevoidcallSuccess(Response<T>response){try{callback.onResponse(OkHttpCall.this,response);}catch(Throwablet){t.printStackTrace();}}});}


这里通过createRawCall方法来获得真正执行equeue的类,再看看这个方法的实现:

privateokhttp3.CallcreateRawCall()throwsIOException{Requestrequest=serviceMethod.toRequest(args);okhttp3.Callcall=serviceMethod.callFactory.newCall(request);if(call==null){thrownewNullPointerException("Call.Factoryreturnednull.");}returncall;}


真正的okhttp3.Call来自于serviceMethod.callFactory.newCall(request),那么serviceMethod.callFactory又是从哪里来的呢。打开ServiceMethod<T>这个类,在构造函数中有如下代码:

this.callFactory=builder.retrofit.callFactory();


说明这个callFactory来自于retrofit.callFactory(),进一步查看Retrofit类的源码:

okhttp3.Call.FactorycallFactory=this.callFactory;if(callFactory==null){callFactory=newOkHttpClient();}


在通过Retrofit.Builder创建retrofit实例的时候,可以通过下面的方法设置factory实例,如果不设置,默认会创建一个OkHttpClient。

publicBuildercallFactory(okhttp3.Call.Factoryfactory){this.callFactory=checkNotNull(factory,"factory==null");returnthis;}


到这里所有的脉络都清楚了,如果创建Retrofit实例时,设置我们自己的callFactory,在该factory中,调用的call.enqueue将根据设置的response直接调用callback中的onResponse或者onFailure方法,从而回避掉异步的问题。具体的实现代码如下:

publicclassMockFactoryextendsOkHttpClient{privateMockCallmockCall;publicMockFactory(){mockCall=newMockCall();}publicvoidmockResponse(Response.BuildermockBuilder){mockCall.setResponseBuilder(mockBuilder);}@OverridepublicCallnewCall(Requestrequest){mockCall.setRequest(request);returnmockCall;}publicclassMockCallimplementsCall{//Guardedbythis.privatebooleanexecuted;volatilebooleancanceled;/**Theapplication'soriginalrequestunadulteratedbyredirectsorauthheaders.*/RequestoriginalRequest;Response.BuildermockResponseBuilder;HttpEngineengine;protectedMockCall(){}//protectedMockCall(RequestoriginalRequest,booleanmockFailure,//Response.BuildermockResponseBuilder){//this.originalRequest=originalRequest;//this.mockFailure=mockFailure;//this.mockResponseBuilder=mockResponseBuilder;//this.mockResponseBuilder.request(originalRequest);//}publicvoidsetRequest(RequestoriginalRequest){this.originalRequest=originalRequest;}publicvoidsetResponseBuilder(Response.BuildermockResponseBuilder){this.mockResponseBuilder=mockResponseBuilder;}@OverridepublicRequestrequest(){returnoriginalRequest;}@OverridepublicResponseexecute()throwsIOException{returnmockResponseBuilder.request(originalRequest).build();}@Overridepublicvoidenqueue(CallbackresponseCallback){synchronized(this){if(executed)thrownewIllegalStateException("AlreadyExecuted");executed=true;}intcode=mockResponseBuilder.request(originalRequest).build().code();if(code>=200&&code<300){try{if(mockResponseBuilder!=null){responseCallback.onResponse(this,mockResponseBuilder.build());}}catch(IOExceptione){//Nothing}}else{responseCallback.onFailure(this,newIOException("MockresponseCallbackonFailure"));}}@Overridepublicvoidcancel(){canceled=true;if(engine!=null)engine.cancel();}@OverridepublicsynchronizedbooleanisExecuted(){returnexecuted;}@OverridepublicbooleanisCanceled(){returncanceled;}}}


下面看下单元测试的时候怎么用。

1)通过反射或者mock,修改被测代码中的retrofit实例,调用callFactory来设置上面的MockFactory

2)准备好要返回的response,设置MockFactory的mockResponse,调用被测方法,校验结果

@Testpublicvoidtestxxx()throwsException{ResponseBodyresponseBody=ResponseBody.create(MediaType.parse("application/json"),RestServiceTestHelper.getStringFromFile("xxx.json"));Response.BuildermockBuilder=newResponse.Builder().addHeader("Content-Type","application/json").protocol(Protocol.HTTP_1_1).code(200).body(responseBody);mMockFactory.mockResponse(mockBuilder);//callthemethodtobetested//verfifyiftheresultisexpected}



参考文献:

1. robolectric.org

2. https://square.github.io/retrofit/

3. http://stackoverflow.com/questions/37909276/testing-retrofit-2-with-robolectric-callbacks-not-being-called

4. https://riggaroo.co.za/retrofit-2-mocking-http-responses/