Android Architecture Components 与可测试代码

软件开发历史大潮中,我们做了许许多多的努力让代码变得更加优雅。但是「优雅」这一件事,并不是简单的让代码变得漂亮而已,我们的「优雅」是有目的的:可维护可扩展可测试。从实用性角度来讲,我认为「可测试」这是我们在产品开发中追求的第一要素,毕竟质量是我们坚持的第一要素,没有质量背书,许多的工作都是空想。今天我们就来讲讲,如何通过Google新发布的Android Architecture Components去写我们的代码,使得我们的项目变得更加「Testable」。

让「可测试」变得有意义

写测试,在圈子里似乎并不是那么的流行,我曾经在几次聚会中问身边的 Android 开发:「你们会自己去写白盒测试么?」,回答都是否定的,如果再问为什么的话,就是「太麻烦了」、「不知道怎么写,不知道写什么」。当我们说 MVC/MVP 的时候,我们究竟是为了什么而实现这些模式的,我们为什么要为一些模块写大量的接口,仅仅是看上去酷炫?最后为了赶工期进度,胡乱写一通的时候,好像也没发生什么问题吧。那么这时候,其实写出来的所谓的「架构」其实并没有什么用处 ———— 当你去重构看起来烦的不行的业务代码的时候,依然需要花大量进行黑盒测试。开发说,架构不可以随便动,因为线上已经跑了很长时间了,至少证明了它暂时是可行的。

我们期望改变这样的状况,我们希望随时随地能真正把我们的产出掌握在自己手里 ———— 哪怕哪一天真的忘记了具体实现,哪怕新同事入职接手了这一份代码,我们也能胸有成竹的说,你随便跑,只用跑一下我写的测试,通过了就好。

模块可替代性

首先我们能达成的共识是:我们的业务代码,很多都是和 UI 耦合在一起,很多都是和「生产环境」绑定在一起,所以我们的代码才变得非常的不可测试。我们使用分层架构模型,也只是为了一个目的:使得下层依赖的模块可替换。那么,基于我们所拥有的条件和所碰到的困难,
我们是否可以把我们的分层架构模型用于替换生产环境呢,这样就可以轻松测试我们的业务逻辑了?答案是肯定的。只要我们把我们的生产环境轻松的替换成我们 Mock 的数据环境,我们的「服务器」就变得可控了,我们可以返回我们期待的结果,看我们的后续业务逻辑流程是否处理正确。

同理,如果我们依赖的 View 层,只用统一的数据流和这一层通信的话,我们的测试只用控制这一个数据流,然后用 Espresoo 等 UI Test 框架去检查 UI 的反应是否正确即可。

Android Architecture Components 提供了一套自顶向下的分层解决方案,很适合我们的场景,使得我们写测试变得更加的容易。

LiveData

其实我更愿意称呼LiveData为「ViewModel」,如果你对react.js有了解,它更像是我们 Component 中的props概念。我们可以为 Activity 或者 Fragment(以下统称 View)提供多个 LiveData。在support-v4升级到27.0.0之后,AppCompatActivity已经自动集成了Lifecycle相关的接口和实现,LiveData本身使用「观察者模式」为View提供数据,如果View的生命周期销毁,那么它会自动解除注册,不需要我们在onStop或者onDestroy中去调用它的生命周期。我们可以使用LiveData彻底对 View 的配置进行解耦。View 层的代码大致就可以如下

class ExampleActivity extends AppCompatActivity {
    @Inject
    Presenter presenter;

    TextView textView;
    Button btn;

    public void onCreate(Bundle savedBundle) {
        Inject.inject(this);
        super.onCreate(savedBundle);

        // 一些初始化
        ...
        // 初始化完毕

        LiveData<String> textLive = presenter.getTextLive().observe(this, new Observer {

            public void onChanged(String value) {
                textView.setText(value);
            }
        })

        btn.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                presenter.fetchText();
            }
        });
    }

}

那么 View 这层实际就干了两件事:

  1. 响应事件
  2. 响应 LiveData 的通知

