Android单元测试(五):依赖注入,将mock方便的用起来

上一篇文章中,我们讲了要将mock出来的dependency真正使用起来,需要在测试环境下通过某种方式set 到用到它的那个对象里面进去,替换掉真实的实现。我们前面举的例子是:

public class LoginPresenter {
    private UserManager mUserManager = new UserManager();

    public void login(String username, String password) {
        //。。。some other code
        mUserManager.performLogin(username, password);
    }
}

在测试LoginPresenter#login()时,为了能够将mock出来的UserManager set到LoginPresenter里面,我们前面的做法是简单粗暴,给LoginPresenter加一个UserManager的setter。然而这种做法毕竟不是很优雅,一般来说,我们正式代码里面是不会去调用这个setter,修改UserManager这个对象的。因此这个setter存在的意义就纯粹是为了方便测试。这个虽然不是没有必要,却不是太好看,因此在有选择的情况下,我们不这么做。在这里,我们介绍依赖注入这种模式。

对于依赖注入(Dependency Injection,以下简称DI)的准确定义可以在这里找到。它的基本理念这边简单描述下,首先这是一种代码模式,这个模式里面有两个概念:Client和Dependency。假如你的代码里面,一个类用到了另外一个类,那么前者叫Client,后者叫Dependency。结合上面的例子,LoginPresenter用到了UserManager,那么LoginPresenter叫Client,UserManager叫Dependency。当然,这是个相对的概念,一个类可以是某个类的Dependency,却是另外一个类的Client。比如说如果UserManager里面用到了Retrofit,那么相对于RetrofitUserManager又是Client。DI的基本思想就是,对于Dependency的创建过程,并不在Client里面进行,而是由外部创建好,然后通过某种方式set到Client里面。这种模式,就叫做依赖注入。

是的,依赖注入就是这么简单的一个概念,这边需要澄清的一点是,这个概念本身跟dagger2啊,RoboGuice这些框架并没有什么关系。现在很多介绍DI的文章往往跟dagger2是在一起的,因为dagger2的使用相对来说不是很直观,所以导致很多人认为DI是多么复杂的东西,甚至认为只能用dagger等框架来实现依赖注入,其实不是这样的。实现依赖注入很简单,dagger这些框架只是让这种实现变得更加简单,简洁,优雅而已。

DI的常见实现方式

下面介绍DI的实现方式,通常来说,这里是大力介绍dagger2的地方。但是,虽然dagger2的确是非常好的东西,然而如果我直接介绍dagger2的话,会很容易导致一个误区,认为在测试的时候,也只能用dagger来做依赖注入或创建对应的测试类,因此,我这边刻意不介绍dagger。先让大家知道最基本的DI怎么实现,然后在测试的时候如何更方便高效的使用。

实现DI这种模式其实很简单,有多种方式,上一篇文章中提到的setter,其实就是实现DI的一种方式,叫做 setter injection 。此外,通过方法的参数传递进去(argument injection),也是实现DI的一种方式:

public class LoginPresenter {

    //这里,LoginPresenter不再持有UserManager的一个引用,而是作为方法参数直接传进去
    public void login(UserManager userManager, String username, String password) {
        //... some other code
        userManager.performLogin(username, password);
    }
}

然而更常用的方式,是将Dependency作为Client的构造方法的参数传递进去:

public class LoginPresenter {

    private final UserManager mUserManager;

    //将UserManager作为构造方法参数传进来
    public LoginPresenter(UserManager userManager) {
        this.mUserManager = userManager;
    }

    public void login(String username, String password) {
        //... some other code
        mUserManager.performLogin(username, password);
    }
}

这种实现DI的模式叫 Constructor Injection。其实一般来说,提到DI,指的都是这种方式。这种方式的好处是,依赖关系非常明显。你必须在创建这个类的时候,就提供必要的dependency。这从某种程度上来说,也是在说明这个类所完成的功能。因此,尽量使用 Constructor injection

