使用 mockito 和 spy 进行单元测试导致错误
Unit testing with mockito and spy causing error
我正在使用 Mockito
和 Spy 对函数进行单元测试。
这是正在测试的class:
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 和间谍进行测试,因为我不想调用真正的函数 recipesAPI.getAllRecipes()
只是验证 it.The 测试被调用 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();
}
如你所写
subscription = recipesAPI.getAllRecipes().subscribeOn(Schedulers.io())
然后方法 getAllRecipes() returns 一些对象,你不能使用
doNothing().when(recipeApiSpy).getAllRecipes();
doNothing() - 用于方法 returns void.
变体是正确的:
doReturn(doReturn(Observable.just(Subscription.class)).when(recipeApiSpy).getAllRecipes()
您正在混合使用 Spy(部分模拟)和 Mocks(完整模拟)。这是不必要的 - Spy
允许您混合模拟和真实方法调用,但您不需要任何部分模拟。在您的情况下,您要么完全嘲笑,要么不嘲笑。 Mockito 的 documentation 有更多关于模拟和间谍的信息。
在您的第一个示例中,错误是您正在尝试 doNothing
一个 return 的方法。 Mockito 不允许这样做。你在第二个例子中所做的几乎是正确的。
对于您的第二个示例,问题是您将 getAllRecipes()
设置为 return 和 Observable.just(Subscription.class)
,但您仍然在被测单元中调用了整个方法链:subscribeOn
、observeOn
和 subscribe
。您还需要模拟这些调用以及对 return 您可以使用的模拟对象或那些抛出 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();
}
代码中的问题是这部分:subscribeOn(Schedulers.io())
。如果我们能消除这个,那么我们就可以从 recipesAPI
返回一个测试数据并测试这个数据是否被 recipeGetAllListener
正确处理了。
因此,我们必须以某种方式创建接缝:如果这是生产代码 - 然后使用 Schedulers.io()
/AndroidSchedulers.mainThread()
,如果这是测试代码 - 然后使用一些特定的调度程序。
让我们声明一个接口,它将提供 Scheduler
s:
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
,它将为测试 classes 提供依赖项。 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);
}
正在测试中class:
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);
}
我正在使用 Mockito
和 Spy 对函数进行单元测试。
这是正在测试的class:
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 和间谍进行测试,因为我不想调用真正的函数 recipesAPI.getAllRecipes()
只是验证 it.The 测试被调用 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();
}
如你所写
subscription = recipesAPI.getAllRecipes().subscribeOn(Schedulers.io())
然后方法 getAllRecipes() returns 一些对象,你不能使用
doNothing().when(recipeApiSpy).getAllRecipes();
doNothing() - 用于方法 returns void.
变体是正确的:
doReturn(doReturn(Observable.just(Subscription.class)).when(recipeApiSpy).getAllRecipes()
您正在混合使用 Spy(部分模拟)和 Mocks(完整模拟)。这是不必要的 - Spy
允许您混合模拟和真实方法调用,但您不需要任何部分模拟。在您的情况下,您要么完全嘲笑,要么不嘲笑。 Mockito 的 documentation 有更多关于模拟和间谍的信息。
在您的第一个示例中,错误是您正在尝试 doNothing
一个 return 的方法。 Mockito 不允许这样做。你在第二个例子中所做的几乎是正确的。
对于您的第二个示例,问题是您将 getAllRecipes()
设置为 return 和 Observable.just(Subscription.class)
,但您仍然在被测单元中调用了整个方法链:subscribeOn
、observeOn
和 subscribe
。您还需要模拟这些调用以及对 return 您可以使用的模拟对象或那些抛出 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();
}
代码中的问题是这部分:subscribeOn(Schedulers.io())
。如果我们能消除这个,那么我们就可以从 recipesAPI
返回一个测试数据并测试这个数据是否被 recipeGetAllListener
正确处理了。
因此,我们必须以某种方式创建接缝:如果这是生产代码 - 然后使用 Schedulers.io()
/AndroidSchedulers.mainThread()
,如果这是测试代码 - 然后使用一些特定的调度程序。
让我们声明一个接口,它将提供 Scheduler
s:
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
,它将为测试 classes 提供依赖项。 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);
}
正在测试中class:
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);
}