【问题标题】:Mock objects in Android not passed as parametersAndroid中的模拟对象未作为参数传递
【发布时间】:2014-04-25 22:09:34
【问题描述】:

我正在尝试测试我在 Android 中创建的 Fragment。我对代码有完全的控制权,所以我可以在我认为合适的时候改变它。问题是我不确定我缺少什么设计模式才能使其合理。

我正在寻找一种方法来模拟 Android 中不作为参数传递的对象。 This question 建议您可能要模拟的任何内容都应编写为作为参数传递。

这在某些情况下是有意义的,但我不知道如何让它在 Android 上运行,其中一些是不可能的。例如,使用Fragment,您不得不让大部分繁重的工作在回调方法中完成。如何将我的模拟对象放入 Fragment?

例如,在这个ListFragment 中,我需要检索要显示给用户的一系列内容。我正在显示的内容需要动态检索并添加到自定义适配器中。目前看起来如下:

public class MyFragment extends ListFragment {

  private List<ListItem> mList;

  void setListValues(List<ListItem> values) {
    this.mList = values;
  }

  List<ListItem> getListValues() {
    return this.mList;
  }

  @Override
  public void onCreateView(LayoutInflater i, ViewGroup vg, Bundle b) {
    // blah blah blah
  }

  @Override
  public void onViewCreated(View view, Bundle savedInstanceState) {
    this.setListValues(ListFactory.getListOfDynamicValues());
    CustomAdapter adapter = new CustomAdapter(
        getActivity(),
        R.layout.row_layout,
        this.getListValues());
    this.setListAdapter(adapter);
  }

}

我正在尝试使用 Mockito 和 Robolectric 来做到这一点。

这是我的 robolectric 测试用例的开始:

public class MyFragmentTest {

  private MyFragment fragment;

  @Before
  public void setup() {
    ListItem item1 = mock(ListItem.class);
    ListItem item2 = mock(ListItem.class);
    when(item1.getValue()).thenReturn("known value 1");
    when(item2.getValue()).thenReturn("known value 2");
    List<ListItem> mockList = new ArrayList<ListItem>();
    mockList.add(item1);
    mockList.add(item2);
    MyFragment real = new MyFragment();
    this.fragment = spy(real);
    when(this.fragment.getValueList()).thenReturn(mockList);
    startFragment();
  }

}

这感觉太不对劲了。来自 mockito api 的This section 指出,除非您处理遗留代码,否则您不应该经常进行这样的部分模拟。

此外,我实际上无法使用这种方法模拟 CustomAdapter 类。

做这种事情的正确方法是什么?我在Fragment 课程中的结构是否不正确?我想我也许可以添加一堆包私有设置器,但这仍然感觉不对。

有人可以解释一下吗?我很高兴进行重写,我只想知道一些处理 Fragments 中状态的好模式以及如何使它们可测试。

