Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing retrofit 2 with robolectric, callbacks not being called

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'

Edit

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.

like image 718
Nitzan Tomer Avatar asked Jun 19 '16 16:06

Nitzan Tomer


1 Answers

After playing around with things, I found what the problems were:

  1. The callbacks were added to the main looper which wasn't run so they were never actually executed.

  2. 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.

like image 63
Nitzan Tomer Avatar answered Sep 20 '22 14:09

Nitzan Tomer