目前的Android單元測試,不少都基於Roboletric框架,迴避了Instrumentation test必須啓動虛擬機或者真機的麻煩,執行效率大大提升。這裏不討論測試框架的選擇問題,網絡上有不少關於此類的資料。同時,如今幾乎全部的App都會進行網絡數據通訊,Retrofit2就是其中很是方便的一個網絡框架,遵循Restful接口設計。如此,再進行Android單元測試時,就必然須要繞過Retrofit的真實網絡請求,mock出不一樣的response來進行本地邏輯測試。java
retrofit官方出過單元測試的方法和介紹,詳見參考文獻4,介紹的很是細緻。可是該方法是基於Instrumentation的,若是基於Robolectric框架,對於異步的請求就會出現問題,在stackoverflow上面有關於異步問題的描述,也給出了一個解決方法,可是須要對源碼進行改動,因此不完美。本文將針對Robolectric+Retrofit2的單元測試過程當中異步問題如何解決,提出一種更完美的解決方法。有理解不當的,後者更好的方案,歡迎你們提出指正。git
通常使用retrofit2的時候,會出現一下代碼片斷github
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進行測試json
第一點能夠藉助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的源碼來查看。app
經過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的類,再看看這個方法的實現:ide
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/