【问题讨论】:

    标签: android unit-testing mockito robolectric


    【解决方案1】:

    我最终为此创建了自己的解决方案。我的方法是为创建或设置对象的每个调用添加另一个间接级别。

    首先,让我指出,我实际上无法让 Mockito 可靠地处理 FragmentActivity 对象。它有些偶然,但特别是在尝试创建 Mockito Spy 对象时,一些生命周期方法似乎没有被调用。我认为这与gotcha number 2 shown here 有关。也许这是由于 Android 使用反射来重新创建和实例化活动和片段的方式?请注意,我并没有像它警告的那样错误地持有引用,而是只与Spy 交互,如所示。

    因此,我无法模拟需要框架调用生命周期方法的 Android 对象。

    我的解决方案是在我的 Activity 和 Fragment 方法中创建更多类型的方法。这些方法是:

    • getter (getX()) 返回名为 X 的字段。
    • 检索器 (retrieveX()) 执行某种工作以获取对象。
    • 创建者 (createMyFragment()) 通过调用 new 创建对象。类似于检索器。

    Getter 具有您需要的任何可见性。我的通常是publicprivate

    Retrievers 和 creators 是包私有的或protected,允许您在测试包中覆盖它们,但不能使其普遍可用。这些方法背后的想法是,您可以使用存根对象对常规对象进行子类化,并在测试期间注入已知值。如果 Mockito 模拟/间谍为您工作,您也可以模拟这些方法。

    总之,测试将如下所示。

    这是我原始问题的片段,已修改为使用上述方法。这是在普通项目中:

    package org.myexample.fragments
    
    // imports
    
    public class MyFragment extends ListFragment {
    
      private List<ListItem> mList;
    
      void setListValues(List<ListItem> values) {
        this.mList = values;
      }
    
      List<ListItem> getListValues() {
        return this.mList;
      }
    
      @Override
      public void onCreateView(LayoutInflater i, ViewGroup vg, Bundle b) {
        // blah blah blah
      }
    
      @Override
      public void onViewCreated(View view, Bundle savedInstanceState) {
        this.setListValues(this.retrieveListItems());
        CustomAdapter adapter = this.createCustomAdapter();
        this.setListAdapter(adapter);
      }
    
      List<ListItem> retrieveListItems() {
        List<Item> result = ListFactory.getListOfDynamicValues();
        return result;
      }
    
      CustomAdapter createCustomAdapter() {
        CustomAdapter result = new CustomAdapter(
            this.getActivity();
            R.layout.row_layout,
            this.getListValues());
        return result;
      }
    
    }
    

    当我测试这个对象时,我希望能够控制传递的内容。我的第一个想法是使用Spy,将retrieveListItems()createCustomAdapter() 的返回值替换为我已知的值。然而,就像我上面所说的,我无法让 Mockito 间谍在处理片段时表现出来。 (尤其是ListFragments--我在其他类型中取得了不同程度的成功,但不要相信它。)所以,我们将继承这个对象。在测试项目中,我有以下内容。请注意,您在真实类中的方法可见性必须允许子类覆盖,因此它需要是包私有的并且在同一个包或protected 中。请注意,我将覆盖检索器和创建器,而是返回我的测试将设置的静态变量。

    package org.myexample.fragments
    
    // imports
    
    public class MyFragmentStub extends MyFragment {
    
      public static List<ListItem> LIST = null;
      public static CustomAdapter ADAPTER = null;
    
    
      /**
       * Resets the state for the stub object. This should be called
       * in the teardown methods of your test classes using this object.
       */
      public static void resetState() {
        LIST = null;
        ADAPTER = null;
      }
    
      @Override
      List<ListItem> retrieveListItems() {
        return LIST_ITEMS;
      }
    
      @Override
      CustomAdapter createCustomAdapter() {
        return CUSTOM_ADAPTER;
      }
    
    }
    

    在我的测试项目的同一个包中,我对片段进行了实际测试。请注意,当我使用 Robolectric 时,这应该适用于您使用的任何测试框架。 @Before 注释变得不那么有用了,因为您需要为各个测试更新静态状态。

    package org.myexample.fragments
    
    // imports
    
    @RunWith(RobolectricTestRunner.class)
    public class MyFragmentTest  {
    
      public MyFragment fragment;
      public Activity activity;
    
      @After
      public void after() {
        // Very important to reset the state of the object under test,
        // as otherwise your tests will affect each other.
        MyFragmentStub.resetState();
      }
    
      private void setupState(List<ListItem> testList, CustomAdapter adapter) {
        // Set the state you want the fragment to use.
        MyFragmentStub.LIST = testList;
        MyFragmentStub.ADAPTER = adapter;
        MyFragmentStub stub = new MyFragmentStub();
        // Start and attach the fragment using Robolectric.
        // This method doesn't call visible() on the activity, though so
        // you'll have to do that yourself.
        FragmentTestUtil.startFragment(stub);
        Robolectric.ActivityController.of(stub.getActivity()).visible();
        this.fragment = stub;
        this.activity = stub.getActivity();
    
      }
    
      @Test
      public void dummyTestWithKnownValues() {
        // This is a test that does nothing other than show you how to use
        // the stub.
        // Create whatever known values you want to test with.
        List<ListItem> list = new ArrayList<ListItem>();
        CustomAdapter adapter = mock(CustomAdapter.class);
        this.setupState(list, adapter);
        // android fest assertions
        assertThat(this.fragment).isNotNull();
      }
    
    }
    

    这肯定比使用模拟框架更冗长。但是,它甚至适用于 Android 的生命周期。如果我正在测试Activity,我也会经常包含static boolean BUILD_FRAGMENTS 变量。如果为真,我将在适当的方法中调用 super 或返回适当的已知片段。通过这种方式,我可以注入我的测试对象并很好地适应 Android 生命周期。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-14
      • 1970-01-01
      • 2020-12-04
      • 1970-01-01
      • 1970-01-01
      • 2019-11-29
      相关资源
      最近更新 更多