使用Mockito独立测试Fragment类

8

添加了@VisibleForTesting和protected修饰符。我的测试现在可以使用该方法:

   @VisibleForTesting
    protected void setupDataBinding(List<Recipe> recipeList) {
        recipeAdapter = new RecipeAdapter(recipeList);
        RecyclerView.LayoutManager layoutManager
                = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false);
        rvRecipeList.setLayoutManager(layoutManager);
        rvRecipeList.setAdapter(recipeAdapter);
    }

使用spy对象更新测试用例:然而,即使我创建了一个会被调用的spy对象的模拟,真正的setupDataBinding(recipe)也会被调用。也许我做错了什么。

@Test
public void testShouldGetAllRecipes() {
    RecipeListView spy = Mockito.spy(fragment);
    doNothing().when(spy).setupDataBinding(recipe);

    fragment.displayRecipeData(recipe);

    verify(recipeItemClickListener, times(1)).onRecipeItemClick();
}

我正在尝试测试我Fragment类中的方法,如下所示。但是,我试图模拟这些方法以验证调用次数是否正确。但是问题是,我有一个private方法setupDataBinding(...),在从displayRecipeData(...)调用时设置在RecyclerView上。我想模拟这些调用,因为我不想调用RecyclerView上的真实对象。我只想验证是否调用了setupDataBinding(...)

我尝试使用spy和VisibleForTesting,但仍然不确定如何做到这一点。

我正在尝试隔离地测试Fragment。

public class RecipeListView
        extends MvpFragment<RecipeListViewContract, RecipeListPresenterImp>
        implements RecipeListViewContract {

    @VisibleForTesting
    private void setupDataBinding(List<Recipe> recipeList) {
        recipeAdapter = new RecipeAdapter(recipeList);
        RecyclerView.LayoutManager layoutManager
                = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false);
        rvRecipeList.setLayoutManager(layoutManager);
        rvRecipeList.setAdapter(recipeAdapter);
    }

    @Override
    public void displayRecipeData(List<Recipe> recipeList) {
        /* Verify this get called only once */
        setupDataBinding(recipeList);

        recipeItemListener.onRecipeItem();
    }
}

这是我的测试方式。我添加了VisibleForTesting,认为它可以帮助。并且我尝试使用spy。

public class RecipeListViewTest {
    private RecipeListView fragment;
    @Mock RecipeListPresenterContract presenter;
    @Mock RecipeItemListener recipeItemListener;
    @Mock List<Recipe> recipe;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(RecipeListViewTest.this);
        fragment = RecipeListView.newInstance();
    }

    @Test
    public void testShouldGetAllRecipes() {
        fragment.displayRecipeData(recipe);
        RecipeListView spy = Mockito.spy(fragment);

        verify(recipeItemListener, times(1)).onRecipeItem();
    }
}

如何最好地独立测试以上内容?

非常感谢任何建议。


仅添加@VisibleForTesting是不够的。您还必须将setupDataBinding(...)的访问修饰符更改为protected、package-private或public。 - liminal
@liminal 我已经更新了我的问题,并尝试了最新的方法。虽然我创建了一个spy对象,但我仍然无法阻止真实方法被调用。 - ant2009
3个回答

7
为了防止调用真实方法,请使用以下代码:Mockito.doNothing().when(spy).onRecipeItem(); 这里是使用的最小示例:
public class ExampleUnitTest {
    @Test
    public void testSpyObject() throws Exception {
        SpyTestObject spyTestObject = new SpyTestObject();
        SpyTestObject spy = Mockito.spy(spyTestObject);

        Mockito.doNothing().when(spy).methodB();

        spy.methodA();
        Mockito.verify(spy).methodB();
    }

    public class SpyTestObject {

        public void methodA() {
            methodB();
        }
        public void methodB() {
            throw new RuntimeException();
        }
    }

}


3

我想模拟这些调用,因为我不想在RecyclerView上调用真正的对象。我只想验证setupDataBinding()被调用。

你还没有创建足够的接口以执行此操作。

如果你声明一个契约来描述“设置数据绑定”将如何发生会怎样呢?换句话说,如果你创建一个带有方法void setupDataBinding(...)的接口,那么RecipeListView将持有该接口的一个实例作为依赖项。因此,RecipeListView永远不会知道这个设置将以什么方式进行:它只知道——它所持有的依赖关系已经“签署了合同”并承担了执行工作的责任。

通常情况下,你会通过构造函数传递该依赖项,但由于Fragment是一个特殊情况,你可以在onAttach()中获取依赖项:

interface Setupper {
    void setupDataBinding(List<Recipe> recipes, ...);
}

class RecipeListView extends ... {

    Setupper setupper;

    @Override public void onAttach(Context context) {
        super.onAttach(context);

        // Better let the Dependency Injection tool (e.g. Dagger) provide the `Setupper`
        // Or initialize it here (which is not recommended)
        Setupper temp = ...
        initSetupper(temp);
    }

    void initSetupper(Setupper setupper) {
        this.setupper = setupper;
    }

    @Override
    public void displayRecipeData(List<Recipe> recipes) {
        // `RecipeListView` doesn't know what exactly `Setupper` does
        // it just delegates the work
        setupper.setupDataBinding(recipes, ...);

        recipeItemListener.onRecipeItem();
    }
}

这对你有什么好处?现在你拥有了一个“接缝”。现在你不再依赖于实现,而是依赖于合同。
public class RecipeListViewTest {

    @Mock Setupper setupper;
    List<Recipe> recipe = ...; // initialize, no need to mock it
    ...

    private RecipeListView fragment;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        fragment = new RecipeListView();
        fragment.initSetupper(setupper);
    }

    @Test
    public void testShouldGetAllRecipes() {
        fragment.displayRecipeData(recipes);

        // You do not care what happens behind this call
        // The only thing you care - is to test whether is has been executed
        verify(setupper).setupDataBinding(recipe, ...);
        // verify(..) is the same as verify(.., times(1))
    }
}

我强烈建议阅读Misko Hevery"编写可测试代码"书籍,该书通过例子简明扼要地阐述了所有技巧(38页)。

1

有一个常见的经验法则: 测试单元时,与其测试它是如何实现的,不如测试它所做的事情。

考虑到这一点,请问自己一个问题 - 为什么首先要模拟setupDataBinding方法呢?它不会进行任何外部调用,只会改变对象的状态。因此,更好的测试方法是检查它是否以正确的方式改变了状态:

@Test
public void testShouldGetAllRecipes() {
     fragment.displayRecipeData(recipeList);

     // Verifies whether RecipeAdapter has been initialized correctly
     RecipeAdapter recipeAdapter = fragment.getRecipeAdapter();
     assertNotNull(recipeAdapter);
     assertSame(recipeList, recipeAdapter.getRecipeList());

     // Verifies whethr RvRecipeList has been initialized correctly 
     RvRecipeList rvRecipeList = fragment.getRvRecipeList();
     assertNotNull(rvRecipeList);
     assertNotNull(rvRecipeList.getLayoutManager());
     assertSame(fragment.getRecipeAdapter(), rvRecipeList.getAdapter());
}

这可能需要添加几个getter/setter来使整个事情更具可测试性。

Misko Hevery:“通常,@VisibleForTesting注释是一种类没有编写为易于测试的迹象。即使它可以让您设置调用列表,但这只是绕过根本问题的一种方法。” - azizbekian
是的,我同意。那么,getter和setter应该就足够了。我已经更新了我的答案。 - Danylo Zatorsky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接