[译] 经过测试来解耦 Activity

经过测试来解耦Activity

ActivityFragment,多是由于一些奇怪的历史巧合,从 Android 推出之时起就被视为构建 Android 应用的最佳构件。咱们把ActivityFragment 是应用的最佳构件这种想法称为“android-centric”架构。javascript

本系列博文是关于 android-centric 架构的可测试性和其它问题之间的联系的,而这些问题正致使 Android 开发者们排斥这种架构。这些博文也涉及单元测试怎样试图告诉咱们:ActivityFragment 不是应用的最佳构件,由于它们迫使咱们写出高耦合低内聚的代码。前端

上次,咱们发现ActivityFragment有低内聚的倾向。此次,经过测试咱们将会发现 Activity 是高耦合的。咱们还会发现如何经过测试来驱使实现一个耦合度更低的设计,这样咱们就能轻易地改变应用和有更多的机会来减去重复代码。像本系列博文中的其余文章同样,咱们依然以 Google I/O 应用为例子进行探讨。java

目标代码

咱们想要测试的“目标代码”,作了如下工做:当用户进入展现全部 Google I/O session 的地图界面时,app 会请求当前位置。若是用户拒绝提供定位权限,咱们会弹出一个 toast 来提示用户已禁用此权限。这是其中的截图:react

拒绝请求的 toast

这是实现代码:android

@Override
public void onRequestPermissionsResult(final int requestCode,
        @NonNull final String[] permissions,
        @NonNull final int[] grantResults) {

    if (requestCode != REQUEST_LOCATION_PERMISSION) {
        return;
    }

    if (permissions.length == 1 &&
            LOCATION_PERMISSION.equals(permissions[0]) &&
            grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        // Permission has been granted.
        if (mMapFragment != null) {
            mMapFragment.setMyLocationEnabled(true);
        }
    } else {
        // Permission was denied. Display error message.
        Toast.makeText(this, R.string.map_permission_denied,
                Toast.LENGTH_SHORT).show();
    }
    super.onRequestPermissionsResult(requestCode, permissions,
            grantResults);
}复制代码

测试代码

让咱们尝试测试下这些代码,咱们的测试代码看起来是这样的:ios

@Test
public void showsToastIfPermissionIsRejected()
        throws Exception {
    MapActivity mapActivity = new MapActivity();

    mapActivity.onRequestPermissionsResult(
            MapActivity.REQUEST_LOCATION_PERMISSION,
            new String[]{MapActivity.LOCATION_PERMISSION}, new int[]{
                    PackageManager.PERMISSION_DENIED});

    assertToastDisplayed();
}复制代码

固然你很但愿能知道 assertToastDisplayed() 是怎么实现的。重点来了:咱们不会直接实现该方法。为了不实现后再重构咱们的代码,咱们须要使用 Roboelectric 和 Powermock。(译者注:Roboelectric 和 Powermock 均为测试框架)git

不过,既然咱们更但愿根据测试来改变咱们写代码的方式,而不是仅仅改变写测试的方式,咱们要停一会来想想这些测试想要告诉咱们什么事情:github

咱们在 MapActivity 里面的代码逻辑和 Toast 紧密地耦合在一块儿。后端

这之间的耦合驱使咱们使用 Roboelectric 来模拟 android 行为和 powermock 来模拟静态的 Toast.makeText 方法。做为替换,让咱们以测试为驱动来去除耦合。session

为了让咱们重构有个方向,咱们先写测试。这将确保咱们的类已经解耦。为了不使用 Roboelectric 框架,咱们须要在这特殊状况下建立一个新类,可是一般来讲,咱们只需重构已存在的类来解耦。

@Test
public void displaysErrorWhenPermissionRejected() throws Exception {

    OnPermissionResultListener onPermissionResultListener =
            new OnPermissionResultListener(mPermittedView);

    onPermissionResultListener.onPermissionResult(
            MapActivity.REQUEST_LOCATION_PERMISSION,
            new String[]{MapActivity.LOCATION_PERMISSION},
            new int[]{PackageManager.PERMISSION_DENIED});

    verify(mPermittedView).displayPermissionDenied();
}复制代码

咱们已经介绍过 OnPermissionResultListener,它的工做就是处理用户对 app 请求权限的反应。代码以下:

