使用 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 客户端收到了它应该收到的响应,但是我的回调(onResponseonFailure)没有被执行。

这些是我的依赖项:

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 我不明白为什么。

玩弄之后,我发现了问题所在:

  1. 回调被添加到不是 运行 的主循环程序中,因此它们从未真正执行过。

  2. 我用我的日志拦截器消耗了响应主体,然后当我想在回调中获取响应主体时,抛出了一个异常 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();
    }
};

(more info here)

第一个问题可以(至少)通过两种方式解决:

(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();
    }
}

希望这对以后的其他人有所帮助。
如果有人提出更好的解决方案,我将很乐意学习。