【问题标题】:Unit testing with mockito and spy causing error使用 mockito 和 spy 进行单元测试导致错误
【发布时间】:2017-06-23 10:53:42
【问题描述】:

我正在使用Mockito 和 Spy 对函数进行单元测试。

这是被测试的类:

public class RecipeListModelImp
        implements RecipeListModelContract {

    private Subscription subscription;
    private RecipesAPI recipesAPI;

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

    @Override
    public void getRecipesFromAPI(final RecipeGetAllListener recipeGetAllListener) {
        subscription = recipesAPI.getAllRecipes()
               .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<List<Recipe>>() {
                    @Override
                    public void onCompleted() {
                              }
                    @Override

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

                    }


                    @Override

                    public void onNext(List<Recipe> recipe) {                       recipeGetAllListener.onRecipeGetAllSuccess(recipe);
                    }
                });
    }

    @Override
    public void shutdown() {
        if(subscription != null && !subscription.isUnsubscribed()) {
            subscription.unsubscribe();
        }
    }
}

我正在尝试使用 Mockito 和 spy 进行测试,因为我不想调用真正的函数 recipesAPI.getAllRecipes() 只是验证它。测试称为 testGetRecipesFromAPI()

public class RecipeListModelImpTest {
    @Mock Subscription subscription;
    @Mock RecipesAPI recipesAPI;
    @Mock RecipeListModelContract.RecipeGetAllListener recipeGetAllListener;

    private RecipeListModelContract recipeListModel;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(RecipeListModelImpTest.this);
        recipeListModel = new RecipeListModelImp(recipesAPI);
    }

    @Test
    public void testGetRecipesFromAPI() {
        RecipeListModelContract recipeListModelSpy = spy(recipeListModel);
        RecipesAPI recipeApiSpy = spy(recipesAPI);
        doNothing().when(recipeApiSpy).getAllRecipes();

        recipeListModelSpy.getRecipesFromAPI(recipeGetAllListener);

        verify(recipesAPI, times(1)).getAllRecipes();

    }

    @Test
    public void testShouldShutdown() {
        recipeListModel.shutdown();
        verify(subscription, times(1)).unsubscribe();
    }
}

这是错误:

org.mockito.exceptions.base.MockitoException: 
Only void methods can doNothing()!
Example of correct use of doNothing():
    doNothing().
    doThrow(new RuntimeException())
    .when(mock).someVoidMethod();
Above means:
someVoidMethod() does nothing the 1st time but throws an exception the 2nd time is called

我也试过这个导致空指针:

  @Test
    public void testGetRecipesFromAPI() {
        RecipeListModelContract recipeListModelSpy = spy(recipeListModel);
        RecipesAPI recipeApiSpy = spy(recipesAPI);

        doReturn(Observable.just(Subscription.class)).when(recipeApiSpy).getAllRecipes();

        recipeListModelSpy.getRecipesFromAPI(recipeGetAllListener);

        verify(recipesAPI, times(1)).getAllRecipes();
    }

【问题讨论】:

  • 你在哪一行得到 NullPointer ?

标签: java android unit-testing mockito tdd


【解决方案1】:

代码中的问题是这部分:subscribeOn(Schedulers.io())。如果我们能消除这一点,那么我们就可以从recipesAPI 返回测试数据并测试该数据是否已被recipeGetAllListener 正确处理。

所以,我们必须以某种方式创建一个接缝:如果这是一个生产代码 - 然后使用 Schedulers.io()/AndroidSchedulers.mainThread(),如果这是一个测试代码 - 然后使用一些特定的调度程序。

让我们声明一个接口,它将提供Schedulers:

interface SchedulersProvider { Scheduler getWorkerScheduler(); Scheduler getUiScheduler(); }

现在让RecipeListModelImp 依赖于SchedulersProvider

public class RecipeListModelImp implements RecipeListModelContract { ... private SchedulersProvider schedulersProvider; @Inject public RecipeListModelImp(@NonNull RecipesAPI recipesAPI, @NonNull SchedulersProvider schedulerProvider) { ... this.schedulersProvider = schedulersProvider; } ... }

现在,我们将替换调度程序:

@Override public void getRecipesFromAPI(final RecipeGetAllListener recipeGetAllListener) { subscription = recipesAPI.getAllRecipes() .subscribeOn(schedulersProvider.getWorkerScheduler()) .observeOn(schedulersProvider.getUiScheduler()) ... }

是时候提供SchedulerProvider了:

@Module public class MyModule { ... @Provides public SchedulerProvider provideSchedulerProvider() { return new SchedulerProvider() { @Override Scheduler getWorkerScheduler() { return Schedulers.io(); } @Override Scheduler getUiScheduler() { return AndroidSchedulers.mainThread(); } } } }

