我正在尝试测试在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
中状态的好模式以及我如何使它们可测试。
答案 0 :(得分:1)
我最终创建了自己的解决方案。我的方法是为每个创建或设置对象的调用添加另一级别的间接。
首先,我要指出,我实际上无法让Mockito与Fragment
或Activity
对象可靠地协同工作。它有点受欢迎,但特别是在尝试创建Mockito Spy
对象时,似乎没有调用某些生命周期方法。我认为这与gotcha number 2 shown here有关。也许这是由于Android使用反射来重新创建和实例化活动和片段的方式?请注意,我没有错误地抓住引用,因为它警告,但只与Spy
进行交互,如图所示。
所以,我无法模拟需要框架调用生命周期方法的Android对象。
我的解决方案是在Activity和Fragment方法中创建更多类型的方法。这些方法是:
getX()
)返回名为X
的字段。retrieveX()
)做某种工作来获取对象。createMyFragment()
创建对象的创建者(new
)。与猎犬类似。 Getters拥有您需要的任何可见性。我的通常是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
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生命周期中发挥出色。