I am facing with Unit testing for the first time and I would like to know what is the best approach for the following scenario.
I am using Mockito for the tests. The following test is for logic(Presenter) layer and I am trying to verify certain behaviors of the view.
App classes
The method of the Presenter that need to be include in the test:
public void loadWeather() {
CityDetailsModel selectedCity = getDbHelper().getSelectedCityModel();
if (selectedCity != null) {
getCompositeDisposableHelper().execute(
getApiHelper().weatherApiRequest(selectedCity.getLatitude(), selectedCity.getLongitude()),
new WeatherObserver(getMvpView()));
} else {
getMvpView().showEmptyView();
}
}
WeatherObserver:
public class WeatherObserver extends BaseViewSubscriber<DayMvpView, WeatherResponseModel> {
public WeatherObserver(DayMvpView view) {
super(view);
}
#Override public void onNext(WeatherResponseModel weatherResponseModel) {
super.onNext(weatherResponseModel);
if (weatherResponseModel.getData().isEmpty()) {
getMvpView().showEmptyView();
} else {
getMvpView().showWeather(weatherResponseModel.getData());
}
}
}
BaseViewSubscriber (Default DisposableObserver base class to be used whenever we want default error handling):
public class BaseViewSubscriber<V extends BaseMvpView, T> extends DisposableObserver<T> {
private ErrorHandlerHelper errorHandlerHelper;
private V view;
public BaseViewSubscriber(V view) {
this.view = view;
errorHandlerHelper = WeatherApplication.getApplicationComponent().errorHelper();
}
public V getView() {
return view;
}
public boolean shouldShowError() {
return true;
}
protected boolean shouldShowLoading() {
return true;
}
#Override public void onStart() {
if (!AppUtils.isNetworkAvailable(WeatherApplication.getApplicationComponent().context())) {
onError(new InternetConnectionException());
return;
}
if (shouldShowLoading()) {
view.showLoading();
}
super.onStart();
}
#Override public void onError(Throwable e) {
if (view == null) {
return;
}
if (shouldShowLoading()) {
view.hideLoading();
}
if (shouldShowError()) {
view.onError(errorHandlerHelper.getProperErrorMessage(e));
}
}
#Override public void onComplete() {
if (view == null) {
return;
}
if (shouldShowLoading()) {
view.hideLoading();
}
}
#Override public void onNext(T t) {
if (view == null) {
return;
}
}
}
CompositeDisposableHelper (CompositeDisposable helper class):
public class CompositeDisposableHelper {
public CompositeDisposable disposables;
public TestScheduler testScheduler;
#Inject public CompositeDisposableHelper(CompositeDisposable disposables) {
this.disposables = disposables;
testScheduler = new TestScheduler();
}
public <T> void execute(Observable<T> observable, DisposableObserver<T> observer) {
addDisposable(observable.subscribeOn(testScheduler)
.observeOn(testScheduler)
.subscribeWith(observer));
}
public void dispose() {
if (!disposables.isDisposed()) {
disposables.dispose();
}
}
public TestScheduler getTestScheduler() {
return testScheduler;
}
public void addDisposable(Disposable disposable) {
disposables.add(disposable);
}
}
My test:
#Test public void loadSuccessfully() {
WeatherResponseModel responseModel = new WeatherResponseModel();
List<WeatherModel> list = new ArrayList<>();
list.add(new WeatherModel());
responseModel.setData(list);
CityDetailsModel cityDetailsModel = new CityDetailsModel();
cityDetailsModel.setLongitude("");
cityDetailsModel.setLatitude("");
when(dbHelper.getSelectedCityModel()).thenReturn(cityDetailsModel);
when(apiHelper.weatherApiRequest(anyString(), anyString())).thenReturn(
Observable.just(responseModel));
dayPresenter.loadWeather();
compositeDisposableHelper.getTestScheduler().triggerActions();
verify(dayMvpView).showWeather(list);
verify(dayMvpView, never()).showEmptyView();
verify(dayMvpView, never()).onError(anyString());
}
When I try to run the test, I get NullPointer, because new WeatherObserver(getMvpView()) is called, and in the BaseViewSubscriber errorHandlerHelper is null because getApplicationCopomnent is null.
As well NullPointer is thrown in the static method AppUtils.isNetworkAvailable() for the same reason.
When I try to comment these lines, the test is OK.
My questions are:
Should I use Dagger for the Unit test as well or? If yes please give
me example for my test.
Should I use PowerMockito for the static method
AppUtils.isNetworkAvailable()? If yes, is it ok just because of
this method to use PowerMockito Runner
#RunWith(PowerMockRunner.class)?
Should I use Dagger for the Unit test as well or? If yes please give me example for my test.
You don't have to use Dagger necessarily at the test, but that's where Dependency Injection will benefit you, as it will help you strip your dependencies out, and tests will be able to replace them.
Should I use PowerMockito for the static method AppUtils.isNetworkAvailable()? If yes, is it ok just because of this method to use PowerMockito Runner
#RunWith(PowerMockRunner.class)?
Static methods are generally bad for testing, as you cannot replace them (at least not easily and without PowerMock) for testing purposes.
The better practice is to use Dagger for the production code to inject those dependencies, preferably at Constructor, so at tests you can simply provide those dependencies according to test needs (using mocks or fakes where necessary).
In your case, you can add both ErrorHandlerHelper and AppUtils to BaseViewSubscriber Constructor. as BaseViewSubscriber shouldn't be injected, you will need to provide those modules to it from outside, in the presenter, that where you should use Injection to get those Objects. again at the Constructor.
At test, simply replace or provide this objects to the presenter that in it's turn will hand it over to the BaseViewSubscriber.
You can read more about tests seams at android here.
Besides that, it some very odd to me the OO hierarchy of Observer and Disposable that wraps the Observable for getting common behavior, it's essentially breaking the functional stream oriented reactive approach, you might want to consider using patterns like compose using Transformers and using doOnXXX operators do apply common behavior at reactive streams.
Related
I create a unit test for my Presenter. My Presenter implements Listener callback if successfully load data from API (use Interactor):
PresenterTest.java
public class MainContactPresenterTest {
#Mock LoadContactInteractor loadContactInteractor;
#Mock ApiService apiService;
#Mock LoadContactView loadContactView;
#Mock ContactRepository contactRepository;
#Mock LoadContactInteractor.OnLoadDataFinishedListener listener;
#InjectMocks MainContactPresenterImpl presenter;
#Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
#Test
public void getContactLists() {
// given
// when
presenter.fetchRemoteContacts();
// then
Mockito.verify(loadContactInteractor).onLoadData(listener);
}
}
Here is my Presenter:
public class MainContactPresenterImpl implements MainContactPresenter,
LoadContactInteractor.OnLoadDataFinishedListener {
private LoadContactView loadContactView;
private LoadContactInteractor loadContactInteractor;
private ContactRepository contactRepository;
#Inject
public MainContactPresenterImpl(LoadContactInteractor loadContactInteractor,
#NonNull LoadContactView loadContactView,
ContactRepository contactRepository) {
this.loadContactView = loadContactView;
this.loadContactInteractor = loadContactInteractor;
this.contactRepository = contactRepository;
}
#Override
public void onSuccessLoad(List<Contact> contacts) {
loadContactView.saveDataToLocalStorage(contacts);
}
#Override
public void onErrorLoad() {
loadContactView.dismissProgress();
loadContactView.showErrorMessage();
}
#Override
public void preCheckCacheData() {
if (contactRepository.getContactCount() == 0) {
// Load contacts from Server
fetchRemoteContacts();
} else {
fetchLocalContacts();
}
}
#Override
public void fetchRemoteContacts() {
loadContactView.showProgress();
loadContactInteractor.onLoadData(this);
}
}
But when I ran test, I got the mocking parameter in verify not match.
I got my presenter that have to be an argument. Not the listener.
Argument(s) are different! Wanted:
loadContactInteractor.onLoadData(
listener
);
Actual invocation has different arguments:
loadContactInteractor.onLoadData(
fanjavaid.gojek.com.contacts.presenter.MainContactPresenterImpl#1757cd72
);
How to handle that? Thank you
You are creating a mock...
#Mock LoadContactInteractor.OnLoadDataFinishedListener listener;
...and then you don't use it ever again and act suprised when verify tells you, that it wasn't actually used. Why? Of course it wasn't used, since you never use it anywhere, so how should your classes know to use that mock object?
Your MainContactPresenterImpl does not use an OnLoadDataFinishedListener as an external dependency (then your could perhaps inject it via #InjectMocks), it is itself such a listener and thus mocking another listener makes no sense here.
In other words, MainContactPresenterImpl has no OnLoadDataFinishedListener field, so Mockito is of course not capable of injecting something in this non-existing field. For something like this to work, you would need to add such a field and then use the content of that field when calling your onLoadData method.
The only invocation of your method is here...
loadContactInteractor.onLoadData(this);
And what is this in that context? It's the MainContactPresenterImpl object that contains the method, in other words, your presenter.
So, what will work is...
Mockito.verify(loadContactInteractor).onLoadData(presenter);
Is there any way to enforce non-nullability of LiveData values? Default Observer implementation seems to have #Nullable annotation which forces an IDE to suggest that the value might be null and should be checked manually:
public interface Observer<T> {
/**
* Called when the data is changed.
* #param t The new data
*/
void onChanged(#Nullable T t);
}
A new option is available if you use Kotlin. You can replace LiveData with StateFlow. It is more suitable for Kotlin code and provides built-in null safety.
Instead of using:
class MyViewModel {
val data: LiveData<String> = MutableLiveData(null) // the compiler will allow null here!
}
class MyFragment: Fragment() {
model.data.observe(viewLifecycleOwner) {
// ...
}
}
You can use:
class MyViewModel {
val data: StateFlow<String> = MutableStateFlow(null) // compilation error!
}
class MyFragment: Fragment() {
lifecycleScope.launch {
model.data.collect {
// ...
}
}
}
StateFlow is part of coroutines and to use the lifecycleScope you need to add the lifecycle-extensions dependency:
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
Note that this API has been experimental before coroutines 1.4.0.
Here's some additional reading about replacing LiveData with StateFlow.
As Igor Bubelov pointed out, another advantage of this approach is that it's not Android specific so it can be used in shared code in multiplatform projects.
If you use Kotlin, you can create much nicer non-null observe function with extension. There is an article about it. https://medium.com/#henrytao/nonnull-livedata-with-kotlin-extension-26963ffd0333
It's possible to do it safely only if you are in control of the code which sets the data because you'll also have to wrap the LiveData class. This way the data setting methods will be protected with #NonNull and you can be sure that the data has already been checked before reaching the Observer.
Wrap the LiveData class:
public class NonNullMutableLiveData<T> extends MutableLiveData<T> implements NonNullLiveData<T> {
private final #NonNull T initialValue;
public NonNullMutableLiveData(#NonNull T initialValue) {
this.initialValue = initialValue;
}
#Override
public void postValue(#NonNull T value) {
super.postValue(value);
}
#Override
public void setValue(#NonNull T value) {
super.setValue(value);
}
#NonNull
#Override
public T getValue() {
//the only way value can be null is if the value hasn't been set yet.
//for the other cases the set and post methods perform nullability checks.
T value = super.getValue();
return value != null ? value : initialValue;
}
//convenience method
//call this method if T is a collection and you modify it's content
public void notifyContentChanged() {
postValue(getValue());
}
public void observe(#NonNull LifecycleOwner owner, #NonNull NonNullObserver<T> observer) {
super.observe(owner, observer.getObserver());
}
}
Create an interface for exposing as immutable:
public interface NonNullLiveData<T> {
#NonNull T getValue();
void observe(#NonNull LifecycleOwner owner, #NonNull NonNullObserver<T> observer);
}
Finally, wrap the Observer:
//not implementing Observer<T> to make sure this class isn't passed to
//any class other than NonNullMutableLiveData.
public abstract class NonNullObserver<T> {
public Observer<T> getObserver() {
return new ActualObserver();
}
public abstract void onValueChanged(#NonNull T t);
private class ActualObserver implements Observer<T> {
#Override
public void onChanged(#Nullable T t) {
//only called through NonNullMutableLiveData so nullability check has already been performed.
//noinspection ConstantConditions
onValueChanged(t);
}
}
}
Now you can create your data like this:
class DataSource {
private NonNullMutableLiveData<Integer> data = new NonNullMutableLiveData<>(0);
public NonNullLiveData<Integer> getData() {
return data;
}
}
And use it like this:
dataSource.getData().observe(this, new NonNullObserver<Integer>() {
#Override
public void onValueChanged(#NonNull Integer integer) {
}
});
Completely null safe.
While there a few things you can do, it is your responsibility to make sure you don't pass null to the LiveData. In addition to that, every 'solution' is more a suppression of the warning, which can be dangerous (if you do get a null value, you might not handle it and Android Studio will not warn you).
Assert
You can add assert t != null;. The assert will not be executed on Android, but Android Studio understands it.
class PrintObserver implements Observer<Integer> {
#Override
public void onChanged(#Nullable Integer integer) {
assert integer != null;
Log.d("Example", integer.toString());
}
}
Suppress the warning
Add an annotation to suppress the warning.
class PrintObserver implements Observer<Integer> {
#Override
#SuppressWarnings("ConstantConditions")
public void onChanged(#Nullable Integer integer) {
Log.d("Example", integer.toString());
}
}
Remove the annotation
This also works in my installation of Android Studio, but it might not work for you, but you could try to just remove the #Nullable annotation from the implementation:
class PrintObserver implements Observer<Integer> {
#Override
public void onChanged(Integer integer) {
Log.d("Example", integer.toString());
}
}
Default methods
It's unlikely you can use this on Android, but purely from a Java perspective, you could define a new interface and add a null check in a default method:
interface NonNullObserver<V> extends Observer<V> {
#Override
default void onChanged(#Nullable V v) {
Objects.requireNonNull(v);
onNonNullChanged(v);
// Alternatively, you could add an if check here.
}
void onNonNullChanged(#NonNull V value);
}
fun <T> LiveData<T>.observeNonNull(owner: LifecycleOwner, observer: (t: T) -> Unit) {
this.observe(owner, Observer {
it?.let(observer)
})
}
You would have to do some additional work to handle null values that come from the library itself.
For example, when you return a LiveData from a #Dao in Room, like:
#Dao interface UserDao {
#get:Query("SELECT * FROM users LIMIT 1")
val user: LiveData<User>
}
And observe the user live data, it will call the onChanged callback with a null value if there is no user.
I am currently learning Dagger, RxJava, Realm and MVP in a simple project.
Basically what this app can do is it can view, add, delete and update data from database, which I'm using Realm.
I have decided to follow MVP architecture and applied repository pattern as well for data manipulation at the back end layer.
For an extra learning, I added Dagger for the dependency injection in the architecture.
Before this, I have developed an app without applying MVP nor repository pattern, not even Dagger and RxJava in mind. All seems to work well without any errors from Realm threading system. Maybe because I tied everything in a single class.
So, now that I'm moving away from that approach, I'm now having trouble implementing it in the new approach, which I think is more loosely coupled and should be better if implemented correctly.
Enough of introduction, let's get back to the topic.
The issue I'm facing right now is Realm always giving me this error:
Exception has been thrown: Realm accessed from incorrect thread.
I was suspecting that my Dagger graph isn't properly managed (especially on providing Realm instance), thus whenever I make query for data, it gives me the error.
So, my Dagger component looks like this:
#Singleton
#Component(modules = {ContextModule.class, RepositoryModule.class, PresenterModule.class})
public interface AppComponent {
/* Inejct application */
void inject(FourdoApp fourdoApp);
/* Realm Helper */
void inject(DatabaseRealm databaseRealm);
/* Activity */
void inject(MainActivity mainActivity);
void inject(TaskDetailActivity taskDetailActivity);
/* Presenter*/
void inject(MainPresenter mainPresenter);
void inject(TaskDetailPresenter taskDetailPresenter);
/* Model repository*/
void inject(TaskRepositoryImpl taskRepository);
}
Inside RepositoryModule.class;
#Module
public class RepositoryModule {
#Provides
#Singleton
Repository<Task> provideTaskRepository() {
return new TaskRepositoryImpl();
}
// Here I provide DatabaseRealm class instance
#Provides
#Singleton
public DatabaseRealm provideDatabaseRealm() {
return new DatabaseRealm();
}
}
Not sure whether I did this correctly or not. You can view the source for DI here.
For the data request to happen, inside MainActivity, I injected MainPresenter and call onRequestData interface to request it from the Presenter. From there, Presenter will make the call to Repository for the said data.
public class MainActivity extends BaseActivity implements MainContract.View {
#Inject
MainPresenter mainPresenter;
// ...
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Injecting MainActivity class
Injector.getAppComponent().inject(this);
mainPresenter.attachView(this);
// Requesting for data from Presenter
mainPresenter.onRequestData();
}
// ...
#Override
public void onRequestDataSuccess(List<String> taskList) {
doAdapter.addAll(taskList);
doAdapter.notifyDataSetChanged();
}
}
Inside MainPresenter, I injected Repository interface to make request from TaskRepositoryImpl for the real data from database.
public class MainPresenter extends BasePresenter<MainContract.View> implements MainContract.Presenter {
#Inject
Repository<Task> taskRepository;
public MainPresenter() {
Injector.getAppComponent().inject(this);
}
#Override
public void onRequestData() {
requestData();
}
private void requestData() {
taskRepository.findAll()
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.map(this::mapToStringList)
.subscribe(new Observer<List<String>>() {
#Override
public void onNext(List<String> strings) { // Error in this line
if (strings.size() > 0) {
mView.onRequestDataSuccess(strings);
} else {
mView.showEmpty();
}
}
});
}
}
Inside TaskRepositoryImpl, here is how I did the findAll and it should return data from DatabaseRealm:
#Override
public Observable<List<Task>> findAll() {
return Observable.create(subscriber -> {
try {
List<Task> models = databaseRealm.findAll(Task.class);
subscriber.onNext(models);
subscriber.onComplete();
} catch (Exception e) {
subscriber.onError(e);
}
});
}
Code for DatabaseRealm are as follows:
public class DatabaseRealm {
#Inject
Context context;
RealmConfiguration realmConfiguration;
public DatabaseRealm() {
Injector.getAppComponent().inject(this);
}
public void setup() {
if (realmConfiguration == null) {
Realm.init(context);
realmConfiguration = new RealmConfiguration.Builder()
.deleteRealmIfMigrationNeeded()
.build();
Realm.setDefaultConfiguration(realmConfiguration);
} else {
throw new IllegalStateException("Realm already configured");
}
}
public Realm getRealmInstance() {
return Realm.getDefaultInstance();
}
public <T extends RealmObject> T add(T model) {
Realm realm = getRealmInstance();
realm.beginTransaction();
realm.copyToRealm(model);
realm.commitTransaction();
return model;
}
public <T extends RealmObject> T update(T model) {
Realm realm = getRealmInstance();
realm.beginTransaction();
realm.copyToRealmOrUpdate(model);
realm.commitTransaction();
return model;
}
public <T extends RealmObject> T remove(T model) {
Realm realm = getRealmInstance();
realm.beginTransaction();
realm.copyToRealm(model);
realm.deleteAll();
realm.commitTransaction();
return model;
}
public <T extends RealmObject> List<T> findAll(Class<T> clazz) {
return getRealmInstance().where(clazz).findAll();
}
public void close() {
getRealmInstance().close();
}
}
Full source code for this flawed code is right here.
I'd like to make it clear that I have limited knowledge on Realm instances being used in Dagger.
I followed this tutorial for the Repository Design Pattern with Realm, but it doesn't include Dagger for its dependency injection.
Can someone guide me on why it is always telling I'm calling Realm from incorrect thread?
I think you get this error because of this:
#Override
public Observable<List<Task>> findAll() {
return Observable.create(subscriber -> {
try {
List<Task> models = databaseRealm.findAll(Task.class);
subscriber.onNext(models);
subscriber.onComplete();
} catch (Exception e) {
subscriber.onError(e);
}
});
}
You are subscribing to io Thread but you inject your databaseRealm in Main Thread.
if you get instance in your observable's create you'll not get this error.
#Override
public Observable<List<Task>> findAll() {
return Observable.create(subscriber -> {
try {
Realm realm = getRealmInstance();
List<Task> models = realm.findAll(Task.class);
subscriber.onNext(models);
subscriber.onComplete();
} catch (Exception e) {
subscriber.onError(e);
}
});
}
You need to setup RealmConfigration only once in the Application class and use the Realm.getDeafultInstance() method to access Realm Database
With Dagger you need to Pass only realm instance in constructor
You can follow this Example and fork it
Its not exactly the same code you posted here.But it might help to understand dagger better with MVP ,RxJava and Realm
I'm trying to test a presenter that use RxJava to retrieve data from an interactor. In the setup method I'm doing something like:
#Before
public void setup() {
RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
#Override
public Scheduler getMainThreadScheduler() {
return Schedulers.immediate();
}
});
}
So in my test method I can test the presenter call:
#Test
public void testLoad() {
presenter.load();
verify(view).dataLoaded(data);
verify(interactor).load();
}
If I run the test with Android Studio everything work as expected, the issue is that If I try on command line
gradle test
Then the test fails because:
Actually, there were zero interactions with this mock.
So I've tried to put a Thread.sleep(2000) right after the call to the presenter and then it works, so I guess the Schedulers.immediate(); is not working from command line but I have no idea why and how to debug/fix. Do you have any idea?
EDIT: presenter implementation ->
public void load() {
Observable<List<Data>> obs = interactor.load()
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io());
obs.subscribe(new Observer<List<Data>>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(List<Data> data) {
view.dataLoaded(data);
}
});
}
You can mock RxJava Schedulers as well.
RxJavaHooks.reset();
RxJavaHooks.setOnIOScheduler(scheduler -> Schedulers.immediate());
Typically a good thing to call reset on setup & teardown.
I am trying to test my ViewModel in my application, here is the constructor:
#Inject
public SearchUserViewModel(#Named("searchUser") UseCase searchUserUseCase) {
this.searchUserUseCase = searchUserUseCase;
}
In my test I create a SearchUserUseCase with mocks like this:
Observable error = Observable.error(new Throwable("Error"));
when(gitHubService.searchUser(MockFactory.TEST_USERNAME_ERROR)).thenReturn(error);
when(ObserverThread.getScheduler()).thenReturn(Schedulers.immediate());
when(SubscriberThread.getScheduler()).thenReturn(Schedulers.immediate());
searchUserUseCase = new SearchUserUseCase(gitHubService, SubscriberThread, ObserverThread);
In my ViewModel class I have this snippet which I want to test:
public void onClickSearch(View view) {
loadUsers();
}
private void loadUsers() {
if (username == null) {
fragmentListener.showMessage("Enter a username");
} else {
showProgressIndicator(true);
searchUserUseCase.execute(new SearchUserSubscriber(), username);
}
}
private final class SearchUserSubscriber extends DefaultSubscriber<SearchResponse> {
#Override
public void onCompleted() {
showProgressIndicator(false);
}
#Override
public void onError(Throwable e) {
showProgressIndicator(false);
fragmentListener.showMessage("Error loading users");
}
#Override
public void onNext(SearchResponse searchResponse) {
List<User> users = searchResponse.getUsers();
if (users.isEmpty()) {
fragmentListener.showMessage("No users found");
} else {
fragmentListener.addUsers(users);
}
}
}
Finally in my test I have this:
#Test
public void shouldDisplayErrorMessageIfErrorWhenLoadingUsers() {
SearchUserViewModel searchUserViewModel = new SearchUserViewModel(searchUserUseCase);
searchUserViewModel.setFragmentListener(mockFragmentListener);
searchUserViewModel.setUsername(MockFactory.TEST_USERNAME_ERROR);
searchUserViewModel.onClickSearch(view);
verify(mockFragmentListener).showMessage("Error loading users");
}
I get this error from Mockito:
Wanted but not invoked:
fragmentListener.showMessage(
"Error loading users"
);
I am not sure if this is a good test, but I somehow want to test the SearchUserSubscriber one way or another. Thanks
Edit: I have found similar questions to this problem here: Can't verify mock method call from RxJava Subscriber (which still isn't answered) and here: Verify interactions in rxjava subscribers. The latter question is similar but does not execute the subscriber in a separate class (which happens in SearchUserUseCase here).
I also tried RobolectricGradleTestRunner instead of MockitoJunitRunner and changed to Schedulers.io() and AndroidSchedulers.mainThread(), but I still get the same error.
Tried mocking SearchUserUseCase instead of GitHubService (which feels cleaner), but I'm not sure on how to test the subscriber that way since that is passed as an argument to the void method execute() in UseCase.
public void execute(Subscriber useCaseSubscriber, String query) {
subscription = buildUseCase(query)
.observeOn(postExecutionThread.getScheduler())
.subscribeOn(threadExecutor.getScheduler())
.subscribe(useCaseSubscriber);
}
And buildUseCase()
#Override
public Observable buildUseCase(String username) throws NullPointerException {
if (username == null) {
throw new NullPointerException("Query must not be null");
}
return getGitHubService().searchUser(username);
}
For me it worked out to add a Observable.Transformer<T, T> as followed:
void gatherData() {
service.doSomeMagic()
.compose(getSchedulerTransformer())
.subscribe(view::displayValue);
}
private <T> Observable.Transformer<T, T> getSchedulerTransformer() {
if (mTransformer == null) {
mTransformer = (Observable.Transformer<T, T>) observable -> observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
return mTransformer;
}
void setSchedulerTransformer(Observable.Transformer<Observable<?>, Observable<?>> transformer) {
mTransformer = transformer;
}
And to set the Transformer. I just passed this:
setSchedulerTransformer(observable -> {
if (observable instanceof Observable) {
Observable observable1 = (Observable) observable;
return observable1.subscribeOn(Schedulers.immediate())
.observeOn(Schedulers.immediate());
}
return null;
});
So just add a #Before method in your test and call presenter.setSchedulerTransformer and it should be able to test this. If you want more detail check this answer.
If you are using Mockito, you can probably get hold of a SearchUserSubscriber using an ArgumentCaptor, for example...
#Captor
private ArgumentCaptor<SearchUserSubscriber> subscriberCaptor;
private SearchUserSubscriber getSearchUserSubscriber() {
// TODO: ...set up the view model...
...
// Execute the code under test (making sure the line 'searchUserUseCase.execute(new SearchUserSubscriber(), username);' gets hit...)
viewModel.onClickSearch(view);
verify(searchUserUseCase).execute(subscriberCaptor.capture(), any(String.class));
return subscriberCaptor.getValue();
}
Now you can have test cases such as...
#Test
public void shouldDoSomethingWithTheSubscriber() {
SearchUserSubscriber subscriber = getSearchUserSubscriber();
...
}