我正在尝试测试我在Android中创建的Fragment。我已经完全控制了代码,因此可以根据需要进行更改。问题是我不确定要使之合理的设计模式是什么。

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

在某些情况下,这是有道理的,但我无法弄清楚如何使其在Android上无法正常工作。例如,使用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进行此操作。

这是我的机器人测试用例的开始:

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

}


这感觉非常错误。来自嘲笑API的This section指出,除非您要处理遗留代码,否则不必经常进行类似的部分嘲笑。

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

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

有人可以阐明这一点吗?我很高兴进行重写,我只想知道一些处理Fragment状态的良好模式,以及如何使它们可测试。

最佳答案

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

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

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

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


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


吸气剂具有您所需的可见性。我的通常是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 -我在其他类型上取得了成功,但不信任它。)因此,我们将对该对象进行子类化。在测试项目中,我有以下内容。请注意,您在真实类中的方法可见性必须允许重写子类,因此它必须是程序包私有的,并且在同一程序包或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变量。如果为true,我将使用适当的方法来调用super或适当地返回一个已知的片段。这样,我就可以注入测试对象并在Android生命周期中很好地玩。

07-28 02:21
查看更多