I'm trying to create unit tests for my restful api with the server.
After some research I came up with a solution that uses robolectric and the OkHttp MockWebServer.
Here's what I have (simplified example):
The api interface:
public interface ServerApi {
@POST("echo")
Call<EchoResponseData> echo(@Body EchoRequestData data);
}
The response/request data objects:
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;
}
}
The api implementation:
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());
}
});
}
}
The test:
@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;
}
}
}
The problem is that the callbacks that I register in the implementation (ServerFacade
) are not being called.
This is the output I get:
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
Before I added the wait
on the mutex object, this is what I got:
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
Any ideas what's going on here?
It's clear that the mock server receives the request and responds as it should.
It's also clear that the http client receives the response as it should, but my callbacks (the onResponse
or onFailure
) are not being executed.
These are my dependencies:
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'
It seems that the callbacks aren't executed because they are supposed to do so in the android main looper, which wasn't running.
Using ShadowLooper.runUiThreadTasks()
I was able to make those callbacks getting called, but the onFailure
callback is called every time with message closed
.
Then I decided to switch to synchronous requests, so I'm adding this to the http client:
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();
Now it happens synchronously alright, but I still get the onFailure
callback with closed
as the message and I don't understand why.
After playing around with things, I found what the problems were:
The callbacks were added to the main looper which wasn't run so they were never actually executed.
I consumed the response body with my logging interceptor, and then when I wanted to get the response body in the callback an exception was thrown saying closed
.
The 2nd part is easily solved like so:
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();
}
};
(more info here)
The first problem can be solved in (at least) 2 ways:
(1) Add your own Dispatcher
/ExecutorService
to make the async requests synced:
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) Make sure the main looper executes the callbacks yourself:
// 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);
}
});
}
And
// 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();
}
}
Hopefully this will help others in the future.
If anyone comes up with better solutions I'll be happy to learn.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With