我正在尝试使用服务器为我的restful api创建单元测试 经过一些研究,我想出了一个使用robolectric和OkHttp MockWebServer的解决方案。
这就是我所拥有的(简化示例):
api界面:
public interface ServerApi {
@POST("echo")
Call<EchoResponseData> echo(@Body EchoRequestData data);
}
响应/请求数据对象:
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$3 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$4 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$3 acceptConnections
INFO: MockWebServer[50510] done accepting connections: Socket closed
after wait
Process finished with exit code 0
在我在互斥对象上添加wait
之前,这就是我得到的:
okhttp3.mockwebserver.MockWebServer$3 execute
INFO: MockWebServer[61085] starting to accept connections
okhttp3.mockwebserver.MockWebServer$3 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()
我可以调用这些回调,但每次都会使用消息onFailure
调用closed
回调。
然后我决定切换到同步请求,所以我将其添加到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
回调,我不明白为什么。
答案 0 :(得分:7)
在玩完东西之后,我发现了问题所在:
回调被添加到主循环中,而没有运行,因此它们从未实际执行过。
我使用我的日志拦截器消耗了响应体,然后当我想在回调中获取响应体时,抛出一个异常,说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();
}
}
希望将来有助于其他人 如果有人想出更好的解决方案,我会很乐意学习。