我正在尝试测试我在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与Fragment
或Activity
对象可靠地工作。它有些偶然,但是尤其是在尝试创建Mockito Spy
对象时,似乎没有调用某些生命周期方法。我认为这与gotcha number 2 shown here有关。也许这是由于Android使用反射来重新创建和实例化活动和片段的方式?请注意,正如我所警告的那样,我并不是错误地抓住引用,而是仅与Spy
进行交互,如所示。
因此,我无法模拟需要框架调用生命周期方法的Android对象。
我的解决方案是在Activity和Fragment方法中创建更多类型的方法。这些方法是:
返回名为getX()
的字段的getter(X
)。
检索器(retrieveX()
)执行某种工作来获取对象。
通过调用createMyFragment()
创建对象的创建者(new
)。类似于猎犬。
吸气剂具有您所需的可见性。我的通常是public
或private
。
检索器和创建者是程序包私有的或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生命周期中很好地玩。