使用 rxjava 对安卓应用程序进行单元测试



我最近跳进了Android单元测试,但我仍然在编写单元测试时苦苦挣扎。我正在尝试测试我的演示器,特别是一种从 Github API 返回存储库列表的方法,但我不断收到空指针异常,我不明白为什么。

存储库演示器方法 我想进行单元测试:

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

整个存储库演示器类:

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

存储库演示器测试:

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

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

在演示器中,调用loadRemoteReposperPageValue"100" ,但在测试中,repositoryMockgiven部分仅与参数值匹配 "per_page_value"

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

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

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

所以你也需要给他们给出的陈述。线上的东西:

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

相关内容

  • 没有找到相关文章