本文由做者潘威受权网易云社区发布。android
愈来愈多的项目开始尝试写单元测试,关于单元测试的好处以及原理已经有不少资料了,这里不在作过多的讲述,本文主要介绍单元测试在模块化应用中的一些思考,以及如何优雅的写单元测试。安全
单元测试最大的痛点就是代码耦合,好比直接持有第三方库的引用、不合理的跨层调用等等,除此以外,static method、new object、singleton 都是不利于测试的代码方式, 这就意味着须要 mock 大量的替身类,增长了测试成本,应该尽可能避免,同时使用依赖注入的方式来代替。服务器
首先,在模块化应用中应该建立公共的单元测试模块,里面能够放一些公共的 BaseTest、Shadow Class、Utils、Junit rules 等等,在业务模块中直接 dependency 进来便可,提升写单元测试的效率。框架
其次,明确须要测试的代码。刚开始的时候,能够只测中间逻辑层和工具类,这部分代码相对“干净”,易于测试,也是逻辑分支最集中的地方。ide
最后,依赖注入来写单元测试。试想一下 mock 的类都可以自动完成注入,是否是很爽?这样能大大提升编写测试用例的速度,避免重复的 mock 替身类和静态方法,并提升测试代码的可读性。模块化
因此,咱们引入了DI框架来作这件事情!工具
一、开发阶段post
咱们只须要在一个相似于 dependency 工厂的地方统一辈子产这些 dependency 对象,以及这些 dependency 的 dependency。全部须要用到这些 dependency 的地方都从这个工厂里面去获取。性能
二、测试阶段单元测试
定义一个一样的 dependency 工厂,不一样的是,该工厂生产的是测试所须要的 Shadow 替身,可以自动识别依赖关系,并实现自动注入!
没错!前面提到的 DI 框架就是 Dagger2,为了下降风险并减小使用成本,选择了一个模块进行尝试,Dagger2 既能实现模块内的自动注入,又能向外提供注入能力,实现跨模块的注入。
在 Dagger2 里,生产这些 dependency 的工厂叫作 Module ,然而使用者并非直接向 Module 要 dependency,而是有一个专门的“工厂管理员”,负责接收使用者的要求,而后到 Module 里面去找到相应的 dependency 对象,最后提供给使用者。这个“工厂管理员”叫作 Component。基本上,这就是 Dagger2 里面最重要的两个概念。
上图是 Dagger2 在模块之间的依赖关系,本文只介绍模块内的应用以及单元测试的实现。
一、建立模块级的 LibComponent 和 LibModule
LibModule里面定义了整个模块都要用的dependency,好比PersonalContentInstance 、Scope、 DataSource等等,因此DaggerLibComponent的存在是惟一的,在模块初始化的时候建立好,放在一个地方便于获取。
mInstance.mComponent = DaggerPersonalContentLibComponent.builder() .personnalContentLibModule(new PersonnalContentLibModule()) .build();
二、建立 Frame 级别的 FrameComponent 和 FrameModule
FrameModule 里面定义了某个页面用到的 dependency,好比 Context、Handler、Logic、Adapter 等等,每一个页面对应一个 DaggerFrameComponent,在页面的 onCreate() 里面建立好。
三、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) }
免费领取验证码、内容安全、短信发送、直播点播体验包及云服务器等套餐
更多网易技术、产品、运营经验分享请访问网易云社区。
相关文章:
【推荐】 当你想进行简单性能测试监控的时候应该如何选择监控命令?
【推荐】 数据分析融入至BI工具的新思路
【推荐】 视觉设计师的进化