目前的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,如需转载请自行联系原作者