简体   繁体   English

使用retrofit2和rxjava2单元测试android应用程序

[英]Unit test android application with retrofit2 and rxjava2

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.我有一个 MVP android 应用程序,它使用 Retofit2 和 RxJava2 从 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.代码运行良好,我能够恢复一个 Observable(Response(List(Headers))),其中 Response 来自 Retrofit2,Headers 来自 OkHttp3。

But when it comes to unit test that, I've encountered an issue: I'm not able to mock a Response(List(Headers)).但是当涉及到单元测试时,我遇到了一个问题:我无法模拟 Response(List(Headers))。 Retrofit2 Response class has a private constructor so I just can't create an instance of that. Retrofit2 Response 类有一个私有构造函数,所以我无法创建它的实例。 I tried then to use OkHttp MockWebServer with the idea of having a MockResponse(List(Headers)).然后我尝试使用 OkHttp MockWebServer 的想法是有一个 MockResponse(List(Headers))。 Despite I can set Headers to the MockResponse instance, I wasn't able to get a MockResponse(List(Headers))尽管我可以将 Headers 设置为 MockResponse 实例,但我无法获得 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).现在,我想对 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:进入我尝试过的 RepositoriesPresenterTest 类:

@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))) ).但它无法编译,因为我无法将Observable.just(headersList)转换为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):之后我尝试使用 MockWebServer 来模拟连接(即使我认为测试没有必要):

@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:但我得到了一个 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?是否有任何可行的方法来模拟 Response(List(Headers)) 或者我应该开始考虑改变从端点获取数据的方式?

Creating a mock response is pretty straightforward using retrofit's own Response object.使用改造自己的 Response 对象创建模拟响应非常简单。 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:例如,如果您想创建一个因 401 错误而失败的响应,您可以这样做:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM