53

Can I test real response from retrofit2beta4? Do i need Mockito or Robolectic?

I don't have activities in my project, it will be a library and I need to test is server responding correctly. Now I have such code and stuck...

@Mock
ApiManager apiManager;

@Captor
private ArgumentCaptor<ApiCallback<Void>> cb;

@Before
public void setUp() throws Exception {
    apiManager = ApiManager.getInstance();
    MockitoAnnotations.initMocks(this);
}

@Test
public void test_login() {
    Mockito.verify(apiManager)
           .loginUser(Mockito.eq(login), Mockito.eq(pass), cb.capture());
    // cb.getValue();
    // assertEquals(cb.getValue().isError(), false);
}

I can make fake response, but I need to test real. Is it success? Is it's body correct? Can you help me with code?

AndrewS
  • 7,418
  • 8
  • 35
  • 50

3 Answers3

111

It is generally not a good idea to test real server requests. See this blog post for an interesting discussion on the topic. According to the author, using your real server is a problem because:

  • Another moving piece that can intermittently fail
  • Requires some expertise outside of the Android domain to deploy the server and keep it updated
  • Difficult to trigger error/edge cases
  • Slow test execution (still making HTTP calls)

You can avoid all the issues above by using a mock server such as OkHttp's MockWebServer to simulate real response results. For example:

@Test
public void test() throws IOException {
    MockWebServer mockWebServer = new MockWebServer();

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(mockWebServer.url("").toString())
            //TODO Add your Retrofit parameters here
            .build();

    //Set a response for retrofit to handle. You can copy a sample
    //response from your server to simulate a correct result or an error.
    //MockResponse can also be customized with different parameters
    //to match your test needs
    mockWebServer.enqueue(new MockResponse().setBody("your json body"));

    YourRetrofitService service = retrofit.create(YourRetrofitService.class);

    //With your service created you can now call its method that should 
    //consume the MockResponse above. You can then use the desired
    //assertion to check if the result is as expected. For example:
    Call<YourObject> call = service.getYourObject();
    assertTrue(call.execute() != null);

    //Finish web server
    mockWebServer.shutdown();
}

If you need to simulate network delays, you can customize your response as follows:

MockResponse response = new MockResponse()
    .addHeader("Content-Type", "application/json; charset=utf-8")
    .addHeader("Cache-Control", "no-cache")
    .setBody("{}");
response.throttleBody(1024, 1, TimeUnit.SECONDS);

Alternatively, you can use MockRetrofit and NetworkBehavior to simulate API responses. See here an example of how to use it.

Finally, if you just want to test your Retrofit Service, the easiest would be to create a mock version of it that emits mock results for your tests. For example, if you have the following GitHub service interface:

public interface GitHub {
    @GET("/repos/{owner}/{repo}/contributors")
    Call<List<Contributor>> contributors(
        @Path("owner") String owner,
        @Path("repo") String repo);
}

You can then create the following MockGitHub for your tests:

public class MockGitHub implements GitHub {
    private final BehaviorDelegate<GitHub> delegate;
    private final Map<String, Map<String, List<Contributor>>> ownerRepoContributors;

    public MockGitHub(BehaviorDelegate<GitHub> delegate) {
        this.delegate = delegate;
        ownerRepoContributors = new LinkedHashMap<>();

        // Seed some mock data.
        addContributor("square", "retrofit", "John Doe", 12);
        addContributor("square", "retrofit", "Bob Smith", 2);
        addContributor("square", "retrofit", "Big Bird", 40);
        addContributor("square", "picasso", "Proposition Joe", 39);
        addContributor("square", "picasso", "Keiser Soze", 152);
    }

    @Override public Call<List<Contributor>> contributors(String owner, String repo) {
        List<Contributor> response = Collections.emptyList();
        Map<String, List<Contributor>> repoContributors = ownerRepoContributors.get(owner);
        if (repoContributors != null) {
            List<Contributor> contributors = repoContributors.get(repo);
            if (contributors != null) {
                response = contributors;
            }
        }
        return delegate.returningResponse(response).contributors(owner, repo);
    }
}

You can then use the MockGitHub on your tests to simulate the kinds of responses you are looking for. For the full example, see the implementations of the SimpleService and SimpleMockService for this Retrofit example.

Having said all this, if you absolutely must connect to the actual server, you can set Retrofit to work synchronously with a custom ImmediateExecutor:

public class ImmediateExecutor implements Executor {
    @Override public void execute(Runnable command) {
        command.run();
    }
}

Then apply it to the OkHttpClient you use when building the Retrofit:

OkHttpClient client = OkHttpClient.Builder()
        .dispatcher(new Dispatcher(new ImmediateExecutor()))
        .build();

