热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

[译]使用Dagger2,Mockito和自定义JUnit规则执行Android测试

原文:AndroidtestingusingDagger2,MockitoandacustomJUnitrule作者:FabioCollini译者:lovexiaov依赖注入是得到

原文:Android testing using Dagger 2, Mockito and a custom JUnit rule
作者:Fabio Collini
译者:lovexiaov

依赖注入是得到可测代码的关键概念。使用依赖注入可以方便的用虚拟技术替换真实对象,以改变和验证系统的行为。

Dagger 2 是一个用于许多 Android 工程的依赖注入库,本文我们将会讲解如何利用该库的优势测试 Android 应用。

我们先来看一个简单的例子, MainService 类使用其他两个类模拟对一个外部服务的调用并打印结果:

public class MainService {
private RestService restService;
private MyPrinter printer;
@Inject public MainService(RestService restService,
MyPrinter printer) {
this.restService = restService;
this.printer = printer;
}
public void doSomething() {
String s = restService.getSomething();
printer.print(s.toUpperCase());
}
}

doSomething 方法没有直接的输入和输出,但有了依赖注入和 Mockito,测试该类并不困难。

其他类的实现很简单:

public class RestService {
public String getSomething() {
return "Hello world";
}
}
public class MyPrinter {
public void print(String s) {
System.out.println(s);
}
}

我们希望独立测试 MainService,因此我们不会在这两个类中使用 Inject 注解(下文有详细介绍)。我们在 Dagger 模块中实例化它们:

@Module
public class MyModule {
@Provides @Singleton public RestService provideRestService() {
return new RestService();
}
@Provides @Singleton public MyPrinter provideMyPrinter() {
return new MyPrinter();
}
}

我们需要一个 Dagger 组件实例化 MainService 对象并且注入 Activity:

@Singleton
@Component(modules = MyModule.class)
public interface MyComponent {
MainService mainService();
void inject(MainActivity mainActivity);
}

使用 Mockito 执行 JUnit 测试

使用 Mockito 可以方便的独立测试 MainService 类:

public class MainServiceTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock RestService restService;
@Mock MyPrinter myPrinter;
@InjectMocks MainService mainService;
@Test public void testDoSomething() {
when(restService.getSomething()).thenReturn("abc");
mainService.doSomething();
verify(myPrinter).print("ABC");
}
}

MockitoRule 的使用与 MockitoJUnitRunner 类似,它调用静态方法 MockitoAnnotations.initMocks 填充注解字段。幸好有 InjectMocks 注解,mainService 对象会自动创建,测试中定义的两个虚拟对象被用来作为构造参数。

Dagger 在这种测试中用不到,因为测试非常简单并且是一个纯单元测试。

Dagger 2 测试

有时我们想使用 Dagger 实例化对象来编写高级测试。Artem Zinnatullin 的这篇文章 中介绍了最简单的重写一个对象的方法。按照他的建议我们可以定义一个继承原始 module 的 TestModule并且重写方法返回两个虚拟对象:

public class TestModule extends MyModule {
@Override public MyPrinter provideMyPrinter() {
return Mockito.mock(MyPrinter.class);
}
@Override public RestService provideRestService() {
return Mockito.mock(RestService.class);
}
}

我们还需要一个 TestComponent 来注入测试对象:

@Singleton
@Component(modules = MyModule.class)
public interface TestComponent extends MyComponent {
void inject(MainServiceDaggerTest test);
}

该测试类包含3个带 Inject 注解的字段,在 setUp 方法中我们创建了 TestComonent 并用它注入测试对象来填充字段:

public class MainServiceDaggerTest {
@Inject RestService restService;
@Inject MyPrinter myPrinter;
@Inject MainService mainService;
@Before public void setUp() {
TestComponent compOnent= DaggerTestComponent.builder()
.myModule(new TestModule()).build();
component.inject(this);
}
@Test public void testDoSomething() {
when(restService.getSomething()).thenReturn("abc");
mainService.doSomething();
verify(myPrinter).print("ABC");
}
}

该测试可以执行,但有些地方需要改进:

  • restServicemyPrinter 字段包含两个虚拟对象,但是使用 Inject 注解而不是前面测试中使用的 Mock 注解。
  • 需要一个测试 module 和一个测试组件来编写和执行测试。

DaggerMock:用来覆盖 Dagger 2 对象的 JUnit 规则

Dagger 使用一个注解处理器分析工程中的所有类来查找注解,但前面例子中的 TestModule 没有包含任何 Dagger 注解。

DaggerMock 的基本思想是创建一个动态创建 Module 子类的 JUnit 规则。 该子类中的方法返回在测试对象中定义的虚拟对象。这有点不好解释,我们来看一下最终结果:

public class MainServiceTest {
@Rule public DaggerMockRule mockitoRule =
new DaggerMockRule<>(MyComponent.class, new MyModule())
.set(component -> mainService = component.mainService());
@Mock RestService restService;
@Mock MyPrinter myPrinter;
MainService mainService;
@Test
public void testDoSomething() {
when(restService.getSomething()).thenReturn("abc");
mainService.doSomething();
verify(myPrinter).print("ABC");
}
}

在此例中,我们利用规则动态创建了一个返回在测试中定义的虚拟对象的 MyModule 的子类,而不是真实的对象。此测试类似于本文中的第一个测试(使用 InjectMocks 注解的那个测试),最大的不同之处在于现在我们使用 Dagger 创建 mainService 字段。使用 DaggerMockRule 的其它好处如下:

  • 不必将所有测试对象的依赖定义在测试中。当一个依赖对象没有在测试中定义,则使用在 Dagger 配置中定义的对象。
  • 覆盖一个没有直接使用的对象十分简单(例如,当 A 对象引用 B 对象而 B 对象持有 C 对象的引用时,我们只想覆盖 C 对象)。