说到这里,你可能会有一个疑问,如果把依赖都声明在Constructor的参数里面,这会不会让这个类的Constructor参数变得非常多?如果真的发生这种情况了,那往往说明这个类的设计是有问题的,需要重构。为什么呢?我们代码里面的类,一般可以分为两种,一种是Data类,比如说UserInfo,OrderInfo等等。另外一种是Service类,比如UserManager, AudioPlayer等等。所以这个问题就有两种情况了:

  1. 如果Constructor里面传入的很多是基本类型的数据或数据类,那么或许你要做的,是创建一个(或者是另一个)数据类把这些数据封装一下,这个过程的价值可是大大滴!而不仅仅是封装一下参数的问题,有了一个类,很多的方法就可以放到这个类里面了。这点请参考Martin Fowler的《重构》第十章“Introduce Parameter Object”。
  2. 如果传入的很多是service类,那么这说明这个类做的事情太多了,不符合单一职责的原则(Single Responsibility Principle,SRP),因此,需要重构。

接下来说回我们的初衷:DI在测试里面的应用。

DI在单元测试里面的应用

所谓DI在单元测试里面的应用,其实说白了就是使用DI模式,将mock出来的Dependency set到Client里面去。我相信这篇文章解释到这里,那么答案也就比较明显了,为了强调我们要尽量使用 Constructor injection,对于 setter InjectionArgument injection 这边就不做代码示例了。
如果你的代码使用的是 Constructor injection

public class LoginPresenter {

    private final UserManager mUserManager;

    //将UserManager作为构造方法参数传进来
    public LoginPresenter(UserManager userManager) {
        this.mUserManager = userManager;
    }

    public void login(String username, String password) {
        //... some other code
        mUserManager.performLogin(username, password);
    }
}

其中我们要测的方法是login(), 要验证login()方法调用了mUserManagerperformLigon()。对应的测试方法如下:

public class LoginPresenterTest {

    @Test
    public void testLogin() {
        UserManager mockUserManager = Mockito.mock(UserManager.class);
        LoginPresenter presenter = new LoginPresenter(mockUserManager);  //创建的时候,讲mock传进去

        presenter.login("xiaochuang", "xiaochuang password");

        Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");
    }
}

很简单,对吧。

小结

这篇文章介绍了DI的概念,以及在单元测试里面的应用,这里特意没有介绍dagger2的使用,目的是要强调:

  1. 一个灵活的,易于测试的,符合SRP的,结构清晰的项目,关键在于要应用依赖注入这种模式,而不是用什么来做依赖注入。
  2. 等你学会使用dagger以后,要记得在测试的时候,如果可以直接mock dependency并传给被测类,那就直接创建,不是一定要使用dagger来做DI

然而如果完全不使用框架来做DI,那么在正式代码里面就有一个问题了,那就是dependency的创建工作就交给上层client去处理了,这可不是件好事情。想想看,LoginActivity里面创建LoginPresenter的时候,还得知道LoginPresenter用了UserManager。然后创建一个UserManager对象给LoginPresenter。对于LoginActivity来说,它觉得我才懒得管你用什么样的UserManager呢,我只想告诉你login的时候,你给我老老实实的login就好了,你用什么Manager我不管。所以,直接在LoginActivity里面创建UserManager,可能不是个好的选择。那怎么样算是一个好的选择呢?dagger2给了我们答案。
于是下一篇文章我们介绍dagger2。

文中的代码在github这个项目里面

最后,如果你对安卓单元测试感兴趣,欢迎加入我们的交流群,因为群成员超过100人,没办法扫码加入,请关注下方公众号获取加入方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,043评论 25 707
  • 小t一直不知道家乡到底是什么样子。 不是从小漂泊流浪在外的孩子,也不是被邻居街坊看不上眼的小子,说来倒也还是一贯被...
    Laeddis阅读 227评论 0 2
  • 前几天感恩节,公司的同事在笔耕不辍的写着信封,寄给千里之外的,离我们很远也很近的会员。 陈童说,这是一封有爱的信。...
    lemon若尘阅读 1,632评论 0 0
  • 蝴蝶能搅乱眼睛 无话不谈的人多含辛 如果飞蛾能逃离烟熏 你说没有念念不忘 我就信 你说故乡有眼睛 黑白分明 那分明...
    池囊阅读 143评论 1 1
  • � 当列车在躁动中狂奔时,窗外的群山一直保持静默,这正是我们对她的敬畏。 ...
    烽火煤阅读 133评论 0 1