现在让我们创建另一个模块 - TestModule,它将为测试类提供依赖项。 TestModule 将扩展 MyModule 并覆盖提供 SchedulerProvider 的方法:

public class TestModule extends MyModule { @Override public SchedulerProvider provideSchedulerProvider() { return new SchedulerProvider() { @Override Scheduler getScheduler() { return Schedulers.trampoline(); } @Override Scheduler getUiScheduler() { return Schedulers.trampoline(); } } } }

Schedulers.trampoline() 将在当前线程上执行任务。

是时候创建测试组件了:

@Component(modules = MyModule.class) public interface TestComponent extends MyComponent { void inject(RecipeListModelImpTest test); }

现在在测试类中:

public class RecipeListModelImpTest { @Mock RecipesAPI recipesAPI; @Mock RecipeListModelContract.RecipeGetAllListener recipeGetAllListener; @Inject SchedulerProvider schedulerProvider; private RecipeListModelContract recipeListModel; @Before public void setup() { TestComponent component = DaggerTestComponent.builder() .myModule(new TestModule()) .build(); component.inject(this); MockitoAnnotations.initMocks(this); recipeListModel = new RecipeListModelImp(recipesAPI, schedulerProvider); } ... }

以及实际测试部分:

    private static final List<Recipe> TEST_RECIPES = new ArrayList<Recipe>() {
        {
            add(new Recipe(1)),
            add(new Recipe(2))
        }
    };

    @Test
    public void testGetRecipesFromAPI() {
        when(recipeAPI.getAllRecipes())
            .thenReturn(Observable.fromIterable(TEST_RECIPES));

        recipeListModel.getRecipesFromAPI(recipeGetAllListener);

        // verifying, that `recipeAPI.getAllRecipes()` has been called once
        verify(recipeAPI).getAllRecipes();

        // verifying, that we received correct result
        verify(recipeGetAllListener).onRecipeGetAllSuccess(TEST_RECIPES);
    }

【讨论】:

  • 这是一个很好的答案,解决了我的问题。这是我使用模拟和使用匕首注入的首选方式。一个简单的问题应该是需要扩展基本组件吗?即public interface TestComponent extends MyComponent
  • 是的,那是因为您可能还有其他要注入的依赖项。如果您扩展基本组件,那么 dagger 将无法提供这些依赖项。
  • 你给了我很好的答案。但是,我在使用 espresso 测试 RxJava2 时遇到了另一个问题,我试图模拟从 API 返回的数据,以便可以单独测试。但是,我在 SubscribeOn(..) 上收到空指针异常我的问题位于此处stackoverflow.com/questions/45717803/…,如果您能提供另一个专家答案,那就太好了。有500积分奖励。提前致谢。
  • 但是,如果我正在为专门针对在 Schedulers.io() 上使用并行执行而发生的错误创建单元测试怎么办?如果我用蹦床替换它,单元测试不会因错误代码而失败。
  • @JustAMartin,您正在处理未正确同步的可变状态,这不应该是这种特殊情况的情况。你能详细描述你的问题吗?
【解决方案2】:

正如你所写的

订阅 = recipesAPI.getAllRecipes().subscribeOn(Schedulers.io())

然后 getAllRecipes() 方法返回一些对象,你不能使用

doNothing().when(recipeApiSpy).getAllRecipes();

doNothing() - 用于方法返回 void。

变体是正确的:

doReturn(doReturn(Observable.just(Subscription.class)).when(recipeApiSpy).getAllRecipes()

【讨论】:

    【解决方案3】:

    您正在混合 Spy(部分模拟)和 Mocks(完整模拟)。这是不必要的 - Spy 允许您混合模拟和真实方法调用,但您不需要任何部分模拟。在您的情况下,您要么完全嘲笑,要么不嘲笑。 Mockito 的documentation 有更多关于模拟和间谍的信息。

    在您的第一个示例中,错误是您尝试在返回某些内容的方法上 doNothing。 Mockito 不允许这样做。您在第二个示例中所做的几乎是正确的。

    对于您的第二个示例,问题是您设置getAllRecipes() 以返回Observable.just(Subscription.class),但您仍然在被测单元中调用了整个方法链:subscribeOnobserveOnsubscribe。您还需要模拟这些调用以返回您可以使用的模拟对象或那些抛出 NullPointerException 的调用。

    @Test
    public void testGetRecipesFromAPI() { 
        //recipesAPI.getAllRecipes() needs to be mocked to return something (likely a mock)
        // so subscribeOn can be called.
        //That needs to be mocked to return something so observeOn can be called
        //etc.
    
        recipeListModel.getRecipesFromAPI(recipeGetAllListener);
    
        verify(recipesAPI, times(1)).getAllRecipes();
    }
    

    【讨论】:

      猜你喜欢
      • 2017-09-19
      • 2019-04-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-13
      • 2020-02-22
      • 1970-01-01
      相关资源
      最近更新 更多