void onPermissionResult(final int requestCode,
            final String[] permissions, final int[] grantResults) {
    if (requestCode != MapActivity.REQUEST_LOCATION_PERMISSION) {
        return;
    }

    if (permissions.length == 1 &&
            MapActivity.LOCATION_PERMISSION.equals(permissions[0]) &&
            grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        // Permission has been granted.
        mPermittedView.displayPermittedView();

    } else {
        // Permission was denied. Display error message.
        mPermittedView.displayPermissionDenied();
    }
}复制代码

咱们把对 MapFragmentToast 的调用替换为对 PermittedView 里面方法的调用,这个对象经过构造函数来传递。PermittedView 是一个接口:

interface PermittedView {
    void displayPermissionDenied();

    void displayPermittedView();
}复制代码

它在 MapActivity 里实现:

public class MapActivity extends BaseActivity implements SlideableInfoFragment.Callback, MapFragment.Callbacks, ActivityCompat.OnRequestPermissionsResultCallback, OnPermissionResultListener.PermittedView {
    @Override
    public void displayPermissionDenied() {
        Toast.makeText(MapActivity.this, R.string.map_permission_denied,
                Toast.LENGTH_SHORT).show();
    }
}复制代码

这也许不是最好的解决方案,可是这能让咱们抓住能够在哪里测试这一重心。这要求 OnPermissionResultListener 下降和 PermittedView 的耦合度。解耦 == 显而易见的进步。

有必要么?

对于这一点,一些读者可能会有所怀疑。“这样真的算优化代码吗?”他们会大惑不解。有两点理由能够确认为何这样设计更好

(不管我给出哪个理由,你都会发现其解释是“由于它的可测试性更好,因此它设计得更好”,这是一个很重要的缘由。)

更容易改变

首先,由于所组成的内容耦合度低,从而可以更容易地改变代码,并且更精彩的是:咱们刚刚测试 Google I/O 应用的代码实际上已经改变了,经过咱们的测试,能让其改代码变得更容易。所测试的代码来自一个较旧的 commit。以后,写 I/O 应用的人们决定把 Toast 替换为 Snackbar

snackbar 拒绝请求

这是一个小改变,可是由于咱们已经把 OnPermissionResultListenerPermittedView 中分离出来,咱们能够只专一于改变 PermittedViewMapActivity 里面的实现,而无需担忧 OnPermissionResultListener

这是咱们改变代码后的样子,使用他们的 PermissionUtils 类来显示 SnackBar

@Override
public void displayPermissionDenied() {
    PermissionsUtils.displayConditionalPermissionDenialSnackbar(this,
            R.string.map_permission_denied, new String[]{LOCATION_PERMISSION},
            REQUEST_LOCATION_PERMISSION);
}复制代码

请再留意,咱们能够不用考虑 OnPermissionResultListener 就直接改变其内容。这实际就是 Larry Constantine 在 70 年代提出对耦合这一律念的定义:

咱们尽力让系统解耦。。。这样咱们就能研究(或者调试、维护)其中一个模块而无需考虑系统中的其余模块

–Edward Yourdon and Larry Constantine, Structured Design

去重

另外一个“为何实际上经过咱们的测试来迫使咱们解耦是一件好事”的有趣缘由是:耦合一般会致使重复。Kent Beck 曾对此有相关见解:

依赖是任意规模的软件开发的重点问题。。。若是依赖成为了问题,这就会体如今重复上。

-Kent Beck, TDD By Example, pg 7.

若是这是对的,当咱们解耦,咱们将会发现更多的去重机会。的确,在咱们此次案例中这个观点显得很准确。事实上有另一个类的 onRequestPermissionsResultMapActivity 的几乎同样:AccountFragment。咱们的测试指引咱们来建立 OnPermissionResultListenerPermittedView 这两个接口,所以无需任何修改就能够在其余类中复用。

结论

因此,当咱们难以测试 ActivityFragment时,一般是由于咱们的测试尝试告诉咱们所写的代码耦合度过高。测试对耦合度的警告一般以咱们没法对代码作出断言的形式表现出来。

当咱们遵从咱们的测试时,与其经过 Roboelectric 和 powermock 替换测试代码,不如改变被测代码,让其耦合度下降,这样咱们就能更容易改代码和有更多的机会去重。

注意

  1. 这也可能表现为没法让你的被测代码在测试中以一个正确的状态表现出来。例如咱们在本篇中所看到的。

咱们在 Unikey 招聘中级 Android 开发者。若是你想要在 Orlando 智能锁定空间里的一间初创公司工做,请发邮件给我。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划

相关文章
相关标签/搜索