使用 robolectric 测试改造 2,未调用回调
Testing retrofit 2 with robolectric, callbacks not being called
我正在尝试使用服务器为我的 restful api 创建单元测试。
经过一些研究,我想出了一个使用 robolectric 和 OkHttp MockWebServer.
的解决方案
这是我的(简化示例):
api界面:
public interface ServerApi {
@POST("echo")
Call<EchoResponseData> echo(@Body EchoRequestData data);
}
response/request 个数据对象:
public class EchoRequestData {
private final String value;
public EchoRequestData(final String value) {
this.value = value;
}
}
public class EchoResponseData {
private String value;
public EchoResponseData() {}
public String getValue() {
return this.value;
}
}
api 实现:
public class ServerFacade {
private final ServerApi serverApi;
private final OkHttpClient httpClient;
private final Retrofit retrofitClient;
public ServerFacade(final String baseUrl) {
this.httpClient = new OkHttpClient.Builder().addNetworkInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long t1 = System.nanoTime();
System.out.println(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers()));
okhttp3.Response response = chain.proceed(request);
long t2 = System.nanoTime();
System.out.println(String.format("Received response for %s in %.1fms%n%s%s", response.request().url(), (t2 - t1) / 1e6d, response.headers(), response.body().string()));
return response;
}
}).build();
this.retrofitClient = new Retrofit.Builder()
.client(this.httpClient)
.baseUrl(baseUrl.toString())
.addConverterFactory(GsonConverterFactory.create())
.build();
this.serverApi = this.retrofitClient.create(ServerApi.class);
}
public void echo(final String value) {
final EchoRequestData data = new EchoRequestData(value);
this.serverApi.echo(data).enqueue(new Callback<EchoResponseData>() {
@Override
public void onResponse(Call<EchoResponseData> call, Response<EchoResponseData> response) {
System.out.println("onResponse");
}
@Override
public void onFailure(Call<EchoResponseData> call, Throwable throwable) {
System.out.println("onFailure: " + throwable.getMessage());
}
});
}
}
测试:
@RunWith(RobolectricTestRunner.class)
@Config(shadows = ServerFacadeTest.MyNetworkSecurityPolicy.class,
manifest = "src/main/AndroidManifest.xml",
constants = BuildConfig.class,
sdk = 16)
public class ServerFacadeTest {
protected MockWebServer mockServer;
protected ServerFacade serverFacade;
@Before
public void setUp() throws Exception {
this.mockServer = new MockWebServer();
this.mockServer.start();
this.serverFacade = new ServerFacade(this.mockServer.url("/").toString());
}
@Test
public void testEcho() throws InterruptedException {
final Object mutex = new Object();
final String value = "hey";
final String responseBody = "{\"value\":\"" + value + "\"}";
this.mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody));
this.serverFacade.echo(value);
synchronized (mutex) {
System.out.println("waiting");
mutex.wait(2500);
System.out.println("after wait");
}
}
@After
public void tearDown() throws Exception {
this.mockServer.shutdown();
}
@Implements(NetworkSecurityPolicy.class)
public static class MyNetworkSecurityPolicy {
@Implementation
public static NetworkSecurityPolicy getInstance() {
try {
Class<?> shadow = MyNetworkSecurityPolicy.class.forName("android.security.NetworkSecurityPolicy");
return (NetworkSecurityPolicy) shadow.newInstance();
} catch (Exception e) {
throw new AssertionError();
}
}
@Implementation
public boolean isCleartextTrafficPermitted() {
return true;
}
}
}
问题是我在实现中注册的回调 (ServerFacade
) 没有被调用。
这是我得到的输出:
okhttp3.mockwebserver.MockWebServer execute
INFO: MockWebServer[50510] starting to accept connections
waiting
Sending request http://localhost:50510/echo on Connection{localhost:50510, proxy=DIRECT hostAddress=localhost/127.0.0.1:50510 cipherSuite=none protocol=http/1.1}
Content-Type: application/json; charset=UTF-8
Content-Length: 15
Host: localhost:50510
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.3.0
okhttp3.mockwebserver.MockWebServer processOneRequest
INFO: MockWebServer[50510] received request: POST /echo HTTP/1.1 and responded: HTTP/1.1 200 OK
Received response for http://localhost:50510/echo in 9.3ms
Content-Length: 15
{"value":"hey"}
okhttp3.mockwebserver.MockWebServer acceptConnections
INFO: MockWebServer[50510] done accepting connections: Socket closed
after wait
Process finished with exit code 0
在互斥对象上添加 wait
之前,这是我得到的:
okhttp3.mockwebserver.MockWebServer execute
INFO: MockWebServer[61085] starting to accept connections
okhttp3.mockwebserver.MockWebServer acceptConnections
INFO: MockWebServer[61085] done accepting connections: Socket closed
Process finished with exit code 0
知道这里发生了什么吗?
很明显,模拟服务器收到了请求并做出了应有的响应。
同样清楚的是,http 客户端收到了它应该收到的响应,但是我的回调(onResponse
或 onFailure
)没有被执行。
这些是我的依赖项:
testCompile 'junit:junit:4.12'
testCompile "org.robolectric:robolectric:3.0"
testCompile 'com.squareup.okhttp3:mockwebserver:3.3.0'
compile 'com.google.code.gson:gson:2.3.1'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
编辑
似乎没有执行回调,因为它们应该在 android 主循环器中执行,而不是 运行。
使用 ShadowLooper.runUiThreadTasks()
我能够调用这些回调,但是每次 closed
.
都会调用 onFailure
回调
然后我决定切换到同步请求,所以我将其添加到 http 客户端:
final Dispatcher dispatcher = new Dispatcher(new AbstractExecutorService() {
private boolean shutingDown = false;
private boolean terminated = false;
@Override
public void shutdown() {
this.shutingDown = true;
this.terminated = true;
}
@NonNull
@Override
public List<Runnable> shutdownNow() {
return new ArrayList<>();
}
@Override
public boolean isShutdown() {
return this.shutingDown;
}
@Override
public boolean isTerminated() {
return this.terminated;
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void execute(Runnable command) {
command.run();
}
});
this.httpClient = new OkHttpClient.Builder()
.addNetworkInterceptor(loggingInterceptor)
.dispatcher(dispatcher)
.build();
现在它同步发生了,但我仍然收到 onFailure
回调消息 closed
我不明白为什么。
玩弄之后,我发现了问题所在:
回调被添加到不是 运行 的主循环程序中,因此它们从未真正执行过。
我用我的日志拦截器消耗了响应主体,然后当我想在回调中获取响应主体时,抛出了一个异常 closed
.
第二部分很容易解决:
Interceptor loggingInterceptor = new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
final long t1 = System.nanoTime();
System.out.println(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers()));
okhttp3.Response response = chain.proceed(request);
final long t2 = System.nanoTime();
final String responseBody = response.body().string();
System.out.println(String.format("Received response for %s in %.1fms%n%s%s", response.request().url(), (t2 - t1) / 1e6d, response.headers(), responseBody));
return response.newBuilder()
.body(ResponseBody.create(response.body().contentType(), responseBody))
.build();
}
};
第一个问题可以(至少)通过两种方式解决:
(1) 添加您自己的 Dispatcher
/ExecutorService
以使异步请求同步:
Dispatcher dispatcher = new Dispatcher(new AbstractExecutorService() {
private boolean shutingDown = false;
private boolean terminated = false;
@Override
public void shutdown() {
this.shutingDown = true;
this.terminated = true;
}
@NonNull
@Override
public List<Runnable> shutdownNow() {
return new ArrayList<>();
}
@Override
public boolean isShutdown() {
return this.shutingDown;
}
@Override
public boolean isTerminated() {
return this.terminated;
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void execute(Runnable command) {
command.run();
}
});
new OkHttpClient.Builder()
.dispatcher(dispatcher)
.build();
(2) 确保主循环器自己执行回调:
// in the ServerFacade
public void echo(final String value, final AtomicBoolean waitOn) {
final EchoRequestData data = new EchoRequestData(value);
this.serverApi.echo(data).enqueue(new Callback<EchoResponseData>() {
@Override
public void onResponse(Call<EchoResponseData> call, Response<EchoResponseData> response) {
System.out.println("***** onResponse *****");
waitOn.set(true);
}
@Override
public void onFailure(Call<EchoResponseData> call, Throwable throwable) {
System.out.println("***** onFailure: " + throwable.getMessage() + " *****");
waitOn.set(true);
}
});
}
和
// in the ServerFacadeTest
@Test
public void testEcho() throws InterruptedException {
final AtomicBoolean callbackCalled = new AtomicBoolean(false);
final String value = "hey";
final String responseBody = "{\"value\":\"" + value + "\"}";
this.mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody));
serverFacade.echo(value, callbackCalled);
while (!callbackCalled.get()) {
Thread.sleep(1000);
ShadowLooper.runUiThreadTasks();
}
}
希望这对以后的其他人有所帮助。
如果有人提出更好的解决方案,我将很乐意学习。
我正在尝试使用服务器为我的 restful api 创建单元测试。
经过一些研究,我想出了一个使用 robolectric 和 OkHttp MockWebServer.
这是我的(简化示例):
api界面:
public interface ServerApi {
@POST("echo")
Call<EchoResponseData> echo(@Body EchoRequestData data);
}
response/request 个数据对象:
public class EchoRequestData {
private final String value;
public EchoRequestData(final String value) {
this.value = value;
}
}
public class EchoResponseData {
private String value;
public EchoResponseData() {}
public String getValue() {
return this.value;
}
}
api 实现:
public class ServerFacade {
private final ServerApi serverApi;
private final OkHttpClient httpClient;
private final Retrofit retrofitClient;
public ServerFacade(final String baseUrl) {
this.httpClient = new OkHttpClient.Builder().addNetworkInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long t1 = System.nanoTime();
System.out.println(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers()));
okhttp3.Response response = chain.proceed(request);
long t2 = System.nanoTime();
System.out.println(String.format("Received response for %s in %.1fms%n%s%s", response.request().url(), (t2 - t1) / 1e6d, response.headers(), response.body().string()));
return response;
}
}).build();
this.retrofitClient = new Retrofit.Builder()
.client(this.httpClient)
.baseUrl(baseUrl.toString())
.addConverterFactory(GsonConverterFactory.create())
.build();
this.serverApi = this.retrofitClient.create(ServerApi.class);
}
public void echo(final String value) {
final EchoRequestData data = new EchoRequestData(value);
this.serverApi.echo(data).enqueue(new Callback<EchoResponseData>() {
@Override
public void onResponse(Call<EchoResponseData> call, Response<EchoResponseData> response) {
System.out.println("onResponse");
}
@Override
public void onFailure(Call<EchoResponseData> call, Throwable throwable) {
System.out.println("onFailure: " + throwable.getMessage());
}
});
}
}
测试:
@RunWith(RobolectricTestRunner.class)
@Config(shadows = ServerFacadeTest.MyNetworkSecurityPolicy.class,
manifest = "src/main/AndroidManifest.xml",
constants = BuildConfig.class,
sdk = 16)
public class ServerFacadeTest {
protected MockWebServer mockServer;
protected ServerFacade serverFacade;
@Before
public void setUp() throws Exception {
this.mockServer = new MockWebServer();
this.mockServer.start();
this.serverFacade = new ServerFacade(this.mockServer.url("/").toString());
}
@Test
public void testEcho() throws InterruptedException {
final Object mutex = new Object();
final String value = "hey";
final String responseBody = "{\"value\":\"" + value + "\"}";
this.mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody));
this.serverFacade.echo(value);
synchronized (mutex) {
System.out.println("waiting");
mutex.wait(2500);
System.out.println("after wait");
}
}
@After
public void tearDown() throws Exception {
this.mockServer.shutdown();
}
@Implements(NetworkSecurityPolicy.class)
public static class MyNetworkSecurityPolicy {
@Implementation
public static NetworkSecurityPolicy getInstance() {
try {
Class<?> shadow = MyNetworkSecurityPolicy.class.forName("android.security.NetworkSecurityPolicy");
return (NetworkSecurityPolicy) shadow.newInstance();
} catch (Exception e) {
throw new AssertionError();
}
}
@Implementation
public boolean isCleartextTrafficPermitted() {
return true;
}
}
}
问题是我在实现中注册的回调 (ServerFacade
) 没有被调用。
这是我得到的输出:
okhttp3.mockwebserver.MockWebServer execute
INFO: MockWebServer[50510] starting to accept connections
waiting
Sending request http://localhost:50510/echo on Connection{localhost:50510, proxy=DIRECT hostAddress=localhost/127.0.0.1:50510 cipherSuite=none protocol=http/1.1}
Content-Type: application/json; charset=UTF-8
Content-Length: 15
Host: localhost:50510
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.3.0
okhttp3.mockwebserver.MockWebServer processOneRequest
INFO: MockWebServer[50510] received request: POST /echo HTTP/1.1 and responded: HTTP/1.1 200 OK
Received response for http://localhost:50510/echo in 9.3ms
Content-Length: 15
{"value":"hey"}
okhttp3.mockwebserver.MockWebServer acceptConnections
INFO: MockWebServer[50510] done accepting connections: Socket closed
after wait
Process finished with exit code 0
在互斥对象上添加 wait
之前,这是我得到的:
okhttp3.mockwebserver.MockWebServer execute
INFO: MockWebServer[61085] starting to accept connections
okhttp3.mockwebserver.MockWebServer acceptConnections
INFO: MockWebServer[61085] done accepting connections: Socket closed
Process finished with exit code 0
知道这里发生了什么吗?
很明显,模拟服务器收到了请求并做出了应有的响应。
同样清楚的是,http 客户端收到了它应该收到的响应,但是我的回调(onResponse
或 onFailure
)没有被执行。
这些是我的依赖项:
testCompile 'junit:junit:4.12'
testCompile "org.robolectric:robolectric:3.0"
testCompile 'com.squareup.okhttp3:mockwebserver:3.3.0'
compile 'com.google.code.gson:gson:2.3.1'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
编辑
似乎没有执行回调,因为它们应该在 android 主循环器中执行,而不是 运行。
使用 ShadowLooper.runUiThreadTasks()
我能够调用这些回调,但是每次 closed
.
onFailure
回调
然后我决定切换到同步请求,所以我将其添加到 http 客户端:
final Dispatcher dispatcher = new Dispatcher(new AbstractExecutorService() {
private boolean shutingDown = false;
private boolean terminated = false;
@Override
public void shutdown() {
this.shutingDown = true;
this.terminated = true;
}
@NonNull
@Override
public List<Runnable> shutdownNow() {
return new ArrayList<>();
}
@Override
public boolean isShutdown() {
return this.shutingDown;
}
@Override
public boolean isTerminated() {
return this.terminated;
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void execute(Runnable command) {
command.run();
}
});
this.httpClient = new OkHttpClient.Builder()
.addNetworkInterceptor(loggingInterceptor)
.dispatcher(dispatcher)
.build();
现在它同步发生了,但我仍然收到 onFailure
回调消息 closed
我不明白为什么。
玩弄之后,我发现了问题所在:
回调被添加到不是 运行 的主循环程序中,因此它们从未真正执行过。
我用我的日志拦截器消耗了响应主体,然后当我想在回调中获取响应主体时,抛出了一个异常
closed
.
第二部分很容易解决:
Interceptor loggingInterceptor = new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
final long t1 = System.nanoTime();
System.out.println(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers()));
okhttp3.Response response = chain.proceed(request);
final long t2 = System.nanoTime();
final String responseBody = response.body().string();
System.out.println(String.format("Received response for %s in %.1fms%n%s%s", response.request().url(), (t2 - t1) / 1e6d, response.headers(), responseBody));
return response.newBuilder()
.body(ResponseBody.create(response.body().contentType(), responseBody))
.build();
}
};
第一个问题可以(至少)通过两种方式解决:
(1) 添加您自己的 Dispatcher
/ExecutorService
以使异步请求同步:
Dispatcher dispatcher = new Dispatcher(new AbstractExecutorService() {
private boolean shutingDown = false;
private boolean terminated = false;
@Override
public void shutdown() {
this.shutingDown = true;
this.terminated = true;
}
@NonNull
@Override
public List<Runnable> shutdownNow() {
return new ArrayList<>();
}
@Override
public boolean isShutdown() {
return this.shutingDown;
}
@Override
public boolean isTerminated() {
return this.terminated;
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void execute(Runnable command) {
command.run();
}
});
new OkHttpClient.Builder()
.dispatcher(dispatcher)
.build();
(2) 确保主循环器自己执行回调:
// in the ServerFacade
public void echo(final String value, final AtomicBoolean waitOn) {
final EchoRequestData data = new EchoRequestData(value);
this.serverApi.echo(data).enqueue(new Callback<EchoResponseData>() {
@Override
public void onResponse(Call<EchoResponseData> call, Response<EchoResponseData> response) {
System.out.println("***** onResponse *****");
waitOn.set(true);
}
@Override
public void onFailure(Call<EchoResponseData> call, Throwable throwable) {
System.out.println("***** onFailure: " + throwable.getMessage() + " *****");
waitOn.set(true);
}
});
}
和
// in the ServerFacadeTest
@Test
public void testEcho() throws InterruptedException {
final AtomicBoolean callbackCalled = new AtomicBoolean(false);
final String value = "hey";
final String responseBody = "{\"value\":\"" + value + "\"}";
this.mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody));
serverFacade.echo(value, callbackCalled);
while (!callbackCalled.get()) {
Thread.sleep(1000);
ShadowLooper.runUiThreadTasks();
}
}
希望这对以后的其他人有所帮助。
如果有人提出更好的解决方案,我将很乐意学习。