用robolectric测试改造2,没有调用回调

Nit*_*mer 4 android unit-testing mocking robolectric retrofit

我正在尝试使用服务器为我的restful api创建单元测试.
经过一些研究,我想出了一个使用robolectric和OkHttp MockWebServer的解决方案.

这就是我所拥有的(简化示例):

api接口:

public interface ServerApi {
    @POST("echo")
    Call<EchoResponseData> echo(@Body EchoRequestData data);
}
Run Code Online (Sandbox Code Playgroud)

响应/请求数据对象:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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());
            }
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

考试:

@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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

问题是我ServerFacade没有调用我在implementation()中注册的回调.

这是我得到的输出:

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
Run Code Online (Sandbox Code Playgroud)

在我添加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
Run Code Online (Sandbox Code Playgroud)

有什么想法在这里发生了什么?
很明显,模拟服务器接收请求并按预期响应.很明显,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'
Run Code Online (Sandbox Code Playgroud)

编辑

似乎回调没有被执行,因为他们应该在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();
Run Code Online (Sandbox Code Playgroud)

现在它同步发生,但我仍然得到onFailure回调closed作为消息,我不明白为什么.

Nit*_*mer 7

在玩完东西之后,我发现了问题所在:

  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();
    }
};
Run Code Online (Sandbox Code Playgroud)

(更多信息在这里)

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

(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();
Run Code Online (Sandbox Code Playgroud)

(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);
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

// 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

希望这将有助于其他人.
如果有人想出更好的解决方案,我会很乐意学习.