ant2009 ant2009 - 27 days ago 6
Android Question

Testing RxJava2 using Espresso and getting a null pointer exception when suscribeOn

Android Studio 3.0 Beta2


I am testing getting a list for an endpoint using RxJava2. The app works fine when running normally. However, when I test using espresso I get a null pointer exception when I try and
subscribeOn(scheduler)
. For the schedulers I use the
trampoline()
for both
subscribeOn
and
observeOn
which are injected.

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'io.reactivex.Observable io.reactivex.Observable.subscribeOn(io.reactivex.Scheduler)' on a null object reference


For testing RxJava2 using espresso is there anything I should do that is different for the
subscribeOn
and
observeOn
?

@Singleton
@Component(modules = {
MockNetworkModule.class,
MockAndroidModule.class,
MockExoPlayerModule.class
})
public interface TestBusbyBakingComponent extends BusbyBakingComponent {
TestRecipeListComponent add(MockRecipeListModule mockRecipeListModule);
}


This is my class under test

public class RecipeListModelImp
implements RecipeListModelContract {

private RecipesAPI recipesAPI;
private RecipeSchedulers recipeSchedulers;
private CompositeDisposable compositeDisposable = new CompositeDisposable();

@Inject
public RecipeListModelImp(@NonNull RecipesAPI recipesAPI, @NonNull RecipeSchedulers recipeSchedulers) {
this.recipesAPI = Preconditions.checkNotNull(recipesAPI);
this.recipeSchedulers = Preconditions.checkNotNull(recipeSchedulers);
}

@Override
public void getRecipesFromAPI(final RecipeGetAllListener recipeGetAllListener) {
compositeDisposable.add(recipesAPI.getAllRecipes()
.subscribeOn(recipeSchedulers.getBackgroundScheduler()) /* NULLPOINTER EXCEPTION HERE */
.observeOn(recipeSchedulers.getUIScheduler())
.subscribeWith(new DisposableObserver<List<Recipe>>() {
@Override
protected void onStart() {}

@Override
public void onNext(@io.reactivex.annotations.NonNull List<Recipe> recipeList) {
recipeGetAllListener.onRecipeGetAllSuccess(recipeList);
}

@Override
public void onError(Throwable e) {
recipeGetAllListener.onRecipeGetAllFailure(e.getMessage());
}

@Override
public void onComplete() {}
}));
}

@Override
public void releaseResources() {
if(compositeDisposable != null && !compositeDisposable.isDisposed()) {
compositeDisposable.clear();
compositeDisposable.dispose();
}
}
}


The interface for the schedulers is here and for testing I am using trampoline which is injected

@Module
public class MockAndroidModule {
@Singleton
@Provides
Context providesContext() {
return Mockito.mock(Context.class);
}

@Singleton
@Provides
Resources providesResources() {
return Mockito.mock(Resources.class);
}

@Singleton
@Provides
SharedPreferences providesSharedPreferences() {
return Mockito.mock(SharedPreferences.class);
}

@Singleton
@Provides
RecipeSchedulers provideRecipeSchedulers() {
return new RecipeSchedulers() {
@Override
public Scheduler getBackgroundScheduler() {
return Schedulers.trampoline();
}

@Override
public Scheduler getUIScheduler() {
return Schedulers.trampoline();
}
};
}
}


Mock Module for RecipleAPI

@Module
public class MockNetworkModule {
@Singleton
@Provides
public RecipesAPI providesRecipeAPI() {
return Mockito.mock(RecipesAPI.class);
}
}


This is how the components are created

public class TestBusbyBakingApplication extends BusbyBakingApplication {
private TestBusbyBakingComponent testBusbyBakingComponent;
private TestRecipeListComponent testRecipeListComponent;

@Override
public TestBusbyBakingComponent createApplicationComponent() {
testBusbyBakingComponent = createTestBusbyBakingComponent();
testRecipeListComponent = createTestRecipeListComponent();

return testBusbyBakingComponent;
}

private TestBusbyBakingComponent createTestBusbyBakingComponent() {
testBusbyBakingComponent = DaggerTestBusbyBakingComponent.builder()
.build();

return testBusbyBakingComponent;
}

private TestRecipeListComponent createTestRecipeListComponent() {
testRecipeListComponent = testBusbyBakingComponent.add(new MockRecipeListModule());
return testRecipeListComponent;
}
}


