资讯专栏INFORMATION COLUMN

如何优雅的写单元测试?

gyl_coder / 2697人阅读

摘要:前言越来越多的项目开始尝试写单元测试,关于单元测试的好处以及原理已经有很多资料了,这里不在做过多的讲述,本文主要介绍单元测试在模块化应用中的一些思考,以及如何优雅的写单元测试。最后,依赖注入来写单元测试。

本文由作者潘威授权网易云社区发布。

前言
越来越多的项目开始尝试写单元测试,关于单元测试的好处以及原理已经有很多资料了,这里不在做过多的讲述,本文主要介绍单元测试在模块化应用中的一些思考,以及如何优雅的写单元测试。

易于测试的代码
单元测试最大的痛点就是代码耦合,比如直接持有第三方库的引用、不合理的跨层调用等等,除此之外,static method、new object、singleton 都是不利于测试的代码方式, 这就意味着需要 mock 大量的替身类,增加了测试成本,应该尽量避免,同时使用依赖注入的方式来代替。

如何做好单元测试?
首先,在模块化应用中应该创建公共的单元测试模块,里面可以放一些公共的 BaseTest、Shadow Class、Utils、Junit rules 等等,在业务模块中直接 dependency 进来即可,提高写单元测试的效率。

其次,明确需要测试的代码。刚开始的时候,可以只测中间逻辑层和工具类,这部分代码相对“干净”,易于测试,也是逻辑分支最集中的地方。

最后,依赖注入来写单元测试。试想一下 mock 的类都能够自动完成注入,是不是很爽?这样能大大提高编写测试用例的速度,避免重复的 mock 替身类和静态方法,并提高测试代码的可读性。

所以,我们引入了DI框架来做这件事情!

1、开发阶段

我们只需要在一个类似于 dependency 工厂的地方统一生产这些 dependency 对象,以及这些 dependency 的 dependency。所有需要用到这些 dependency 的地方都从这个工厂里面去获取。

2、测试阶段

定义一个同样的 dependency 工厂,不同的是,该工厂生产的是测试所需要的 Shadow 替身,能够自动识别依赖关系,并实现自动注入!

Dagger2 的应用
没错!前面提到的 DI 框架就是 Dagger2,为了降低风险并减少使用成本,选择了一个模块进行尝试,Dagger2 既能实现模块内的自动注入,又能向外提供注入能力,实现跨模块的注入。

在 Dagger2 里,生产这些 dependency 的工厂叫做 Module ,然而使用者并不是直接向 Module 要 dependency,而是有一个专门的“工厂管理员”,负责接收使用者的要求,然后到 Module 里面去找到相应的 dependency 对象,最后提供给使用者。这个“工厂管理员”叫做 Component。基本上,这就是 Dagger2 里面最重要的两个概念。

上图是 Dagger2 在模块之间的依赖关系,本文只介绍模块内的应用以及单元测试的实现。

1、创建模块级的 LibComponent 和 LibModule

LibModule里面定义了整个模块都要用的dependency,比如PersonalContentInstance 、Scope、 DataSource等等,所以DaggerLibComponent的存在是唯一的,在模块初始化的时候创建好,放在一个地方便于获取。

mInstance.mComponent = DaggerPersonalContentLibComponent.builder()

            .personnalContentLibModule(new PersonnalContentLibModule())
            .build();

2、创建 Frame 级别的 FrameComponent 和 FrameModule

FrameModule 里面定义了某个页面用到的 dependency,比如 Context、Handler、Logic、Adapter 等等,每个页面对应一个 DaggerFrameComponent,在页面的 onCreate() 里面创建好。

3、FrameComponent 依赖于 LibComponent

在 Frame 中可以享受到 LibComponent 中全局依赖的注入,只需要在页面初始化的时候完成注入即可。

DaggerFrameComponent.builder()

.libComponent(mInstance.getComponent())
.frameModule(new FrameModule(this))
.build()
.injectMembers(this);

再看看单元测试里面如何来mock dependency? 比如,LearnRecordDetailLogic 会调用mScope 和 mDataSource 中的方法,而 IPersonalContentScope 和 IDataSource 的实例对象是从 Dagger2 的 Component 里面获取的,怎样把 mScope 和 mDataSource 给 mock 掉呢?

实际上,LearnRecordDetailLogic 向 DaggerLibComponent 获取实例调用的是 PersonnalContentLibModule 中的 provideDataSource() 和 provideScope() 方法,最后返回给 LearnRecordDetailLogic ,也就是说,真正实例化 IPersonalContentScope 和 IDataSource 的地方是在 PersonnalContentLibModule。

@Modulepublic class PersonnalContentLibModule {

......    @PerLibrary
@Provides
PersonalContentInstance providePersonalContentInstance() {        return PersonalContentInstance.getInstance();
}    @PerLibrary
@Provides
IPersonalContentScope provideScope(PersonalContentInstance instance) {        return instance.getScope();
}    @PerLibrary
@Provides
IDataSource provideDataSource(PersonalContentInstance instance) {        return instance.getDataSourse();
}

}
前面创建 DaggerLibComponent 的时候,给它的 builder 传递了一个 PersonnalContentLibModule 对象,如果我们传给 DaggerLibComponent 的 Module 是一个 TestModule,在它的 provide 方法被调用时,返回一个 mock 的 IPersonalContentScope 和 IDataSource,那么在测试代码中获得的,不就是 mock 后的替身对象吗?

