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


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


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

public void testMethod() {    OkHttpClient client = new OkHttpClient();    Retrofit retrofit = new Retrofit.Builder()                .baseUrl(BASE_URL)                .addConverterFactory(JacksonConverterFactory.create())                .client(client)                .build();    service = retrofit.create(xxxService.class);    Call call = service.getxxx();    call.enqueue(new Callback() {        @Override        public void onResponse(Call call, Response response) {        // Deal with the successful case        }        @Override        public void onFailure(Call call, Throwable t) {        // Deal with the failure case        }    });}


单元测试会测试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 create(final Class service) {    Utils.validateServiceInterface(service);    if (validateEagerly) {        eagerlyValidateMethods(service);    }    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },            new InvocationHandler() {                private final Platform platform = Platform.get();                @Override public Object invoke(Object proxy, Method method, Object... args)                        throws Throwable {                    // If the method is a method from Object then defer to normal invocation.                    if (method.getDeclaringClass() == Object.class) {                        return method.invoke(this, args);                    }                    if (platform.isDefaultMethod(method)) {                        return platform.invokeDefaultMethod(method, service, proxy, args);                    }                    ServiceMethod serviceMethod = loadServiceMethod(method);                    OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);                    return serviceMethod.callAdapter.adapt(okHttpCall);                }            });}


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

@Override public void enqueue(final Callback callback) {if (callback == null) throw new NullPointerException("callback == null");okhttp3.Call call;Throwable failure;synchronized (this) {if (executed) throw new IllegalStateException("Already executed.");executed = true;call = rawCall;failure = creationFailure;if (call == null && failure == null) {try {call = rawCall = createRawCall();} catch (Throwable t) {failure = creationFailure = t;}}}if (failure != null) {callback.onFailure(this, failure);return;}if (canceled) {call.cancel();}call.enqueue(new okhttp3.Callback() {@Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse)throws IOException {Response response;try {response = parseResponse(rawResponse);} catch (Throwable e) {callFailure(e);return;}callSuccess(response);}@Override public void onFailure(okhttp3.Call call, IOException e) {try {callback.onFailure(OkHttpCall.this, e);} catch (Throwable t) {t.printStackTrace();}}private void callFailure(Throwable e) {try {callback.onFailure(OkHttpCall.this, e);} catch (Throwable t) {t.printStackTrace();}}private void callSuccess(Response response) {try {callback.onResponse(OkHttpCall.this, response);} catch (Throwable t) {t.printStackTrace();}}});}


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

private okhttp3.Call createRawCall() throws IOException {Request request = serviceMethod.toRequest(args);okhttp3.Call call = serviceMethod.callFactory.newCall(request);if (call == null) {throw new NullPointerException("Call.Factory returned null.");}return call;}


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

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


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

okhttp3.Call.Factory callFactory = this.callFactory;if (callFactory == null) {    callFactory = new OkHttpClient();}


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

public Builder callFactory(okhttp3.Call.Factory factory) {    this.callFactory = checkNotNull(factory, "factory == null");    return this;}


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

public class MockFactory extends OkHttpClient {    private MockCall mockCall;    public MockFactory() {        mockCall = new MockCall();    }    public void mockResponse(Response.Builder mockBuilder) {        mockCall.setResponseBuilder(mockBuilder);    }    @Override    public Call newCall(Request request) {        mockCall.setRequest(request);        return mockCall;    }    public class MockCall implements Call {        // Guarded by this.        private boolean executed;        volatile boolean canceled;        /** The application's original request unadulterated by redirects or auth headers. */        Request originalRequest;        Response.Builder mockResponseBuilder;        HttpEngine engine;        protected MockCall() {}//        protected MockCall(Request originalRequest, boolean mockFailure,//                           Response.Builder mockResponseBuilder) {//            this.originalRequest = originalRequest;//            this.mockFailure = mockFailure;//            this.mockResponseBuilder = mockResponseBuilder;//            this.mockResponseBuilder.request(originalRequest);//        }        public void setRequest(Request originalRequest) {            this.originalRequest = originalRequest;        }        public void setResponseBuilder(Response.Builder mockResponseBuilder) {            this.mockResponseBuilder = mockResponseBuilder;        }        @Override        public Request request() {            return originalRequest;        }        @Override        public Response execute() throws IOException {            return mockResponseBuilder.request(originalRequest).build();        }        @Override        public void enqueue(Callback responseCallback) {            synchronized (this) {                if (executed) throw new IllegalStateException("Already Executed");                executed = true;            }            int code = mockResponseBuilder.request(originalRequest).build().code();            if (code >= 200 && code < 300) {                try {                    if (mockResponseBuilder != null) {                        responseCallback.onResponse(this,                                mockResponseBuilder.build());                    }                } catch (IOException e) {                    // Nothing                }            } else {                responseCallback.onFailure(this, new IOException("Mock responseCallback onFailure"));            }        }        @Override        public void cancel() {            canceled = true;            if (engine != null) engine.cancel();        }        @Override        public synchronized boolean isExecuted() {            return executed;        }        @Override        public boolean isCanceled() {            return canceled;        }    }}


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

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

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

@Testpublic void testxxx() throws Exception {    ResponseBody responseBody = ResponseBody.create(MediaType.parse("application/json"),            RestServiceTestHelper.getStringFromFile("xxx.json"));    Response.Builder mockBuilder = new Response.Builder()            .addHeader("Content-Type", "application/json")            .protocol(Protocol.HTTP_1_1)            .code(200)            .body(responseBody);    mMockFactory.mockResponse(mockBuilder);    // call the method to be tested// verfify if the result is expected}



参考文献:

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/


更多相关文章

  1. Android截屏方法总结
  2. Android蓝牙开发浅谈【转】
  3. android如何停止Thread和AsyncTask
  4. Android(安卓)源码分析 - 事件分发机制
  5. Android中使用Espresso进行UI测试
  6. Android图片压缩,自己编译libjpeg
  7. Android期末复习题
  8. Android中的网络管理源码分析--netd
  9. 雷电android game学习笔记(1)

随机推荐

  1. 当你的Android(安卓)Studio 设置No proxy
  2. Android模拟器和安装APK文件基础教程
  3. Android签名用keytool和jarsigner制作apk
  4. Android消息处理机制——Looper,Handler,
  5. android 定制 View派生类
  6. 【Android】自定义View、画布Canvas与画
  7. 网络编程之——他山之石OkHttp
  8. 2020.9.8 oppo Java开发(Android)一面面经
  9. android EditText 限定中文个数与英文个
  10. Android(安卓)使用LayerDrawable自定制Se