8
Android Studio 3.0 Beta2 

Testuję pobieranie listy dla punktu końcowego za pomocą RxJava2. Aplikacja działa poprawnie, gdy działa normalnie. Jednak gdy testuję przy użyciu espresso, otrzymuję wyjątek wskaźnika pustego, gdy próbuję i subscribeOn(scheduler). Dla planistów używam trampoline() zarówno dla subscribeOn i observeOn, które są wstrzykiwane.Testowanie RxJava2 przy użyciu Espresso i uzyskiwanie wyjątku wskaźnika pustego, gdy suscribeOn

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 

Do testowania RxJava2 używając espresso jest coś zrobić, że jest inna dla subscribeOn i observeOn?

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

To moja klasa badanego

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

Interfejs dla planistów jest tu i do testów używam trampolina, który jest wtryskiwany

@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 moduł do RecipleAPI

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

W ten sposób elementy są tworzone

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

A do testu expresso robie następujące: trace

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

stosu:

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) 

Wielkie dzięki za wszelkie sugestie,

+1

Czy możesz opublikować ślad stosu wyjątku, który otrzymujesz? Ponadto, gdzie jest wstrzykiwany MockRecipeSchedulersModule? Widzę tylko wstrzyknięcie MockRecipeListModule. – jdonmoyer

+0

@jdonmoyer Przepraszamy, dodałem zły moduł. Jest nazywany MockAndroidModule, który zawiera dostawców, którzy zwracają harmonogramy (zaktualizowałem moje pytanie). Sposób ich wstrzykiwania polega na użyciu wstrzyknięcia konstruktora. Inject public RecipeListModelImp (NonNull RecipesAPI recipesAPI, NonNull RecipeSchedulers recipeSchedulers). Nie mogę dostarczyć śladu stosu, dopóki nie wrócę do domu później. Dzięki – ant2009

+1

Gdzie zapewniasz testową implementację RecipesAPI? Widzę tylko, że jest wstrzyknięty do RecipeListViewAndroidTest i ma typ "Mock", prawda? Ponadto nie widzę, gdzie 'MockAndroidModule' jest dodawany do' TestBusbyBakingComponent'. Czy mógłbyś bardziej szczegółowo to wyjaśnić? –

Odpowiedz

6

Istnieje wiele problemów w twojej bazie kodów. Ale przede wszystkim chodzi o: tworzysz instancje nowych obiektów rzeczywistych (nie mock) i właśnie dlatego uzyskujesz NPE, nie ma to nic wspólnego z subscribeOn().

  1. Powinieneś rozszerzyć komponent testowy na komponent produkcyjny. Obecnie jest to , a nie.

    public interface TestRecipeListComponent extends RecipeListComponent {...} 
    
  2. W swojej klasie aplikacji testowych mieszanie wywołań zwrotnych, tj tworzysz TestRecipeListComponent ciągu createApplicationComponent zwrotnego, ale masz inny oddzwanianie do tej operacji: createRecipeListComponent().

  3. Powinieneś nie wyśmiewać każdego z wszystkiego w swoim MockRecipeListModule. Po prostu wyśmiewaj komponent, który naprawdę musisz wyszydzić. Na przykład, jeśli kpisz z wersji RecipeAdapter, to w jaki sposób możesz oczekiwać widoku recyklera, aby narysować coś na ekranie? Musisz tylko wyłudzić dostawcę źródła danych, który w twoim przypadku to RecipeApi. Poza tym nic nie powinno być wyśmiewane, to nie jest test jednostkowy, to jest test oprzyrządowania.

  4. ciągu RecipeListView#onCreate() tworzysz nową RecipeListComponent, natomiast należy nie, to powinien dostać tego składnika z klasy Application, bo masz już go tam. Ma to wpływ na testy: nie można kontrolować zależności z tego miejsca, ponieważ RecipeListView po prostu zignorowałoby wszystkie zależności zmienione w wyniku testów i utworzy nowy komponent, który zapewni inne zależności, a zatem Państwa kody pośredniczące będą , a nie zwracać danych, które jawnie zakodowane w teście (w rzeczywistości nie będą nawet wywoływane, prawdziwe obiekty będą). Właśnie z tym wystąpiłeś.

Naprawiłem to wszystko. Doszedłem do punktu, w którym stwierdzenie, które napisałeś, nie przechodzi. Powinieneś wziąć kłopot, aby kontynuować, ponieważ jest to związane z logiką/architekturą, której używasz.

Mam otwarte żądanie pobrania here.

+0

Świetna robota. Sprawdziłem kod i wszystko działało dobrze. Musiałem dokonać drobnego refaktoryzacji w TestBusbyBakingApplication i stworzyłem PR tutaj: https://github.com/azizbekian/BusbyBaking/pull/1. Jednak to odpowiedział na moje pytanie. Wielkie dzięki. – ant2009