Espresso 测试

已经有许多关于 Dagger,Mockito 和 Espresso 集成的文章,例如 Chui-Ki Chan 的这篇文章 包含了该问题做常见的解决方案。

我们来看另一个例子,在 Activity 中调用之前例子中的方法:

public class MainActivity extends AppCompatActivity {
@Inject MainService mainService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
App app = (App) getApplication();
app.getComponent().inject(this);
mainService.doSomething();
//...
}
}

我们可以使用 ActivityTestRule 测试该 Activity,该测试与 MainServiceDaggerTest 类似(使用了 TestComponentTestModule):

public class MainActivityTest {
@Rule public ActivityTestRule activityRule =
new ActivityTestRule<>(MainActivity.class, false, false);
@Inject RestService restService;
@Inject MyPrinter myPrinter;
@Before
public void setUp() throws Exception {
EspressoTestComponent compOnent=
DaggerEspressoTestComponent.builder()
.myModule(new EspressoTestModule()).build();
getApp().setComponent(component);

component.inject(this);
}
private App getApp() {
return (App) InstrumentationRegistry.getInstrumentation()
.getTargetContext().getApplicationContext();
}
@Test
public void testCreateActivity() {
when(restService.getSomething()).thenReturn("abc");
activityRule.launchActivity(null);
verify(myPrinter).print("ABC");
}
}

DaggerMock 和 Espresso

这个测试可以简单的使用 DaggerMockRule,在 lambda 表达式中我们设置了应用中的组件来使用虚拟对象覆盖 Dagger 对象:

public class MainActivityTest {
@Rule public DaggerMockRule daggerRule =
new DaggerMockRule<>(MyComponent.class, new MyModule())
.set(component -> getApp().setComponent(component));
@Rule public ActivityTestRule activityRule =
new ActivityTestRule<>(MainActivity.class, false, false);
@Mock RestService restService;
@Mock MyPrinter myPrinter;
//...
}

此规则也可被用在 Robolectric 测试中,在该工程 中有一个例子。

自定义规则

相同的规则经常被用于一个工程中的所有测试,我们可以创建一个子类来避免复制和粘贴。例如之前例子中的规则可以写入到一个新类 MyRule 中:

public class MyRule extends DaggerMockRule {
public MyRule() {
super(MyComponent.class, new MyModule());
set(component -> getApp().setComponent(component));
}
private App getApp() {
return (App) InstrumentationRegistry.getInstrumentation()
.getTargetContext().getApplicationContext();
}
}

某些情况下我们希望覆盖一个对象,但我们不需要在测试中引用。例如在一个 Espresso 测试中我们不想跟踪对远程服务器事件的分析,我们可以使用虚拟对象解决该问题。若要定义一个自定义对象,我们可以调用基于规则的下列方法之一:

  • provides(Class originalClass, T newObject): 使用指定对象覆盖一个类的对象;
  • provides(Class originalClass, Provider provider): 与上一个方法类似,但对非单例对象非常有用;
  • providesMock(Class… originalClasses): 使用作为参数传入的所有虚拟对象覆盖。这是对 provide(MyObject.class, Mockito.mock(MyObject.class)) 的一种简写形式.

一个使用这些方法自定义规则的例子可以在 CoseNonJaviste 中查看:

public class CnjDaggerRule
extends DaggerMockRule {
public CnjDaggerRule() {
super(ApplicationComponent.class, new AppModule(getApp()));
provides(SchedulerManager.class,
new EspressoSchedulerManager());
providesMock(WordPressService.class, TwitterService.class);
set(component -> getApp().setComponent(component));
}
public static CoseNonJavisteApp getApp() {
return (CoseNonJavisteApp)
InstrumentationRegistry.getInstrumentation()
.getTargetContext().getApplicationContext();
}
}

最终的 Espresso 测试版本非常简单(你将不必使用 TestComponentTestModule!):

public class MainActivityTest {
@Rule public MyRule daggerRule = new MyRule();
@Rule public ActivityTestRule activityRule =
new ActivityTestRule<>(MainActivity.class, false, false);
@Mock RestService restService;
@Mock MyPrinter myPrinter;
@Test
public void testCreateActivity() {
when(restService.getSomething()).thenReturn("abc");
activityRule.launchActivity(null);
verify(myPrinter).print("ABC");
}
}

DaggerMock 是一个 GitHub 上的开源工程,你可以使用 JitPack 仓库轻易的将它集成到你的工程中。


推荐阅读
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 标题: ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 本文介绍了在iOS开发中使用UITextField实现字符限制的方法,包括利用代理方法和使用BNTextField-Limit库的实现策略。通过这些方法,开发者可以方便地限制UITextField的字符个数和输入规则。 ... [详细]
  • 本文介绍了在MFC下利用C++和MFC的特性动态创建窗口的方法,包括继承现有的MFC类并加以改造、插入工具栏和状态栏对象的声明等。同时还提到了窗口销毁的处理方法。本文详细介绍了实现方法并给出了相关注意事项。 ... [详细]
  • GreenDAO快速入门
    前言之前在自己做项目的时候,用到了GreenDAO数据库,其实对于数据库辅助工具库从OrmLite,到litePal再到GreenDAO,总是在不停的切换,但是没有真正去了解他们的 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
author-avatar
Q小泓别_431
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有