Android中的模拟对象未作为参数传递

时间:2014-04-25 22:09:34

标签: android unit-testing mockito robolectric

我正在尝试测试在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课程中错误地构建了一些东西吗?我想我可以添加一堆包私有的setter,但这仍然感觉不对。

有人可以对此有所了解吗?我很乐意做重写,我只想知道一些处理我Fragment中状态的好模式以及我如何使它们可测试。

1 个答案:

答案 0 :(得分:1)

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

首先,我要指出,我实际上无法让Mockito与FragmentActivity对象可靠地协同工作。它有点受欢迎,但特别是在尝试创建Mockito Spy对象时,似乎没有调用某些生命周期方法。我认为这与gotcha number 2 shown here有关。也许这是由于Android使用反射来重新创建和实例化活动和片段的方式?请注意,我没有错误地抓住引用,因为它警告,但只与Spy进行交互,如图所示。

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

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

  • getters(getX())返回名为X的字段。
  • 检索器(retrieveX())做某种工作来获取对象。
  • 通过调用createMyFragment()创建对象的创建者(new)。与猎犬类似。

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

检索器和创建者是包私有或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间谍表现出来。 (特别是ListFragment s - 我在其他类型中取得了不同的成功,但不相信它。)因此,我们将继承这个对象的子类。在测试项目中,我有以下内容。请注意,您的实际类中的方法可见性必须允许子类覆盖,因此它必须是包私有的并且在同一个包或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生命周期中发挥出色。