【问题标题】:Testing RxJava2 using Espresso and getting a null pointer exception when suscribeOn使用 Espresso 测试 RxJava2 并在 suscribeOn 时获取空指针异常
【发布时间】:2018-01-24 20:20:03
【问题描述】:
Android Studio 3.0 Beta2

我正在测试使用 RxJava2 获取端点列表。该应用程序在正常运行时运行良好。但是,当我使用 espresso 进行测试时,当我尝试使用 subscribeOn(scheduler) 时出现空指针异常。对于调度程序,我将trampoline() 用于注入的subscribeOnobserveOn

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

对于使用 espresso 测试 RxJava2,对于 subscribeOnobserveOn,我应该做些什么不同的事情?

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

这是我正在测试的课程

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

调度程序的接口在这里,我正在使用注入的蹦床进行测试

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

RecipleAPI 的模拟模块

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

这就是组件的创建方式

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

对于 expresso 测试,我正在执行以下操作:

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

堆栈跟踪:

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)

非常感谢您的任何建议,

【问题讨论】:

  • 你能发布你得到的异常的堆栈跟踪吗?另外,MockRecipeSchedulersModule 在哪里被注入?我只看到 MockRecipeListModule 被注入。
  • @jdonmoyer 抱歉,我包含了错误的模块。它被称为 MockAndroidModule ,其中包含返回调度程序的提供程序(我已经更新了我的问题)。他们被注入的方式是使用构造函数注入。注入公共RecipeListModelImp(NonNull RecipesAPI recipesAPI,NonNull RecipeScheduler recipeScheduler)。在今天晚些时候回家之前,我无法提供堆栈跟踪。谢谢
  • 你们在哪里提供RecipesAPI的测试实现?我只能看到,它被注入到 RecipeListViewAndroidTest 并且是Mock 类型,对吗?此外,我看不到 MockAndroidModule 添加到 TestBusbyBakingComponent 的位置。能否请您详细说明一下?
  • @R.Zagórski 我已经更新了我的问题。是的,RecipeAPI 将返回模拟接口,基本上是模拟(RecipeAPI.class)。我已经添加了这个。我已将 TestBusbyBakingComponent 添加到我的问题中。但是,我觉得问题与传递给 SubscribeOn(...) 的调度程序有关
  • 请发帖RecipeListModelImp

标签: android unit-testing nullpointerexception android-espresso android-instrumentation


【解决方案1】:

您的代码库中存在许多问题。但首先也是最重要的是:您以某种方式实例化了新的真实对象(而不是模拟),这就是您获得 NPE 的原因,与 subscribeOn() 无关。

  1. 您应该使您的测试组件从您的生产组件扩展。目前它不是

    public interface TestRecipeListComponent extends RecipeListComponent {...}
    
  2. 在您的测试应用程序类中,您正在混合回调,即您在 createApplicationComponent 回调中创建 TestRecipeListComponent,但您还有另一个回调:createRecipeListComponent()

  3. 您应该模拟您的 MockRecipeListModule 中的每一个内容。只是模拟出你真正需要模拟出来的组件。例如,如果你 mock RecipeAdapter,那么你怎么会期望 recycler view 在屏幕上绘制任何东西呢?你只需要模拟出数据源提供者,在你的例子中是RecipeApi。除了不应该嘲笑之外,这不是单元测试,这是仪器测试。

  4. RecipeListView#onCreate() 中,您正在创建一个新的RecipeListComponent,而您应该,您应该从Application 类中获取该组件,因为您已经在那里创建了它.这会影响测试:您无法从那里控制依赖项,因为 RecipeListView 只会忽略您从测试中更改的所有依赖项,并将创建一个新组件来提供其他依赖项,因此您的存根将 not 返回您在测试中明确硬编码的数据(实际上它们甚至不会被调用,真实对象会被调用)。这正是您遇到问题的原因。

我已经解决了所有这些问题。我已经到了你写的断言没有通过的地步。您应该不厌其烦地继续这样做,因为它与您正在使用的逻辑/架构有关。

我已经打开了一个拉取请求here

【讨论】:

  • 伟大的工作。我检查了代码,一切正常。我不得不在 TestBusbyBakingApplication 中进行一些小的重构,我在这里创建了一个 PR:github.com/azizbekian/BusbyBaking/pull/1。然而,这回答了我的问题。非常感谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-07
  • 2013-07-27
  • 1970-01-01
  • 2023-03-18
  • 1970-01-01
  • 2018-08-27
相关资源
最近更新 更多