使用 Espresso 测试 RxJava2 并在 suscribeOn 时出现空指针异常
Testing RxJava2 using Espresso and getting a null pointer exception when suscribeOn
Android Studio 3.0 Beta2
我正在测试使用 RxJava2 获取端点列表。该应用程序在 运行 正常时工作正常。但是,当我使用 espresso 进行测试时,我在尝试 subscribeOn(scheduler)
时遇到空指针异常。对于调度程序,我对注入的 subscribeOn
和 observeOn
使用 trampoline()
。
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,对于 subscribeOn
和 observeOn
有什么我应该做的不同吗?
@Singleton
@Component(modules = {
MockNetworkModule.class,
MockAndroidModule.class,
MockExoPlayerModule.class
})
public interface TestBusbyBakingComponent extends BusbyBakingComponent {
TestRecipeListComponent add(MockRecipeListModule mockRecipeListModule);
}
这是我的class正在测试
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)
非常感谢您的任何建议,
您的代码库中存在许多问题。但首先也是最重要的是:你正在以某种方式实例化新的真实对象(不是模拟对象),这就是你得到 NPE 的原因,与 subscribeOn()
.
无关
您应该使测试组件从生产组件扩展。目前是不是。
public interface TestRecipeListComponent extends RecipeListComponent {...}
在您的测试应用程序 class 中,您正在混合回调,即您在 createApplicationComponent
回调中创建 TestRecipeListComponent
,但您有另一个回调来执行此操作: createRecipeListComponent()
.
你应该而不是模拟你MockRecipeListModule
中的每一件事。只是模拟组件,你真的需要模拟。例如,如果你模拟 RecipeAdapter
,那么你怎么会期望回收器视图在屏幕上绘制任何东西?您只需要模拟数据源提供程序,在您的情况下是 RecipeApi
。除此之外,什么都不应该模拟,这不是单元测试,这是仪器测试。
在 RecipeListView#onCreate()
中,您正在创建一个新的 RecipeListComponent
,而您应该 而不是 ,您应该从 Application
class,因为你已经在那里创建了它。这对测试有影响:你不能从那里控制依赖关系,因为 RecipeListView
只会忽略你从测试中更改的所有依赖关系,并会创建一个新组件来提供其他依赖关系,因此你的存根会 not return 您在测试中明确硬编码的数据(实际上它们甚至不会被调用,真正的对象会被调用)。这正是您遇到问题的原因。
我已经解决了所有这些问题。我已经到了你写的断言没有通过的地步。你应该不厌其烦地继续这个,因为它与你正在使用的 logics/architecture 相关联。
我打开了拉取请求 here。
Android Studio 3.0 Beta2
我正在测试使用 RxJava2 获取端点列表。该应用程序在 运行 正常时工作正常。但是,当我使用 espresso 进行测试时,我在尝试 subscribeOn(scheduler)
时遇到空指针异常。对于调度程序,我对注入的 subscribeOn
和 observeOn
使用 trampoline()
。
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,对于 subscribeOn
和 observeOn
有什么我应该做的不同吗?
@Singleton
@Component(modules = {
MockNetworkModule.class,
MockAndroidModule.class,
MockExoPlayerModule.class
})
public interface TestBusbyBakingComponent extends BusbyBakingComponent {
TestRecipeListComponent add(MockRecipeListModule mockRecipeListModule);
}
这是我的class正在测试
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)
非常感谢您的任何建议,
您的代码库中存在许多问题。但首先也是最重要的是:你正在以某种方式实例化新的真实对象(不是模拟对象),这就是你得到 NPE 的原因,与 subscribeOn()
.
您应该使测试组件从生产组件扩展。目前是不是。
public interface TestRecipeListComponent extends RecipeListComponent {...}
在您的测试应用程序 class 中,您正在混合回调,即您在
createApplicationComponent
回调中创建TestRecipeListComponent
,但您有另一个回调来执行此操作:createRecipeListComponent()
.你应该而不是模拟你
MockRecipeListModule
中的每一件事。只是模拟组件,你真的需要模拟。例如,如果你模拟RecipeAdapter
,那么你怎么会期望回收器视图在屏幕上绘制任何东西?您只需要模拟数据源提供程序,在您的情况下是RecipeApi
。除此之外,什么都不应该模拟,这不是单元测试,这是仪器测试。在
RecipeListView#onCreate()
中,您正在创建一个新的RecipeListComponent
,而您应该 而不是 ,您应该从Application
class,因为你已经在那里创建了它。这对测试有影响:你不能从那里控制依赖关系,因为RecipeListView
只会忽略你从测试中更改的所有依赖关系,并会创建一个新组件来提供其他依赖关系,因此你的存根会 not return 您在测试中明确硬编码的数据(实际上它们甚至不会被调用,真正的对象会被调用)。这正是您遇到问题的原因。
我已经解决了所有这些问题。我已经到了你写的断言没有通过的地步。你应该不厌其烦地继续这个,因为它与你正在使用的 logics/architecture 相关联。
我打开了拉取请求 here。