android10 / Android-CleanArchitecture

This is a sample app that is part of a series of blog posts I have written about how to architect an android application using Uncle Bob's clean architecture approach.
Apache License 2.0
15.49k stars 3.32k forks source link

Testing the observers #223

Open quanturium opened 7 years ago

quanturium commented 7 years ago

Hi, I would like to test observers, based on the different results my UseCase may return. For example https://github.com/android10/Android-CleanArchitecture/blob/master/presentation/src/main/java/com/fernandocejas/android10/sample/presentation/presenter/UserListPresenter.java

In this case, onComplete(), onError() and onNext() are not tested. I'd like to be able to write a test named "testUserListObserverOnError" and make sure hideViewLoading(), showErrorMessage(), showViewRetry() are called.

I haven't managed to do that since the Observer class is a private inner class. I can not mock it. How would you go about testing this?

urizev commented 7 years ago

I recommend you these links:

I hope they're helpful

quanturium commented 7 years ago

@urizev Thanks for the links, however, they don't explain how to test what the Observer do. In the example I mentioned above, how would you test that the onError() calls some methods on the view?

Bukoow commented 7 years ago

You have some information ?

Maybe see something like a TestRule implementation overriding a TestRule class with theses methods :

public class RxSchedulersOverrideRule implements TestRule {

  @Override public Statement apply(final Statement base, Description d) {
    return new Statement() {
      @Override public void evaluate() throws Throwable {
        RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
          @Override public Scheduler apply(Scheduler scheduler) throws Exception {
            return Schedulers.trampoline();
          }
        });

        RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() {
          @Override public Scheduler apply(Scheduler scheduler) throws Exception {
            return Schedulers.trampoline();
          }
        });

        RxJavaPlugins.setNewThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
          @Override public Scheduler apply(Scheduler scheduler) throws Exception {
            return Schedulers.trampoline();
          }
        });

        RxAndroidPlugins.setInitMainThreadSchedulerHandler(
            new Function<Callable<Scheduler>, Scheduler>() {
              @Override public Scheduler apply(Callable<Scheduler> schedulerCallable)
                  throws Exception {
                return Schedulers.trampoline();
              }
            });

        base.evaluate();
        RxJavaPlugins.reset();
        RxAndroidPlugins.reset();
      }
    };
  }
}

And use this class in presenter test : @RunWith(MockitoJUnitRunner.class) public class PresenterTest extends TestWithRxSchedulersOverrideRule

But nothing works ...

(see : https://medium.com/@peter.tackage/overriding-rxandroid-schedulers-in-rxjava-2-5561b3d14212 https://medium.com/@fabioCollini/testing-asynchronous-rxjava-code-using-mockito-8ad831a16877 )

need help ;)

ultraon commented 7 years ago

First of all you need to understand that rule statement will be called after "before" method. I think you have to call trigger RxAndroidPlugins & RxJavaPlugins above in the static initializer of unit test class (static {...}) or static method annotated with BeforeClass. And don't reset trampoline scheduler, it is redundant.

Bukoow commented 7 years ago

In fact, i've already worked with this type of Android architecture (in a work project), but we used RxJava 1.X and when we called a presenter we didn't have a "use case", we called a presenter method like that :

@Override public void loadData() {
    compositeSubscription.add(dataService.getData()
        .map(new Func1<List<DataEntity>, DataViewModel>() {
          @Override public DataViewModelcall(List<DataEntity> dataEntities) {
            return dataEntitiesToDataViewModelMapper.transform(dataEntities);
          }
        })
        .subscribeOn(Schedulers.io())//
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<DataViewModel>() {
          @Override public void onCompleted() {
            //Do Nothing
          }

          @Override public void onError(Throwable e) {
            view.displayDialogError();
            Timber.e(e, e.getMessage());
          }

          @Override public void onNext(DataViewModel dataViewModel) {
            view.populate(dataViewModel);
          }
        }));
  }
}

And in the unit test, we used the rule that I wrote before (with Rx Java 1 specifications) :

@RunWith(MockitoJUnitRunner.class) public class DataPresenterTest extends TestWithRxSchedulersOverrideRule {
  @Mock DataService dataService;
  @Mock DataEntitiesToDataViewModelMapper dataEntitiesToDataViewModelMapper;
  @Mock DataView view;
  private DataPresenter dataPresenter ;

