【问题标题】:Testing a Fragment class in isolation using Mockito使用 Mockito 单独测试 Fragment 类
【发布时间】:2017-11-20 15:24:02
【问题描述】:

添加了@VisibleForTesting 并受到保护。我的测试现在可以这个方法了:

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

使用间谍对象更新了测试用例:但是,即使我创建了一个将被调用的间谍模拟,真正的 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 认为我可以提供帮助。我尝试过使用间谍。

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(...) 的访问修饰符更改为受保护的、包私有的或公共的。
  • @liminal 我已经用我最近的尝试更新了我的问题。即使我创建了它的间谍对象,我也未能阻止调用真正的方法。

标签: java android unit-testing mockito android-testing


【解决方案1】:

为了防止真正的方法被调用使用: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();
        }
    }

}

【讨论】:

    【解决方案2】:

    有一条通用的经验法则:测试单元的作用比测试它的作用要好得多。

    考虑到这一点,问自己一个问题 - 为什么我首先要模拟 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 注释是一种味道,表明该类不是为了易于测试而编写的。尽管它可以让你设置调用列表,但它只是一个 hack解决根本问题。”
    • 是的,我同意。那么,getter 和 setter 就足够了。我已经更新了我的答案。
    【解决方案3】:

    我想模拟这些调用,因为我不想调用RecyclerView 上的真实对象。我只是想验证一下,setupDataBinding() 被调用了。

    您没有创建足够的接缝来执行此操作。

    如果您声明一个合同,其中描述了“设置数据绑定”将如何发生,该怎么办?换句话说,如果你用void setupDataBinding(...)方法创建一个接口呢?然后RecipeListView 将持有该接口的一个实例作为依赖项。因此,RecipeListView 永远不会知道这个设置将如何发生:它知道一件事 - 他持有的依赖已经“签署了合同”并承担了执行工作的责任。

    通常,您会通过构造函数传递该依赖项,但因为Fragment is a specific case,可以在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"Writing Testable Code" 书,它通过示例和简洁的方式说明了所有技术(38 页)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-11-08
      • 2018-07-01
      • 1970-01-01
      • 2012-02-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多