[译] 通过测试来解耦 Activity

本文讲的是[译] 通过测试来解耦 Activity,

通过测试来解耦Activity

Activity 和 Fragment,可能是因为一些奇怪的历史巧合,从 Android 推出之时起就被视为构建 Android 应用的最佳构件。我们把Activity 和 Fragment 是应用的最佳构件这种想法称为“android-centric”架构。

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

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

目标代码

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

拒绝请求的 toast

这是实现代码:

@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);
}

测试代码

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

@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 均为测试框架)

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

我们在 MapActivity 里面的代码逻辑和 Toast 紧密地耦合在一起。

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

为了让我们重构有个方向,我们先写测试。这将确保我们的新类已经解耦。为了避免使用 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();
    }
}

我们把对 MapFragment 和 Toast 的调用替换为对 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 拒绝请求

这是一个小改变,但是因为我们已经把 OnPermissionResultListener 从 PermittedView 中分离出来,我们可以只专注于改变 PermittedView 在 MapActivity 里面的实现,而无需担心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.

如果这是对的,当我们解耦,我们将会发现更多的去重机会。的确,在我们这次案例中这个观点显得很准确。事实上有另外一个类的 onRequestPermissionsResult 和 MapActivity 的几乎一样:AccountFragment。我们的测试指引我们来创建 OnPermissionResultListener 和PermittedView 这两个接口,因此无需任何修改就可以在其他类中复用。

结论

所以,当我们难以测试 Activity 和 Fragment时,通常是因为我们的测试尝试告诉我们所写的代码耦合度太高。测试对耦合度的警告通常以我们无法对代码做出断言的形式表现出来。

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

注意

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






原文发布时间为:2017年5月11日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-09-10 00:13:25

[译] 通过测试来解耦 Activity的相关文章

[译]多变量测试:5个简单步骤提升转化率

前言 自Google出现并改变了游戏规则之后,用户对于网页的关注时间一直在下降.对于任何一个时下话题,有千万条结果可以关注,可以抓住访问者注意力的机会非常明显地下降了(2002年,BBC报告指出大约在9秒内).想象一下你自己浏览网页时的时:你会阅读所有的文字和图片,尝试着彻底了解整个网页内容是什么吗?最有可能的答案是:"不会."伴随着充斥四周的信息轰炸,我们像被宠坏了的孩子那样,不会投入足够的的注意力去关注一个网页到底想告诉我们什么. 我们快速决定是否关注一个网站时,取决于我们在几毫秒

Android测试教程(14):ActivityInstrumentationTestCase2示例

ActivityInstrumentationTestCase2 用来测试单个的Activity,被测试的Activity可以使用InstrumentationTestCase.launchActivity 来启动,然后你能够直接操作被测试的Activity. ActivityInstrumentationTestCase2 也支持: 可以在UI线程中运行测试方法. 可以注入Intent对象到被测试的Activity中 ActivityInstrumentationTestCase2 取代之前的

编写Android测试单元该做的和不该做的事

在本文中, 我将根据我的实际经验,为大家阐述一个编写测试用例的最佳实践.在本文中我将使用 Espresso 编码, 但是它们可以用到单元测试和仪器测试(instrumentation test)当中.基于以上目的,我们来研究一个新闻程序. 以下内容纯属虚构,如有雷同纯属巧合 :P 一个新闻 APP 应该会有以下这些 activity. 语言选择 - 当用户第一次打开软件, 他必须至少选择一种语言.选择后,选项保存在共享偏好中,用户跳转到新闻列表 activity. 新闻列表 - 当用户来到新闻列

编写 android 测试单元该做的和不该做的事

一个新闻 APP 应该会有以下这些 activity.   语言选择 - 当用户第一次打开软件, 他必须至少选择一种语言.选择后,选项保存在共享偏好中,用户跳转到新闻列表 activity.   新闻列表 - 当用户来到新闻列表 activity,将发送一个包含语言参数的请求到服务器,并将服务器返回的内容显示在 recycler view 上(包含有新闻列表的 id, news_list). 如果共享偏好中未存语言参数,或者服务器没有返回一个成功消息, 就会弹出一个错误对话框并且 recycle

Android 不能返回 parent Activity 的问题

使用 ActionBar,开启返回按钮: 在 Activity 的 onCreate 中添加下面代码 getSupportActionBar().setDisplayHomeAsUpEnabled(true); 这里左侧会多出一个返回的箭头,点击图标后会触发 click 事件: @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.

android开发 Activity设置模拟对话框效果

来先看下效果,有个感性的认识. 开发 Activity设置模拟对话框效果-activity 对话框效果"> 中间那个提示其实是一个activity,好的,下面开始一步步实现这个神奇的效果. 第一步:设计对话框页面activity_simulate_dialog.xml <?xml version="1.0" encoding="utf-8"?>   <RelativeLayout xmlns:android="http:/

马云爸爸“翻译情未了”? 回顾阿里翻译平台的进击之路!

雷锋网AI科技评论按:在百度翻译,谷歌翻译几乎要霸占整个机器翻译市场时,阿里翻译宣布已成功研发阿里云PAI工具,基于阿里云PAI可以将神经网络翻译训练效率提升5倍,这将大大加速阿里翻译平台的建设.希望阿里翻译以后也能走进我们的生活中. 众所周知,马云爸爸在创立阿里之前是做翻译服务及开翻译公司的.随着近几年阿里的业务不断扩大,全球化战略进程加速,语言问题也成了最基础的需求之一,尤其是跨境电商交易对多语言翻译需求尤甚.此前阿里在语言服务上做过不少努力,包括收购国内最大的人工翻译平台,但这远远不能满足

MVP框架 – Ted Mosby的软件架构

作者:Hannes Dorfmann 原文链接 : Ted Mosby – Software Architect 文章出自 : Android开发技术前线 译者 : Mr.Simple 我给这篇关于Android库的博客起的名字灵感来源于<老爸老妈浪漫史>中的建筑设计师Ted Mosby.这个Mosby库可以帮助大家在Android上通过Model-View-Presenter模式做出一个完善稳健.可重复使用的软件,还 可以借助ViewState轻松实现屏幕翻转. Model-View-Pre

MVP模式在携程酒店的应用和扩展

前言 酒店业务部门是携程旅行的几大业务之一,其业务逻辑复杂,业务需求变动快,经过多年的研发,已经是一个代码规模庞大的工程,如何规范代码,将代码按照其功能进行分类,将代码写到合适的地方对项目的迭代起着重要的作用. MVP模式是目前客户端比较流行的框架模式,携程在很早之前就开始探索使用该模式进行相关的业务功能开发,以提升代码的规范性和可维护性,积累了一定的经验.本文将探讨一下该模式在实际工程中的优点和缺陷,并介绍携程面对这些问题时的思考,解决方案以及在实践经验基础上对该模式的扩展模式MVCPI. 一