【问题标题】:Unit test android application with rxjava使用 rxjava 对 android 应用程序进行单元测试
【发布时间】:2019-01-03 10:51:46
【问题描述】:

我最近参加了 Android 单元测试,但我仍在努力编写单元测试。 我正在尝试测试我的 Presenter,特别是一种从 Github Api 返回存储库列表的方法, 但我不断收到空指针异常,我不明白为什么。

RepositoriesPresenter 方法我要单元测试:

public void presenterLoadRepos(boolean onlineRequired, String owner) {
    // Clear old data on view
    view.clearRepos();

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

    if(onlineRequired){
        Disposable disposable = repository.loadRemoteRepos(owner, accessTokenString, accessTokenTypeString, PER_PAGE_VALUE)  //this is line 188
                .subscribeOn(ioScheduler) //this is line 189
                .observeOn(uiScheduler)
                .subscribe(this::handleReturnedData, this::handleError, () -> view.stopLoadingIndicator());
        disposeBag.add(disposable);
    }else {
        Disposable disposable = repository.loadLocalRepos(owner, accessTokenString, accessTokenTypeString, PER_PAGE_VALUE)
                .subscribeOn(ioScheduler)
                .observeOn(uiScheduler)
                .subscribe(this::handleReturnedData, this::handleError, () -> view.stopLoadingIndicator());
        disposeBag.add(disposable);
    }

}

整个 RepositoriesPresenter 类:

public class RepositoriesPresenter implements RepositoriesContract.Presenter, LifecycleObserver {

private static final String TAG = RepositoriesPresenter.class.getSimpleName();


private GitHubChallengeRepository repository;

private RepositoriesContract.View view;

private Scheduler ioScheduler;
private Scheduler uiScheduler;

private CompositeDisposable disposeBag;

@Inject
public RepositoriesPresenter(GitHubChallengeRepository repository, RepositoriesContract.View view,
                             @RunOn(IO) Scheduler ioScheduler, @RunOn(UI) Scheduler uiScheduler) {
    this.repository = repository;
    this.view = view;
    this.ioScheduler = ioScheduler;
    this.uiScheduler = uiScheduler;

    // Initialize this presenter as a lifecycle-aware when a view is a lifecycle owner.
    if (view instanceof LifecycleOwner) {
        ((LifecycleOwner) view).getLifecycle().addObserver(this);
    }

    disposeBag = new CompositeDisposable();
}

@Override @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void onAttach() {
    presenterLoadRepos(false, view.getOwner());
}

@Override @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) public void onDetach() {
    // Clean up any no-longer-use resources here
    disposeBag.clear();
}


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

private void handleHeaderError(Throwable error) {
    Log.e(TAG, error.getMessage(), error);
    view.showErrorMessage(error.getLocalizedMessage());
}

@Override public void searchRepo(final String owner) {

    view.showProgressBarIfHidden();

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

    // Load new one and populate it into view
    Disposable disposable = repository.loadRemoteRepos(owner, accessTokenString, accessTokenTypeString, "100")
            .flatMap(Observable::fromIterable)
            .filter(repo -> repo.getName() != null)
            .toList()
            .toObservable()
            .subscribeOn(ioScheduler)
            .observeOn(uiScheduler)
            .subscribe(repos -> {
                if (repos.isEmpty()) {
                    // Clear old data from recycler view
                    view.clearRepos();
                    // Show notification
                    view.showEmptySearchResult();
                } else {
                    // Update recycler view items
                    view.showRepos(repos);

                }
            });

    disposeBag.add(disposable);

}

public void presenterLoadRepos(boolean onlineRequired, String owner) {
    // Clear old data on view
    view.clearRepos();

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

    if(onlineRequired){
        Disposable disposable = repository.loadRemoteRepos(owner, accessTokenString, accessTokenTypeString, "100")
                .subscribeOn(ioScheduler)
                .observeOn(uiScheduler)
                .subscribe(this::handleReturnedData, this::handleError, () -> view.stopLoadingIndicator());
        disposeBag.add(disposable);
    }else {
        // Load new repositories and paginate them with 100 (GitHub API max) repositories par page.
        Disposable disposable = repository.loadLocalRepos(owner, accessTokenString, accessTokenTypeString, "100")
                .subscribeOn(ioScheduler)
                .observeOn(uiScheduler)
                .subscribe(this::handleReturnedData, this::handleError, () -> view.stopLoadingIndicator());
        disposeBag.add(disposable);
    }

}

/**
 * Updates view after loading data is completed successfully.
 */
private void handleReturnedData(List<Repo> list) {
    view.stopLoadingIndicator();
    if (list != null && !list.isEmpty()) {
        view.showRepos(list);
    } else {
        view.showNoDataMessage();
    }
}

/**
 * Updates view if there is an error after loading data from repository.
 */
