使用rxjava进行Android单元测试

时间:2019-01-03 10:51:47

标签: android junit mockito

我最近参加了Android单元测试,但仍在编写单元测试中挣扎。 我正在尝试测试Presenter,特别是一种从Github Api返回存储库列表的方法, 但是我一直收到Null Pointer Exception,但我不明白为什么。

我要进行单元测试的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

从stacktrace看来我没有使用参数,我在做什么错了?

2 个答案:

答案 0 :(得分:1)

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

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

因此,您也需要为其提供给出的声明。线路上的东西:

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

答案 1 :(得分:1)

在您的演示者中,loadRemoteRepos中的perPageValue被调用"100",但是在您的测试中,given的{​​{1}}部分仅与参数值repositoryMock

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