Retrofit retrofit = new Retrofit.Builder()
        .client(client)
        //Your params
        .build();
Ricardo
  • 7,785
  • 8
  • 40
  • 60
  • All your answer is based on "simulation". As i said on question topic - i can do that. My project is a library that will work with server API. The only thing i need to test - is changing on server, i need to test real responses. – AndrewS Mar 10 '16 at 13:00
  • 2
    I offered alternatives because I think it doesn't make sense to test on a real server. You can't be sure that the test will work in different locations for different users, you can't easily test connections problems, and so on. The server is not something that belongs to your library and should not be treated as such, I think. That's why it is generally better to test server _responses_ instead. If you use a `MockWebServer` you can have your tests run as if connected to the real server. Your library wouldn't know the difference. – Ricardo Mar 10 '16 at 13:57
  • 3
    If i make fake success response - i will get success test. What is the point of this test? I just need to know when server response changed (by tests), to update my library for new response. I will never know that something changed if i make fake response. – AndrewS Mar 10 '16 at 14:02
  • 6
    That's why you need to make success responses and bad responses as well. And test how your library reacts to the different kinds of responses a server can give to your library. With a `MockWebServer` you can and _should_ test as many different types of responses as possible. – Ricardo Mar 10 '16 at 14:08
  • My library has no reaction and no uses of response. It just get data from REST and parse it to Model. I need to test that server is not changing models. I don't need to test my code. How can i test that? is was my question. – AndrewS Mar 10 '16 at 15:17
  • I edited my answer with a way to use `OkHttpClient` and `Retrofit` to connect to the actual server synchronously so you can use them on your robolectric tests. – Ricardo Mar 10 '16 at 16:48
  • 4
    If you need to test that the server is not changing the models then what you should test is the server itself. Your api tests should only depend on your actual api code and mock all the server responses (both correct and incorrect ones) – Alberto S. Mar 10 '16 at 16:59
  • 1
    @Dr.Pelocho server is not mine, but my project is based on that server. That's why i need to be sure that nothing changed there. – AndrewS Mar 11 '16 at 10:16
  • @Ricardo retrofit-mock:2.0.0-beta2 does not have BehaviorDelegate, it exist in the commit logs. Which retoofit mock version are you using? – Milan Mar 16 '16 at 07:36
  • I'm not currently using it like the example above. My versions of GitHubMock simply return the objects I want to test. In any case, BehaviorDelegate is not yet available on Maven. You have to clone the master repository if you want to use it – Ricardo Mar 16 '16 at 11:36
  • 1
    for me: new Retrofit.Builder() ... .build() throws a java.lang.reflect.InvocationTargetException. Anyone know why that would be? – Mike6679 Feb 07 '17 at 14:48
  • 2
    What's the point of endpoint testing if you're not testing against your production environments. If that part fails then good! You can tell your darn API team! Otherwise you are just testing Retrofit.. Pointless. – Oliver Dixon Mar 06 '17 at 08:08
  • Whoever here are supporting to test with real endpoints are forgetting that this is 'Unit testing' and you are expected to test only one part of the whole application. No code can work if it's dependencies are failing, but in unit testing, you need to assume all possible behaviors of dependencies (modules\classes\endpoints\library) and write testcases just to test your peice of code. – Anuj Garg Jan 31 '20 at 13:43
18

The answer is too easy than i expected:

Using CountDownLatch makes your test wait until you call countDown()

public class SimpleRetrofitTest {

private static final String login = "your@login";
private static final String pass = "pass";
private final CountDownLatch latch = new CountDownLatch(1);
private ApiManager apiManager;
private OAuthToken oAuthToken;

@Before
public void beforeTest() {
    apiManager = ApiManager.getInstance();
}

@Test
public void test_login() throws InterruptedException {
    Assert.assertNotNull(apiManager);
    apiManager.loginUser(login, pass, new ApiCallback<OAuthToken>() {
        @Override
        public void onSuccess(OAuthToken token) {
            oAuthToken = token;
            latch.countDown();
        }

        @Override
        public void onFailure(@ResultCode.Code int errorCode, String errorMessage) {
            latch.countDown();
        }
    });
    latch.await();
    Assert.assertNotNull(oAuthToken);
}

@After
public void afterTest() {
    oAuthToken = null;
}}
AndrewS
  • 7,418
  • 8
  • 35
  • 50
1

Unless you are testing QA server API, it is a bad idea for several reason.

  • Firstly you are populating your production database with bad/fake data
  • Using server resources, when they can better used to serve valid request

The best way to use Mockito, or Mock your responses

Also, if you must test your production API, test it once and add @Ignore annotation. That way they are not run all the time and don't spam your server with fake data and you can use it whenever you feel the api isn't behaving correctly.

Akshay
  • 806
  • 11
  • 26