  @Before public void setUp() throws Exception {
    dataPresenter = new DataPresenter (dataService, dataEntitiesToDataViewModelMapper);
    dataPresenter .attachView(view);
  }

  @After public void tearDown() throws Exception {
    dataPresenter .destroy();
  }

  /***********************************************************************************************/

  @Test public void should_loadAllInfrapole() throws Exception {
    List<DataEntity> dataEntities= new ArrayList<>();
    DataViewModel dataViewModel= new DataViewModel();
    when(dataService.getData()).thenReturn(Observable.just(dataEntities));
    when(dataEntitiesToDataViewModelMapper.transform(dataEntities)).thenReturn(dataViewModel);
    dataPresenter .loadData();

     /// Verification in OnNext method
    verify(view).populate(dataViewModel);
    verify(view, never()).displayDialogError();
  }
}

Here, when I try my solution or yours (called in @BeforeClass all RxAndroidPlugins.setInitMainThreadSchedulerHandler and cie), I always have the error : Wanted but not invoked: view.myMethod(); Like if we don't have access to OnNext or OnError method...

epetrenko commented 6 years ago

@quanturium @Bukoow

Let me share a solution which I usually use in such case.

For example, we have a screen with list of articles. To fetch articles we have an use case GetArticlesUseCase:

public class GetArticlesUseCase extends UseCase<List<Article>> {

    private final Repository repository;

    @Inject
    GetArticlesUseCase(Repository repository) {
        this.repository = repository;
    }

    @Override
    Observable<List<Article>> buildUseCaseObservable() {
        return repository.getArticles();
    }
}

That's our presenter:

public class ArticlesPresenter implements Presenter {

    private final GetArticlesUseCase getArticlesUseCase;

    @Inject
    ArticlesPresenter(GetArticlesUseCase getArticlesUseCase) {
        this.getArticlesUseCase = getArticlesUseCase;
    }    

    ...

    public void loadArticles() {
        getArticlesUseCase.execute(new ArticlesObserver());
    }

    ...

    private final class ArticlesObserver extends DefaultObserver<List<Article>> {

        @Override 
        public void onComplete() {
            view.hideLoading();
        }

        @Override 
        public void onError(Throwable error) {
            view.hideLoading();
            view.showError(error);
        }

        @Override 
        public void onNext(List<Article> articles) {
            view.showArticles(articles);
        }
    }    
}

Then how we can test that, a particular methods will be called from observer callbacks:

public class ArticlesPresenterTest {

    @Mock private ArticlesView articlesView;
    @Mock private GetArticlesUseCase getArticlesUseCase;

    // this is our savior 
    @Captor private ArgumentCaptor<DefaultObserver<List<Article>>> articlesObserverCaptor;

    @InjectMocks private ArticlesPresenter articlesPresenter;

    ...

    @Test
    public void loadArticles_showArticlesOnNoError() {
          articlesPresenter.loadArticles();

          verify(getArticlesUseCase).execute(articlesObserverCaptor);

          // articlesList is a stubbed List<Article> which you want to use for verifying         
          articlesObserverCaptor.getValue().onNext(articlesList);

          verify(articlesView).showArticles(eq(articlesList));
          verify(articlesView).hideLoading();
    }

    @Test
    public void loadArticles_showErrorMessageOnError() {
          articlesPresenter.loadArticles();

          verify(getArticlesUseCase).execute(articlesObserverCaptor);

          // exception is any Throwable which you want to use for verifying         
          articlesObserverCaptor.getValue().onError(exception);

          verify(articlesView).hideLoading();
          verify(articlesView).showError(eq(exception));
    }

    ...

}

After that observer callback methods will be marked as covered by tests.

You can find such approach in the popular android architecture blueprints repository. For example, TasksPresenter and TasksPresenterTest.

droidster commented 6 years ago

Thanks @epetrenko. You're awesome!

garfieldcoked commented 6 years ago

@epetrenko I had the same setup as you outlined but was struggling test. I just discovered "Captor", read up about it but wasnt sure how to use, your solution was perfect in helping with this.

Thank you.