And for the expresso test I am doing the following:

@RunWith(MockitoJUnitRunner.class)
public class RecipeListViewAndroidTest {
@Inject RecipesAPI recipesAPI;

@Mock RecipeListModelContract.RecipeGetAllListener mockRecipeListener;

@Rule
public ActivityTestRule<MainActivity> mainActivity =
new ActivityTestRule<>(
MainActivity.class,
true,
false);

@Before
public void setup() throws Exception {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
BusbyBakingApplication busbyBakingApplication =
(BusbyBakingApplication)instrumentation.getTargetContext().getApplicationContext();

TestBusbyBakingComponent component = (TestBusbyBakingComponent)busbyBakingApplication.createApplicationComponent();
component.add(new MockRecipeListModule()).inject(this);
}

@Test
public void shouldReturnAListOfRecipes() throws Exception {
List<Recipe> recipeList = new ArrayList<>();
Recipe recipe = new Recipe();
recipe.setName("Test Brownies");
recipe.setServings(10);
recipeList.add(recipe);

when(recipesAPI.getAllRecipes()).thenReturn(Observable.just(recipeList));
doNothing().when(mockRecipeListener).onRecipeGetAllSuccess(recipeList);

mainActivity.launchActivity(new Intent());

onView(withId(R.id.rvRecipeList)).check(matches(hasDescendant(withText("Test Brownies"))));
}
}


Stack trace:

at me.androidbox.busbybaking.recipieslist.RecipeListModelImp.getRecipesFromAPI(RecipeListModelImp.java:37)
at me.androidbox.busbybaking.recipieslist.RecipeListPresenterImp.retrieveAllRecipes(RecipeListPresenterImp.java:32)
at me.androidbox.busbybaking.recipieslist.RecipeListView.getAllRecipes(RecipeListView.java:99)
at me.androidbox.busbybaking.recipieslist.RecipeListView.onCreateView(RecipeListView.java:80)
at android.support.v4.app.Fragment.performCreateView(Fragment.java:2192)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1299)
at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1528)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1595)
at android.support.v4.app.BackStackRecord.executeOps(BackStackRecord.java:758)
at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2363)
at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2149)
at android.support.v4.app.FragmentManagerImpl.optimizeAndExecuteOps(FragmentManager.java:2103)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:2013)
at android.support.v4.app.FragmentController.execPendingActions(FragmentController.java:388)
at android.support.v4.app.FragmentActivity.onStart(FragmentActivity.java:607)
at android.support.v7.app.AppCompatActivity.onStart(AppCompatActivity.java:178)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1237)
at android.support.test.runner.MonitoringInstrumentation.callActivityOnStart(MonitoringInstrumentation.java:544)
at android.app.Activity.performStart(Activity.java:6268)


Many thanks for any suggestions,

Answer Source

There are numerous of problems in your codebase. But first and foremost is following: you are somehow instantiating new real objects (not mocks) and that's why you are getting NPE, there is nothing to do with subscribeOn().

  1. You should make your test component extend from your production component. Currently it is not.

    public interface TestRecipeListComponent extends RecipeListComponent {...}
    
  2. In your test application class you are mixing callbacks, i.e. you are creating TestRecipeListComponent within createApplicationComponent callback, but you have another callback for doing that: createRecipeListComponent().

  3. You should not mock out each an everything in your MockRecipeListModule. Just mock out component, that you really need to mock out. For example, if you mock RecipeAdapter, then how come you expect recycler view to draw anything on the screen? You just need to mock out data source provider, which in your case is RecipeApi. Other than that nothing should be mocked out, this is not a unit test, this is instrumentation test.

  4. Within RecipeListView#onCreate() you are creating a new RecipeListComponent, whereas you should not, you should get that component from the Application class, because you have already created it there. This affect on the tests: you cannot control dependencies from there, because RecipeListView would just ignore all the dependencies you have changed from tests and will create a new component which will provide other dependencies, thus your stubs would not return the data that you have explicitly hard coded in test (in fact they won't be even called, real objects would be). This is exactly what you were experiencing the issue from.

I've fixed all of this. I've come to a point where the assertion you wrote does not pass. You should take the hassle to continue with this, because it is connected with the logics/architecture you are using.

I've opened a pull request here.