它的全部职责就是获取到真正的Presenter(业务实现),并处理好视图相关的所有事情。这样的模式比起以前的「万能 Activity」不知道清晰多少倍。那么如何对这个 View 进行测试呢?很显然,我们只要提供一个受我们控制的Presenter类即可,后续控制 Presenter 中的 LiveData,通过推送不同的数据,来观察我们的 View 各个字段显示是否正确,测试用的TestPresenter可以由Dagger等框架进行注入。

Presenter.png

ViewModel

这里的 ViewModel 指的是 ViewModel 这个类的本身。它并不是我们传统理解的那个「ViewModel」,Google 在这是把他定义为存储 View 相关数据的,有生命周期意识的类 —— 它保证了在屏幕翻转等情况下,数据的存续。众所周知,Android 中四大组件的生命周期都不是由我们自己控制的,因此对一些对象的生命周期控制,有时候会显得非常蠢,试想一个场景:

你的一个 Activity 中,有两个 Fragment,他们共享某个共同的状态,不仅对这个状态产生一些响应,同时又都可以改变这个状态。也许其中一个 Fragment 还拥有一个子 Fragment,可能也会改变该状态。

这个场景下,我们想快速写出简单漂亮的代码几乎是不可能的事,那么使用「ViewModel」能让我们在 Activity 或者 Fragment 中获取一个准确的生命周期的内存缓存对象,其中可以存有我们需要的 LiveData,然后只要订阅了这个 LiveData,所有的 View 状态都会一致!

viewmodel-lifecycle

如果看一下ViewModel源码的话,你会发现,它的本质是在Activity或者Fragment中,生成一个看不见的Fragment ———— 也就是ViewProviders.of()这个函数传的第一个参数中。然后把 ViewModel 存入这个Fragment里,所有的变量的生命周期跟随这个隐藏的Fragment

这么看下来,我更宁愿称呼这个ViewModelPresenter,事实上我们内部项目也是这么命名的,它的职责就是和数据仓库打交道,然后把组装回来的数据通过LiveData post 到 View 层给开发者。

我们使用RxJava2来实现数据仓库,测试该层,我们首先可以使用Mockito来 mock 需要的 LiveData,然后使用MockServer或者其他的方式,获取我们期望的数据,然后看 LiveData 的行为是否和我们期望的一致(此处的测试更像是 BDD)。

Repository

我们期望把所有 IO(DB and Network)封装成 Repository 层供给 Presenter 去调用,表达形式是 Observable,Presenter 层完全不需要知道数据的存储形式是怎么样的,是缓存还是线上的数据,只需要从 Repository 去取就对了。对于这层的测试可以使用 okhttp 提供的MockWebServer组件去测试相关的代码行为。

Clean Architecture

如果你之前看过「Clean 架构」这个概念,那么你一定对这张图非常熟悉
Clean Architecture
事实上,我们的整个架构组织也是按照「Clean」的模式来的,在使用 Clean 的过程中,有一个原则非常重要,那就是单向数据流。为了保证逻辑的简单和统一,数据流的「单向」必须保证,必须可控,这和 flux 的世界观非常像:
flux-simple-f8-diagram-explained

如果有条件的话,非常建议可以看看 flux 相关的视频 https://facebook.github.io/flux/docs/in-depth-overview.html 加深理解。

结语

做一次架构优化或者做一次架构设计前,我们一定要仔细考虑清楚我们的目的是什么,不能盲目的直接下手,否则我们根本无法对架构设计者的一些理念做出深刻的理解,可能只会徒增烦恼。在质量要求越来越高的当下,我们做出的产品一定是以质量为第一要义的,因此以上是我对Android Architecture Components在可测试领域上的思考。如果有任何的建议和意见,欢迎留言,欢迎指点。

PS:有赞共享移动技术岗位急缺 iOS / Android,最近有想法的同学快来我们的怀抱吧

Android / iOS 开发工程师

岗位职责

  • 参与需求讨论
  • 编写代码文档
  • 业务模块开发
  • 公共组件开发
  • 编写单元测试

我们不仅写测试,还写构建系统,还写 WebView 加速模块,还写 Xposed 等等各种好玩的等着你!
简历速来 wenshengzhang@youzan.com