【发布时间】:2015-02-06 00:06:41
【问题描述】:
讨论如何对完全不切实际的事物进行单元测试的帖子似乎没有尽头。
大量的教程、视频等概述了单元测试是什么以及如何进行。然而,似乎没有多少资源(如果有的话)概述了如何测试真实的东西。
毕竟.. 实际上,我们正在测试的“单元”通常比接受输入并给出输出的方法复杂得多。
我目前正在使用 Android,并且正在研究如何对我的应用程序进行单元测试。 我的应用程序本质上是由视图和服务器请求组成的。您单击按钮 x,它会更改显示的视图。您单击按钮 y,它会从服务器加载数据并填充一个列表。
下面是一些源代码。我基本上拼凑了一个示例设置,它演示了(对我而言)令人困惑的事情。我发现在概念上难以进行单元测试的东西。
public class ChainActivity extends FragmentActivity {
private PRFragmentTabHost mTabHost;
public GetChainResponse.Data responseData;
Integer chainId; //ref to chain we are getting - passed in
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Get the chain id we are getting
Bundle extras = getIntent().getExtras();
chainId = extras.getInt("chainId");
setContentView(R.layout.activity_base);
//Set up the tabs
mTabHost = (PRFragmentTabHost) findViewById(android.R.id.tabhost);
mTabHost.setup(this, getSupportFragmentManager(), android.R.id.tabcontent);
mTabHost.addTab(mTabHost.newTabSpec("Details").setIndicator("Details", null), ChainDetailsFragment.class, null);
mTabHost.addTab(mTabHost.newTabSpec("Pictures").setIndicator("Pictures", null), PicturesFragment.class, null);
mTabHost.addTab(mTabHost.newTabSpec("Cats").setIndicator("Cats", null), CatsFragment.class, null);
}
@Override
public void onStart() {
super.onStart();
//Initiate the data load
loadChainData();
}
//Method loads the chain data
public void loadChainData(){
PRAPIInterface apiService = ApiService.getInstance();
Integer limit = 4;
apiService.getChain(chainId, limit, new Callback<GetChainResponse>() {
@Override
public void success(GetChainResponse pr, Response response) {
lastData = System.nanoTime();
//Save the response data
responseData = pr.data;
//Get the current tab and pass the loaded data to it
String currentTabTag = mTabHost.getCurrentTabTag();
DataLoadedInterface currentTab = (DataLoadedInterface) getSupportFragmentManager().findFragmentByTag(currentTabTag);
currentTab.dataLoaded(responseData, false);
}
@Override
public void failure(RetrofitError retrofitError) {
// Log error here since request failed
Log.w("Failed", "Failed" + retrofitError.getUrl());
Log.w("Failed", "Failed" + retrofitError.getBody());
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Fragment fragment = getSupportFragmentManager().findFragmentByTag(mTabHost.getCurrentTabTag());
if (fragment != null) {
fragment.onActivityResult(requestCode, resultCode, data);
}
}
}
所以.. 我知道 Roboeletric、Robotium 等以及其他可用于在 Android 上进行测试的库。不过,我正在寻找概念性建议。
Android 提供ActivityUnitTestCase
我可以对它进行子类化并为我的活动设置测试。
第 1 部分
原则上,我可以通过验证 mTabHost 不为空来测试我的 onCreate,但我不想让它公开,也不想让它的值有一个 getter。
我认为我可以测试我的片段是否存在,但实际上我不能。因为活动在“隔离”中运行,它似乎实际上并没有为选项卡创建片段。
第 2 部分
接下来是onStart。这调用了另一种方法。它没有返回值。我无法测试响应。
然而,重要的是我测试 onStart 我们加载我们的初始数据..
在loadChainData 内,我可以设置一个布尔值,指示我正在加载数据并验证这一点,但同事可以默认将此布尔值设置为 true,我的测试将通过。
此外,我不想“再次”测试 loadChainData。无论如何我都会测试这个方法。想到的一个想法是存根 loadChainData 并验证它是否被调用并将其保留。然而,这似乎很难在 Android 上实现(任何人..?),并且与测试应该很有趣的观点不符。
第三部分
loadChainData 使用改造从服务器加载一些数据。因为实际上这个方法是异步执行的,所以这个方法再次没有响应。我已经找到了一种通过更换改造客户端来返回模拟数据的适当方法,但进行这种交换显然并不简单。
目前我为我的 ApiService 使用单例。我想从本质上替换第一次调用这个单例时构建的内容。有一些潜在的复杂解决方案,比如使用依赖注入库(如 Dagger),但考虑到我想要实现的目标,我觉得应该有一种更简单的方法来做到这一点。
我最初的想法是,如果可以使用测试标志来实例化应用程序。单例将返回测试客户端。或者,它会默认为真正的客户。这在我的脑海里有点味道.. 谁能解释一下这是什么味道,以及如何适当地解决它?
即使以上被认为是一个公平的建议,似乎也没有简单的方法来实际使用 ActivityUnitTestCase。
第四部分
最后是onActivityResult。
再次,没有回应。
这一次,所讨论的方法与其他地方的其他单位相互作用。无论如何,在 ActivityUnitTestCase 的约束下行为不同的单元。
我可以包装我对支持片段管理器的操作,模拟我的包装器,返回一个模拟片段,验证它的 onActivityResult 方法是否被调用......但这似乎很难做到。此外,这增加了我的代码的复杂性,以允许 something 是可测试的。我对增加复杂性只是为了测试没有兴趣..
所以..
在移动设备上具有实际单元测试经验的人是否对如何正确测试诸如此类的类有任何见解。正如你所看到的,这并不是“把 2 放进去,4 出来”的情况:)
很多资源都提到了测试是如何进行的。这就是为什么 :) 任何建议都将不胜感激。
T
【问题讨论】:
-
我总体上同意你的看法。我尝试将 Roboguice 用于 DI 并使用 Mockito 来替换为 mock,它会更好一些,但是我讨厌这个。我在移动甚至 OOP 方面没有太多经验,但我认为做这样的测试应该容易得多。我希望有真正经验的人能给出我也可以使用的答案:)
标签: android unit-testing testing tdd integration-testing