Roboletric+Retrofit2单元测试

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


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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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这个方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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方法,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@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的类,再看看这个方法的实现:

1
2
3
4
5
6
7
8
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>这个类,在构造函数中有如下代码:

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


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

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


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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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,调用被测方法,校验结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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
}



本文转自jazka 51CTO博客,原文链接:http://blog.51cto.com/jazka/1880290,如需转载请自行联系原作者

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值