Roboletric+Retrofit2单元测试

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

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


分享文章:Roboletric+Retrofit2单元测试
文章链接:http://myzitong.com/article/podopc.html