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

时间:2016-06-19 16:16:07

标签: android unit-testing mocking retrofit robolectric

我正在尝试使用服务器为我的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客户端会收到响应,但我的回调(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()我可以调用这些回调,但每次都会使用消息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回调,我不明白为什么。

1 个答案:

答案 0 :(得分:7)

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

  1. 回调被添加到主循环中,而没有运行,因此它们从未实际执行过。

  2. 我使用我的日志拦截器消耗了响应体,然后当我想在回调中获取响应体时,抛出一个异常,说closed

  3. 第二部分很容易解决:

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

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