private void handleError(Throwable error) {
    if(error.getMessage().equalsIgnoreCase("http 403 forbidden")){
        view.startLogin();
    }else {
        view.stopLoadingIndicator();
        view.showErrorMessage(error.getLocalizedMessage());
    }
}

@Override public void getRepo(int repoId) {
    Disposable disposable = repository.getRepo(repoId)
            .filter(repo -> repo != null)
            .subscribeOn(ioScheduler)
            .observeOn(uiScheduler)
            .subscribe(repo -> view.showRepositoryDetail(repo));
    disposeBag.add(disposable);
}
}

RepositoriesPresenterTest:

import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
//other imports omitted

@RunWith(MockitoJUnitRunner.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";

    @Mock private GitHubChallengeRepository repositoryMock;

    @Mock private RepositoriesContract.View viewMock;

    private TestScheduler testScheduler;

    private RepositoriesPresenter SUT;  //System Under Test

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

    @Test public void repoPresenter_reposReturned_showReposOnViewExpected() {
        // Given
        given(repositoryMock.loadRemoteRepos(  //this is line 128
            OWNER,
            ACCESS_TOKEN_STRING,
            ACCESS_TOKEN_TYPE,
        PER_PAGE_VALUE)).willReturn(Observable.just(THREE_REPOS));

        // When
        SUT.presenterLoadRepos(true, OWNER);  //this is line 135
        testScheduler.triggerActions();

        // Then
        then(viewMock).should().showRepos(THREE_REPOS);
        then(viewMock).should(atLeastOnce()).stopLoadingIndicator();
    }
} 

这是我运行测试时得到的结果:

java.lang.NullPointerException
at link.mgiannone.githubchallenge.ui.repositories.RepositoriesPresenter.presenterLoadRepos(RepositoriesPresenter.java:189)
at link.mgiannone.githubchallenge.ui.repositories.RepositoriesPresenterTest.repoPresenter_reposReturned_showReposOnViewExpected(RepositoriesPresenterTest.java:138)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.mockito.internal.runners.DefaultInternalRunner$1.run(DefaultInternalRunner.java:68)
at org.mockito.internal.runners.DefaultInternalRunner.run(DefaultInternalRunner.java:74)
at org.mockito.internal.runners.StrictRunner.run(StrictRunner.java:39)
at org.mockito.junit.MockitoJUnitRunner.run(MockitoJUnitRunner.java:161)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:131)

[MockitoHint] RepositoriesPresenterTest.repoPresenter_reposReturned_showReposOnViewExpected (see javadoc for MockitoHint):
[MockitoHint] 1. Unused... -> at link.mgiannone.githubchallenge.ui.repositories.RepositoriesPresenterTest.repoPresenter_reposReturned_showReposOnViewExpected(RepositoriesPresenterTest.java:131)
[MockitoHint]  ...args ok? -> at link.mgiannone.githubchallenge.ui.repositories.RepositoriesPresenter.presenterLoadRepos(RepositoriesPresenter.java:188)


Process finished with exit code 255

从堆栈跟踪看来我没有使用参数,我做错了什么?

【问题讨论】:

  • 你必须模拟 repository.getAccessTokenString()
  • 嗨 notTdar,我添加了 given(repositoryMock.getAccessTokenString()).willReturn(ACCESS_TOKEN_STRING)given(repositoryMock.getAccessTokenType()).willReturn(ACCESS_TOKEN_TYPE) 但我仍然遇到同样的错误。还有其他建议吗?

标签: android junit mockito


【解决方案1】:

在您的演示者中,loadRemoteRepos 使用 "100"perPageValue 调用,但在您的测试中,repositoryMockgiven 部分仅与 "per_page_value" 的参数值匹配。

要么将最后一个参数与 anyString 匹配(在这种情况下,所有其他参数都应包装在 eq 匹配器中),要么在测试中使用与演示者代码中相同的值,要么通过演示者的构造函数。

【讨论】:

    【解决方案2】:

    由于您的存储库是一个模拟实例,因此您需要模拟其所有方法的响应。这包括这两个:

    String accessTokenString = repository.getAccessTokenString();
    String accessTokenTypeString = repository.getAccessTokenType();
    

    因此,您还需要为它们提供 given 语句。即将上线的东西:

    given(repositoryMock.getAccessTokenString()).willReturn("A string")
    given(repositoryMock.getAccessTokenType()).willReturn("A string")
    

    【讨论】:

    • 我添加了这 2 个 given 语句,但我仍然得到同样的错误。
    • 你添加了什么而不是“字符串”?
    • 我添加了在第三个给定语句中使用的相同常量:ACCESS_TOKEN_STRING 和 ACCESS_TOKEN_TYPE
    • 用整个 RepositoriesPresenter 类更新帖子
    猜你喜欢
    • 2017-02-11
    • 2020-03-07
    • 1970-01-01
    • 1970-01-01
    • 2019-06-02
    • 2012-03-21
    • 2011-03-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多