8

Testen von RxJava2 mit Espresso und Abrufen einer Nullzeigerausnahme bei Verwendung von "SubscribeOn"

Ich teste gerade das Abrufen einer Liste für einen Endpunkt mit RxJava2. Die App funktioniert einwandfrei, wenn sie normal läuft. Wenn ich jedoch einen Espresso benutze, bekomme ich eine Nullzeiger-Ausnahme, wenn ich es versuche und subscribeOn(scheduler). Für die Scheduler benutze ich die trampoline() sowohl für als auch observeOn, die injiziert werden.

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 

Zum Testen RxJava2 Espresso verwendet, ist es etwas, was ich, dass andere für die subscribeOn und observeOn tun sollte?

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

Das ist meine Klasse im 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(); 
     } 
    } 
} 

Die Schnittstelle für die Disponenten hier ist und zum Testen ich Trampolin bin mit dem

injiziert wird
@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-Modul für RecipleAPI

@Module 
public class MockNetworkModule { 
    @Singleton 
    @Provides 
    public RecipesAPI providesRecipeAPI() { 
     return Mockito.mock(RecipesAPI.class); 
    } 
} 
Dies ist 210, wie die Komponenten

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

erstellt und für den Expresso Test, den ich tue folgende:

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

Stapelüberwachung:

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) 

Vielen Dank für Ihre Anregungen,

+1

Können Sie den Stack-Trace der Ausnahme, die Sie erhalten, veröffentlichen? Wo wird MockRecipeSchedulersModule auch injiziert? Ich sehe nur MockRecipeListModule, das injiziert wird. – jdonmoyer

+0

@jdonmoyer Entschuldigung, ich habe das falsche Modul eingefügt. Es heißt MockAndroidModule, das die Provider enthält, die die Scheduler zurückgeben (Ich habe meine Frage aktualisiert). Die Art, wie sie injiziert werden, benutzt die Konstruktorinjektion. Inject public RecipeListModelImp (NonNull RecipesAPI rezepteAPI, NonNull RecipeSchedulers recipeScheduler). Ich kann keinen Stack-Trace bereitstellen, bis ich später nach Hause komme. Danke – ant2009

+1

Wo bieten Sie Test-Implementierung von RecipesAPI? Ich kann nur sehen, dass es in RecipeListViewAndroidTest injiziert wird und vom 'Mock' Typ ist, richtig? Außerdem sehe ich nicht, wo 'MockAndroidModule' zu' TestBusbyBakingComponent' hinzugefügt wird. Könnten Sie das bitte näher ausführen? –

Antwort

6

Es gibt zahlreiche Probleme in Ihrer Codebasis. Aber in erster Linie folgt folgendes: Sie inspizieren irgendwie neue reale Objekte (keine Mocks) und deshalb erhalten Sie NPE, es gibt nichts mit subscribeOn() zu tun.

  1. Sie sollten Ihre Testkomponente von Ihrer Produktionskomponente erweitern. Derzeit ist es nicht.

    public interface TestRecipeListComponent extends RecipeListComponent {...} 
    
  2. In Ihrem Testanwendungsklasse Sie Rückrufe mischen, das heißt Sie TestRecipeListComponent innerhalb createApplicationComponent Rückruf erstellen, aber Sie einen anderen Rückruf haben, dass zu tun: createRecipeListComponent().

  3. Sie sollten nicht mock out jedes ein alles in Ihrem MockRecipeListModule. Nur spotten Komponente, die Sie wirklich verspotten müssen. Wenn Sie zum Beispiel RecipeAdapter vortäuschen, wie können Sie erwarten, dass die Recycler-Ansicht alles auf dem Bildschirm zeichnet? Sie müssen nur den Datenquellenanbieter ausspionieren, der in Ihrem Fall RecipeApi ist. Ansonsten sollte nichts verspottet werden, dies ist kein Unit-Test, das ist Instrumentierungstest.

  4. Innerhalb RecipeListView#onCreate() Sie erstellen einen neuen RecipeListComponent, während Sie sollten nicht, können Sie diese Komponente aus der Application Klasse bekommen sollte, weil Sie bereits dort geschaffen haben. Dies wirkt sich auf die Tests aus: Sie können keine Abhängigkeiten von dort steuern, da RecipeListView alle Abhängigkeiten ignoriert, die Sie aus Tests geändert haben, und eine neue Komponente erstellt, die andere Abhängigkeiten bereitstellt. Ihre Stubs würden also die Daten, die Sie haben, zurückgeben haben im Test explizit hart codiert (tatsächlich werden sie nicht einmal aufgerufen, reale Objekte wären). Genau davon haben Sie das Problem erfahren.

Ich habe das alles behoben. Ich bin an einen Punkt gekommen, an dem die Behauptung, die du geschrieben hast, nicht besteht. Sie sollten sich die Mühe machen, damit fortzufahren, weil es mit der Logik/Architektur verbunden ist, die Sie verwenden.

Ich habe eine Pull-Anforderung here geöffnet.

+0

Große Arbeit. Ich habe den Code überprüft und alles hat funktioniert. Ich musste in TestBusbyBakingApplication ein wenig Refactoring durchführen und habe hier eine PR erstellt: https://github.com/azizbekian/BusbyBaking/pull/1. Dies beantwortete jedoch meine Frage. Vielen Dank. – ant2009

Verwandte Themen