【问题标题】:Unit testing realistic Android classes. Test environment, the lifecycle, and responses单元测试现实的 Android 类。测试环境、生命周期和响应
【发布时间】: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 部分

原则上,我可以通过验证 mTab​​Host 不为空来测试我的 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


【解决方案1】:

onCreate

我将通过两种方式进行测试,一种使用某种形式的AndroidTestCase 这将允许测试您在捆绑包中发送的附加内容。
(如果额外不存在,我会添加错误处理以引发错误)
(我会更改 loadChainData() 以将 chainId 作为参数)

为了测试您的选项卡实例化是否正确,我将使用 espresso 进行验收测试以验证您的视图是否存在。

onStart

这里没有Dependency Inversion,所以你不能测试调用的服务方法。你会想要测试 getChain 被调用。创建该服务时会发生什么,您可以在构造函数中进行吗?
(我会离开这里的 Activity 并将来自 onStart 的所有行为封装在其他一些类中[也许 MVC 可以帮助],这样你就不会受到生命周期的限制。
(我还将在 Volley 之上添加另一层回调到 Activity 中,这样您可以测试您的侦听器返回正确的成功或失败,但您不必担心选项卡 [活动 ui])。

如果您真的想测试异步性(我不建议这样做),您可以使用 Espresso,它可以帮助您管理测试以仍然获得回调 see custom resource idoling

onActivityResult

我相信使用 Robolectric 您可以将 Mockito 模拟片段管理器传递给它,以便向您返回一个存根片段并验证片段选项卡主机的行为,然后您可以自己调用 onActivityResult:测试这是否真的给了您任何好处都是另一个问题。

整体测试生命周期方法就像测试其他任何东西一样,为了使其可测试,您需要抽象出任何线程概念,您需要控制您的依赖关系,我发现您越接近 SOLID 越容易测试是。在这些绊脚石之后,这就是为什么创建 RobolectricInstrumentationTestsEspresso 的原因。

抱歉,这里没有直截了当的答案,你必须问自己 - 我从测试这些东西中得到什么好处,我是在测试我的代码还是在测试框架,测试的成本效益是什么这个活动实际上阻碍了未来的变化,而不是让我获得信心。

【讨论】:

    猜你喜欢
    • 2015-04-29
    • 1970-01-01
    • 1970-01-01
    • 2011-08-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-03
    • 1970-01-01
    相关资源
    最近更新 更多