>>分享Android开发相关的技术 书籍支持  卫琴直播  品书摘要  在线测试  资源下载  联系我们
发表一个新主题 开启一个新投票 回复文章 您是本文章第 19795 个阅读者 刷新本主题
 * 贴子主题:  Roboletric+Retrofit2单元测试 回复文章 点赞(0)  收藏  
作者:javathinker    发表时间:2020-03-11 15:17:16     消息  查看  搜索  好友  复制  引用

   目前的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<xxxService> call = service.getxxx();
    call.enqueue(new Callback<xxx>() {

        @Override
        public void onResponse(Call<xxx> call, Response<xxxResponse> response) {
        // Deal with the successful case
        }

        @Override
        public void onFailure(Call<xxxResponse> 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> T create(final Class<T> 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<xxxService>的时候,实际获得的是OkHttpCall。那么call.enqueue实际调用的也是OkHttpCall的enqueue方法,其源码如下:

@Override public void enqueue(final Callback<T> 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<T> 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<T> 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<T>这个类,在构造函数中有如下代码:

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,调用被测方法,校验结果

@Test
public 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/

            

----------------------------
原文链接:https://blog.51cto.com/jazka/1880290

程序猿的技术大观园:www.javathinker.net



[这个贴子最后由 flybird 在 2020-03-14 10:10:52 重新编辑]
  Java面向对象编程-->变量的作用域和初始化
  JavaWeb开发-->JavaWeb应用入门(Ⅱ)
  JSP与Hibernate开发-->Spring、JPA与Hibernate的整合
  Java网络编程-->基于MVC和RMI的分布式应用
  精通Spring-->通过Vuex进行状态管理
  Vue3开发-->Vue组件开发基础
  Android UI学习 - TableLayout
  Android 自定义Menu
  Android Application Theme的实现及管理
  ]android:gravity / android:layout_Gravity 的区别
  Android 4.0 : 复制APK,复制动态库的Android.mk 文件
  Android SDCard Mount 流程分析
  Android ListView高度问题
  Android性能优化:App启动原理分析及启动时间优化
  到底什么是AndroidX?
  Android 应用程序组件
  Android 开发环境搭建
  安卓sqlite和Listview
  Android访问WEBAPI,传递json
  Appbarlayout+Recycleview滑动效果颜色渐变
  Android中竖着的Tablayout的简单使用
  更多...
 IPIP: 已设置保密
树形列表:   
1页 0条记录 当前第1
发表一个新主题 开启一个新投票 回复文章


中文版权所有: JavaThinker技术网站 Copyright 2016-2026 沪ICP备16029593号-2
荟萃Java程序员智慧的结晶,分享交流Java前沿技术。  联系我们
如有技术文章涉及侵权,请与本站管理员联系。