java if or android,使用rxjava进行Android单元测试

I recently jumped in Android Unit testing and I'm still struggling in writing my unit tests.

I'm trying to test my Presenter, specifically a method that returns a list of repositories from Github Api,

but I keep getting a Null Pointer Exception and I don't understand why.

RepositoriesPresenter method I want to unit test:

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

}

}

Whole RepositoriesPresenter class:

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> 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 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 NO_REPOS = Collections.emptyList();

private static final List 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();

}

}

This is what I get when I run the test:

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

from the stacktrace it seems I'm not using an argument, what am I doing wrong?

解决方案

In your presenter, loadRemoteRepos is called with a perPageValue of "100", but in your test, the given part of the repositoryMock only matches the parameter value of "per_page_value".

Either match the last parameter with anyString (in this case all the other parameters should be wrapped in an eq matcher), or use the same value in the test as in the presenter code, or inject the value through the constructor of the presenter.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值