【问题标题】:Unit test android application with retrofit2 and rxjava2使用 retrofit2 和 rxjava2 对 android 应用程序进行单元测试
【发布时间】:2019-06-02 01:15:05
【问题描述】:

注意:我使用 () 而不是尖括号 来表示数据类型,因为无法找到让它们在论坛上工作的方法。

我有一个 MVP android 应用程序,它使用 Retofit2 和 RxJava2 从 GitHub Api 获取数据。代码运行良好,我能够恢复 Observable(Response(List(Headers))),其中 Response 来自 Retrofit2,Headers 来自 OkHttp3。

但是在进行单元测试时,我遇到了一个问题:我无法模拟 Response(List(Headers))。 Retrofit2 Response 类有一个私有构造函数,所以我无法创建它的实例。 然后我尝试使用 OkHttp MockWebServer 来实现 MockResponse(List(Headers))。尽管我可以将 Headers 设置为 MockResponse 实例,但我无法获得 MockResponse(List(Headers))

我的服务:

@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);

我的主持人:

    @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;
    }
}

现在,我想对 handleReturnedHeaderData(Response(List(Headers)) response) 进行单元测试。这是测试类:

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


}

进入我试过的 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();

}

但它无法编译,因为我无法将 Observable.just(headersList) 转换为 一个Observable(Response(List(Headers))))。

之后我尝试使用 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();
}

但我遇到了 ClassCastException:

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

是否有任何可行的方法来模拟 Response(List(Headers)) 或者我应该开始考虑改变从端点获取数据的方式?

【问题讨论】:

    标签: android unit-testing retrofit2


    【解决方案1】:

    使用改造自己的 Response 对象创建模拟响应非常简单。正如您所说,构造函数是私有的,但您可以使用静态方法创建成功的响应:

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

    如果您想创建一个失败的响应,例如 401 错误,您可以这样做:

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

    【讨论】:

    • 哦。我应该注意到这一点。它奏效了,谢谢。我会接受你的解决方案。如果你能给我一些其他建议,那会很有帮助,因为测试仍然以与this one 相同的方式失败
    猜你喜欢
    • 2018-11-15
    • 2016-06-15
    • 1970-01-01
    • 2017-02-11
    • 2012-03-21
    • 2011-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多