1
votes

Note: I'm using () instead of angle brackets <> for data types as couldn't find a way to have them working here on the forum.

I have a MVP android application that uses Retofit2 and RxJava2 to get data from the GitHub Api. The code is working fine and I'm able to recover an Observable(Response(List(Headers))), where Response is from Retrofit2 and Headers form OkHttp3.

But when it comes to unit test that, I've encountered an issue: I'm not able to mock a Response(List(Headers)). Retrofit2 Response class has a private constructor so I just can't create an instance of that. I tried then to use OkHttp MockWebServer with the idea of having a MockResponse(List(Headers)). Despite I can set Headers to the MockResponse instance, I wasn't able to get a MockResponse(List(Headers))

My service:

@GET("users/{username}/repos")
Observable<Response<List<Headers>>> checkReposPerUser(@Path("username") String owner,
                                                      @Query("access_token") String accessTokenString,
                                                      @Query("token_type") String accessTokenTypeString,
                                                      @Query("per_page") String perPageValue);

My presenter:

    @Override
public void checkRepoPerUser(String owner) {

    //recovering access token data from Shared Preferences;
    String accessTokenString = repository.getAccessTokenString();
    String accessTokenTypeString = repository.getAccessTokenType();

    //Asking for a list of repositories with 1 repository per page.
    //This let us know how many repositories we found and also to deal with error response code
    Disposable disposable = repository.checkReposPerUser(owner, accessTokenString, accessTokenTypeString, "1")
            .subscribeOn(ioScheduler)
            .observeOn(uiScheduler)
            .subscribe(this::handleReturnedHeaderData, this::handleHeaderError);
    disposeBag.add(disposable);
}

@VisibleForTesting
public void handleReturnedHeaderData(Response<List<Headers>> response) {
    //getting value 'Link' from response headers in order to count the repositories
    String link = response.headers().get("Link");
    String message = response.message();

    //checking GitHub API requests limit
    String limit = response.headers().get("X-RateLimit-Limit");
    Log.d(TAG, "Limit requests: " + limit);
    String limitRemaining = response.headers().get("X-RateLimit-Remaining");
    Log.d(TAG, "Limit requests remaining: " + limitRemaining);

    //getting http response code
    int code = response.code();

    switch (code){
        case 404:
            if(message.equalsIgnoreCase("not found")){ //User not exists
                view.showUserNotFoundMessage();
            }else{
                view.showErrorMessage(message);
            }
            break;
        case 403:
            //GitHub API requests limit reached
            //Instead of showing an error, we start the login process,
            // store another access token in shared Preferences and resend the same request that failed before
            view.startLogin();
            break;
        case 200:
            if(link == null){ //Link value is not present into the header, it means there's 0 or 1 repo
                Log.d(TAG, "Total repos for current user is 0 or 1.");
                //get the repository
                searchRepo(view.getOwner()); //Starting looking for data
            }else if( link != null){
                //get last page number: considering that we requested all the repos paginated with
                //only 1 repo per page, the last page number is equal to the total number of repos
                String totalRepoString = link.substring(link.lastIndexOf("&page=") + 6, link.lastIndexOf(">"));
                Log.d(TAG, "Total repos for current user are " + totalRepoString);

                // TODO once we know how many repositories we have, we can decide how many calls to do (total repositories/100 rounded up )

                //get the repositories
                searchRepo(view.getOwner()); //Starting 3 looking for data
            }
            break;
        default:
            searchRepo(view.getOwner()); //Starting 3 looking for data
            break;
    }
}

Now, I want to unit test handleReturnedHeaderData(Response(List(Headers)) response). This is the Test class:

public class RepositoriesPresenterTest {
    private static final Repo REPO1 = new Repo();
    private static final Repo REPO2 = new Repo();
    private static final Repo REPO3 = new Repo();
    private static final List<Repo> NO_REPOS = Collections.emptyList();
    private static final List<Repo> THREE_REPOS = Arrays.asList(REPO1, REPO2, REPO3);

    public static final String OWNER = "owner";
    public static final String ACCESS_TOKEN_STRING = "access_token_string";
    public static final String ACCESS_TOKEN_TYPE = "access_token_type";
    public static final String PER_PAGE_VALUE = "per_page_value";

    @Parameterized.Parameters
    public static Object[] data() {
        return new Object[] {NO_REPOS, THREE_REPOS};
    }

    @Parameterized.Parameter
    public List<Repo> repos;

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Mock private GitHubChallengeRepository repositoryMock;

    @Mock private RepositoriesContract.View viewMock;

    @Mock private Response<List<Headers>> responseListHeaders;


    private TestScheduler testScheduler;

    private RepositoriesPresenter SUT;  //System Under Test

    @Before public void setUp() {
        testScheduler = new TestScheduler();
        SUT = new RepositoriesPresenter(repositoryMock, viewMock, testScheduler, testScheduler);
    }


}

Into the RepositoriesPresenterTest class I tried:

@Test public void repoPresenter_CheckRepoPerUser_responseHeadersListExpected() throws Exception {

    // Mock a response with status 200 and some Headers
    MockResponse mockResponse = new MockResponse()
            .setResponseCode(200)
            .setBody("{}")
            .setHeader("Status", "status_value")
            .setHeader("X-RateLimit-Limit", "60")
            .setHeader("X-RateLimit-Remaining", "57");
    List<Headers> headersList = Arrays.asList(mockResponse.getHeaders());

    // Given
    given(repositoryMock.getAccessTokenString()).willReturn(ACCESS_TOKEN_STRING);
    given(repositoryMock.getAccessTokenType()).willReturn(ACCESS_TOKEN_TYPE);
    given(repositoryMock.checkReposPerUser(
            OWNER,
            ACCESS_TOKEN_STRING,
            ACCESS_TOKEN_TYPE,
            PER_PAGE_VALUE)).willReturn((Observable<Response<List<Headers>>>) Observable.just(headersList));

    // When
    SUT.checkRepoPerUser(OWNER);
    testScheduler.triggerActions();

    // Then
    then(viewMock).should().getOwner();

}

But it doesn't compile because I can't cast Observable.just(headersList) to an Observable(Response(List(Headers)))).

After that I tried to use the MockWebServer to simulate the connection (even if I think is not necessary for the test):

@Test
public void repoPresenter_CheckRepoPerUser_responseHeadersListExpected() throws InterruptedException, IOException {


    MockWebServer mockWebServer = new MockWebServer();

    TestObserver testObserver = new TestObserver<Response<List<Headers>>>();

    String path = "\"users/{username}/repos\"";

    // Mock a response with status 200 and some Headers
    MockResponse mockResponse = new MockResponse()
            .setResponseCode(200)
            .setBody("{}")
            .setHeader("Status", "status_value")
            .setHeader("X-RateLimit-Limit", "60")
            .setHeader("X-RateLimit-Remaining", "57");
    // Enqueue request
    mockWebServer.enqueue(mockResponse);

    // Call the API
    repositoryMock.checkReposPerUser(
            OWNER,
            ACCESS_TOKEN_STRING,
            ACCESS_TOKEN_TYPE,
            PER_PAGE_VALUE).subscribe((Consumer<? super Response<List<Headers>>>) Observable.just(Arrays.asList(mockResponse.getHeaders()));

    testScheduler.triggerActions();

    testObserver.awaitTerminalEvent(2, TimeUnit.SECONDS);

    // No errors
    testObserver.assertNoErrors();

    // Make sure we made the request to the required path
    assertEquals("60", mockResponse.getHeaders().get("X-RateLimit-Limit"));



    // When
    SUT.checkRepoPerUser(OWNER);
    testScheduler.triggerActions();

    // Then
    then(viewMock).should().getOwner();

    // Shut down the server. Instances cannot be reused.
    mockWebServer.shutdown();
}

but I got a ClassCastException:

java.lang.ClassCastException: io.reactivex.internal.operators.observable.ObservableJust cannot be cast to io.reactivex.functions.Consumer 

Is there any viable way to mock a Response(List(Headers)) or I should start thinking to change the way I get data from the endpoint?

1

1 Answers

1
votes

Creating a mock response is pretty straightforward using retrofit's own Response object. As you said the constructor is private, but you can create successful responses using the static methods:

Response<List<Headers>> response = Response.success(headersList);

If you'd like to create a response that fails with a 401 error for example, you can do it like this:

    ResponseBody theBody = ResponseBody.create(MediaType.parse("text/html"), "");
    Response<List<Headers>> response = Response.error(401, theBody);