public class PersonnalContentLibTestModule extends PersonnalContentLibModule {

......    @Override
PersonalContentInstance providePersonalContentInstance() {        return PowerMockito.mock(PersonalContentInstance.class);
}    @Override
IPersonalContentScope provideScope(PersonalContentInstance instance) {        return PowerMockito.mock(IPersonalContentScope.class);
}    @Override
IDataSource provideDataSource(PersonalContentInstance instance) {        return PowerMockito.mock(IDataSource.class);
}

}
以上就是 Dagger2 在单元测试里的应用。在 LibModule 的基础上派生出一个 LibTestModule,除此之外,LearnRecordDetailLogic 还用到了 Context 和 Handler 对象,所以需要创建一个Frame级别的 Module,然后 override 掉 provide方法,让它返回你想要的 mock 对象。

看一下效果,越复杂的类越能发挥出 Dagger2 的威力!

//使用dagger之前mContext = mock(Context.class);
mHandler = mock(Handler.class);
mDataSource = mock(IDataSource.class);
mScope = mock(IPersonalContentScope.class);
mContentInstance = mock(PersonalContentInstance.class);
when(mContentInstance.getDataSourse()).thenReturn(mDataSource);
when(mContentInstance.getScope()).thenReturn(mScope);
mockStatic(PersonalContentInstance.class);
when(PersonalContentInstance.getInstance()).thenReturn(mContentInstance);//daggerDaggerFrameTestComponent.builder()

.libComponent(ComponentUtil.getLibTestComponent)
.frameTestModule(new FrameTestModule())
.build()
.inject(this);

总结
本文介绍了 Dagger2 在模块内以及单元测试中的应用,DI是一种很好的开发模式,即使不做单元测试,也会让我们的代码更加简洁、干净、解耦,只不过在单元测试中发挥出了更大的威力,让很多难测的代码测试起来更加容易。

最后,介绍一下 Dagger2 的配置方法:

在模块的 build.gradle 中添加

dependencies {

//other dependencies

//Dagger2
compile "com.google.dagger:dagger:${DAGGER_VERSION}"
annotationProcessor "com.google.dagger:dagger-compiler:${DAGGER_VERSION}"}

正常情况下,main 目录下的源代码 build 后,生成代码放在 /build/generated/source/apt/buildType 下面,但是 test 目录下的测试代码,在 compile-time 阶段却无法识别。查看 build 目录,发现存在这部分代码,但是无法正常 import 进来。所以还需要在 build.gradle 中添加如下代码:

android.libraryVariants.all { def aptOutputDir = new File(buildDir, "generated/source/apt/${it.unitTestVariant.dirName}")

it.unitTestVariant.addJavaSourceFoldersToModel(aptOutputDir)

}

免费领取验证码、内容安全、短信发送、直播点播体验包及云服务器等套餐

更多网易技术、产品、运营经验分享请访问网易云社区。

文章来源: 网易云社区

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/25359.html

相关文章

  • 说说Python中的单元测试

    摘要:必然的,他们会抛弃标准库中的,使用或者发明自己心仪的单元测试框架。究其原因,一些人会说时间写代码都不够,哪还有空写单元测试。最后我的个人观点,单元测试其实还有一个非常重要的作用,就是替代函数文档注释。希望从今天起,你的代码也都有单元测试。 单元测试是每种编程语言必学的课题,是保护开发者的强力护盾,每个程序员都在时间允许的情况下尽可能多的写单元测试,今天我们不讨论其必要性,只抛砖引玉聊一...

    chengjianhua 评论0 收藏0
  • 在2018年如何优雅的开发一个typescript语言的npm包?

    摘要:实际开发中,如果每个包都去走一遍这些步骤,步骤好像确实有点多。 欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由小明plus发表 很多时候,我们可能想要用 typescript 语言来创建一些模块,并提交到 npm 供别人使用, 那么在 2018 年,如果我想要初始化这样的一个模块,我需要做哪些步骤呢?: 答案是:创建一个优雅的,对开发者友好的模块,至少需要以下 15...

    Karuru 评论0 收藏0
  • 2020年如何写一个现代的JavaScript库

    摘要:我写过一些开源项目,在开源方面有一些经验,最近开到了阮老师的微博,深有感触,现在一个开源项目涉及的东西确实挺多的,特别是对于新手来说非常不友好最近我写了一个,旨在从多方面快速帮大家搭建一个标准的库,本文将已为例,介绍写一个开源库的知识 我写过一些开源项目,在开源方面有一些经验,最近开到了阮老师的微博,深有感触,现在一个开源项目涉及的东西确实挺多的,特别是对于新手来说非常不友好 show...

    joyqi 评论0 收藏0
  • ContiPerf:: 更为优雅和方便的单元压力测试工具。

    摘要:概述是一个轻量级的单元测试工具,基于二次开发,使用它基于注解的方式,快速在本地进行单元压测并提供详细的报告。当和都有指定时,以执行次数多的为准。测试报告最终的测试报告位于,使用浏览器打开即可。 概述 ContiPerf 是一个轻量级的单元测试工具,基于JUnit 4二次开发,使用它基于注解的方式,快速在本地进行单元压测并提供详细的报告。 Example 1. 新建 SpringBoot...

    _Zhao 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<