• Android单元测试之Local unit tests(上)

    • 简介
    • 本地单元测试
      • JUnit 4

        • 添加依赖
        • 测试例子
        • 结论
      • Mockito
        • 添加依赖
        • 测试例子-mock基本使用
        • 测试例子-mock与spy区别
        • 测试例子-异步任务回调
        • 测试例子-mock注解
        • 测试例子-mock Android dependencies
        • 结论
    • 未完待续
    • 参考链接

Android单元测试之Local unit tests(上)

单元测试目前的地位比较尴尬,你问一个软件开发“单元测试重要么?”大家都说重要,但在真正生产环境中使用的团队少之又少。为什么会出现这种情况呢?总结了下主要有两个原因:
1. 开发任务紧张,敏捷开发没有时间去做单元测试,影响开发效率
2. Android开发单元测试不好做,不知如何去做

对于问题1,Martin Fowler在《重构》里面还解释了为什么单元测试可以节省时间:“我们写程序的时候,其实大部分时间不是花在写代码上面,而是花在debug上面,是花在找出问题到底出在哪上面,而单元测试可以最快的发现你的新代码哪里不work,这样你就可以很快的定位到问题所在,然后给以及时的解决,这反而能提高工作效率”

问题2正是我这篇分享的内容了,让我们一起去做好Android单元测试。

简介

Android单元测试分为两类:

Local unit tests(本地单元测试)

测试在计算机的本地 Java 虚拟机 (JVM) 上运行。 当您的测试没有 Android 框架依赖项或当您可以模拟 Android 框架依赖项时,可以利用这些测试来尽量缩短执行时间。

Instrumented tests(仪器测试)

测试在设备或模拟器上运行。 这些测试有权访问 Instrumentation API,让您可以获取某些信息(例如您要测试的应用的 Context), 并且允许您通过测试代码来控制受测应用。 可以在编写集成和功能 UI 测试来自动化用户交互时,或者在测试具有模拟对象无法满足的 Android 依赖项时使用这些测试。

做为这个系列的第一篇,我们主要来聊聊本地单元测试

本地单元测试

一个很重要的点是:“我们什么情况下使用本地单元测试?” 有以下两个准则
1. 测试类是pure java类
2. 测试类简单依赖Android环境

使用Android studio创建新项目时会自动帮你创建本地测试文件目录module-name/src/test/java/.

在本地单元测试中使用最广泛的是JUnit 4,Mockito,Robolectric 我们分别介绍一下:

JUnit 4

JUnit是在Java中使用最为广泛的单元测试库,其他大部分测试框架基于或兼容JUnit,我们看看在Android中是如何使用的

添加依赖
dependencies {testCompile 'junit:junit:4.12'
}
测试例子

我们有个待测试的类Calculator

public class Calculator {public int add(int one, int another) {return one + another;}public int multiply(int one, int another) {return one * another;}
}

我们现在需要对add()multiply()进行单元测试,有两种方式生成测试类:
1. 我们在目录module-name/src/test/java/.新建对应测试类
2. 我们在待测试类Calculator上使用快捷键CTRL+SHIFT+T一键生成测试类

AndroidStudio上推荐使用第二种方式,会很方便。勾选相应的测试类及setup与tearDown即可生成

public class CalculatorTest {@Beforepublic void setUp() throws Exception {}@Afterpublic void tearDown() throws Exception {}@Testpublic void add() throws Exception {}@Testpublic void multiply() throws Exception {}}

普通的Java类,但是方法上会有相应的注解,我们这里来讲解下这些注解:

@Before:初始化方法 对于每一个测试方法都要执行一次(注意与BeforeClass区别,后者是对于所有方法执行一次)
@After:释放资源 对于每一个测试方法都要执行一次(注意与AfterClass区别,后者是对于所有方法执行一次)
@Test:测试方法,在这里可以测试期望异常和超时时间
@Test(expected=ArithmeticException.class)检查被测方法是否抛出ArithmeticException异常
@Ignore:忽略的测试方法
@BeforeClass:针对所有测试,只执行一次,且必须为static void
@AfterClass:针对所有测试,只执行一次,且必须为static void

我们验证下执行顺序:

public class CalculatorTest2 {@BeforeClasspublic static void beforeClass() {System.out.println("@BeforeClass");}@Beforepublic void setUp() throws Exception {System.out.println("@Before");}@Afterpublic void tearDown() throws Exception {System.out.println("@After");}@Testpublic void add() throws Exception {System.out.println("@Test add()");}@Testpublic void multiply() throws Exception {System.out.println("@Test multiply()");}@AfterClasspublic static void afterClass() {System.out.println("@AfterClass");}}
@BeforeClass
@Before
@Test add()
@After
@Before
@Test multiply()
@After
@AfterClass

JUnit另一个重要的知识点就是Assert(断言),JUnit提供了一系列assertXXX方法用来对预期与实际结果进行测试,内容比较简单,具体可以参考JUnit官方文档 Assertions CN

我们有了这些基础知识,就可以开始进行单元测试了

public class CalculatorTest {Calculator mCalculator;@Beforepublic void before() {/*** @Before 一般设置测试的前置条件*/mCalculator = new Calculator();}@Testpublic void add() {Assert.assertEquals(4, mCalculator.add(1, 3));}@Testpublic void multiply() {Assert.assertEquals(5, mCalculator.multiply(1, 5));}@Afterpublic void after() {}
}

在相应的方法上点击就能直接运行单元测试,也可以在测试类上点击运行,一次运行多个测试方法。

整个流程,我们可以发现使用JUnit结合AndroidStudio快捷键可以很快完成一次单元测试。

结论

优点:执行速度快,一般用于工具类或Pure Java类(MVP架构中的Presenter层等)
缺点:只能测试有明确返回值的方法,对void方法无法测试;对存在复杂依赖的类无法测试

Mockito

Mockito是什么?Mockito是一套非常强大的测试框架,被广泛的应用于Java程序的unit test中。Mockito可以很好的解决JUnit4的两个缺点。它主要能做到以下两点:
1. 验证对象方法调用的情况
2. mock(模拟/代理/替换)对象方法的行为

添加依赖
dependencies {testCompile 'junit:junit:4.12'testCompile "org.mockito:mockito-core:1.10.19"
}
测试例子-mock基本使用

我们新建一个测试类MockTest,这里有Mockito的最重要的几个API例子,代码中有详细注释

public class MockTest {@Testpublic void testMock() throws Exception {// 使用Mockito.mock(xxx) 创建一个Mock对象ArrayList mockedList = Mockito.mock(ArrayList.class);// 通过Mockito.when(xxx).thenXXX();可以改变对象方法的行为// 语义理解是当调用mockedList.get(0)时返回"first" Mockito.when(mockedList.get(0)).thenReturn("first");// 控制台打印出 "first"// 实际上 mockedList是一个空对象,我们并没有添加任何元素,这就是Mockito改变对象行为的能力System.out.println(mockedList.get(0));// 对于没有stubbed的方法会返回默认值,(e.g: 0,false, ... for int/Integer, boolean/Boolean, ...)System.out.println(mockedList.get(999));// 验证list.get(0)是否调用Mockito.verify(mockedList).get(0);// 验证list.get(2)没有调用Mockito.verify(mockedList, Mockito.never()).get(2);// 验证list.get(int)调用次数;Mockito.提供了很多anyXXX方法,在你不关心参数的情况下可以直接使用Mockito.verify(mockedList, Mockito.times(2)).get(Mockito.anyInt());}
}

我们再来整理一下:
1. 通过Mockito.mock(xxx) 创建一个Mock对象
2. 通过Mockito.when(xxx).thenXXX();可以(指定/替换)对象方法的行为
3. 通过Mockito.verify(xxx).someMethod();验证该方法是否被调用

测试例子-mock与spy区别

另一个与Mockito.mock(xxx)相似的重要方法是Mockito.spy(xxx),因为都会创建一个Mock对象很多人会弄不清它们的区别,我们通过代码来看一下:

    @Testpublic void testSpy() {ArrayList<String> mockObj = Mockito.mock(ArrayList.class);mockObj.add("one");mockObj.add("two");// Mockito.mock创建的对象对没有stubbed的方法返回默认值// print 0System.out.println(mockObj.size());// print nullSystem.out.println(mockObj.get(0));// Mockito.spy 生成的对象,执行中会调用对象实现方法;mock则是返回默认值,不会调用实现List<String> spyObj = Mockito.spy(ArrayList.class);spyObj.add("one");spyObj.add("two");// Mockito.spy创建的对象会调用真实的方法// print 2System.out.println(spyObj.size());// print "one"System.out.println(spyObj.get(0));// 因为spyObj.get(2)会真实调用,会抛出异常throw IndexOutOfBoundsException// Mockito.when(spyObj.get(2)).thenReturn("first");Mockito.doReturn("first").when(spyObj).get(2);System.out.println(spyObj.get(2));}

整理一下:
1. Mockito.mock创建的对象对没有stubbed的方法返回默认值,不会调用实现
2. Mockito.spy 生成的对象,执行中会调用对象实现方法
3. 当你需要关心对象实现细节时使用#spy;当你只想知道对象方法调用情况使用#mock就能满足需求

测试例子-异步任务回调

我们在开发中经常会使用到回调的方式,那这种情况该如何通过Mockito模拟呢?我们用登录的例子来讲解,一起看看以下代码:

// 登录管理类
public class LoginMgr {/*** 访问服务器进行登录验证*/private LoginApi mLoginApi;public LoginMgr(LoginApi loginApi) {mLoginApi = loginApi;}public void login(String name, String password) {mLoginApi.login(name, password, new LoginCallback() {@Overridepublic void onSuccess() {loginSuccess();}@Overridepublic void onFailed() {loginFail();}});}public void loginSuccess() {System.out.println("LoginMgr.loginSuccess");}public void loginFail() {System.out.println("LoginMgr.loginFail");}
}
// 登录回调接口
public interface LoginCallback {void onSuccess();void onFailed();
}

我们现在要对login方法进行测试,验证流程
1. LoginMgr登录成功调用loginSuccess();
2. LoginMgr登录失败调用loginFail();

public class LoginMgrTest {private LoginApi mLoginApi;private LoginMgr mLoginMgr;private ArgumentCaptor<LoginCallback> mArgumentCaptor;@Beforepublic void setUp() throws Exception {// 我们关注LoginApi#login是否得到调用,使用mockmLoginApi = Mockito.mock(LoginApi.class);// 我们关注LoginMgr#login需要真正执行,使用spymLoginMgr = Mockito.spy(new LoginMgr(mLoginApi));// 通过ArgumentCaptor捕获传递到LoginApi#login的回调mArgumentCaptor = ArgumentCaptor.forClass(LoginCallback.class);}@Testpublic void testLoginSuccess() {// 调用登录方法mLoginMgr.login("lee", "123");// 验证LoginApi#login是否执行Mockito.verify(mLoginApi).login(Mockito.anyString(), Mockito.anyString(), mArgumentCaptor.capture());// 调用捕获到的回调onSuccess函数mArgumentCaptor.getValue().onSuccess();// 验证LoginMgr 回调中loginSuccess是否执行Mockito.verify(mLoginMgr).loginSuccess();// 流程验证无问题}@Testpublic void testLoginFailed() {mLoginMgr.login("lee", "123");Mockito.verify(mLoginApi).login(Mockito.anyString(), Mockito.anyString(), mArgumentCaptor.capture());mArgumentCaptor.getValue().onFailed();Mockito.verify(mLoginMgr).loginFail();}}

总结一下:
1. 通过ArgumentCaptor#capture()可以捕获传递的回调
2. 通过ArgumentCaptor控制回调的流程

测试例子-mock注解

Mockito对大部分API支持注解,极大的减少了重复代码,增强可读性,易于排查错误。
我们通过例子来了解如何使用这些注解:

@RunWith(MockitoJUnitRunner.class)
public class LoginMgrAnnotationTest {/*** @Mock* @Spy* @Captor** 1.方便对象的创建* 2.减少对象创建的重复代码* 3.提高测试代码可读性*/@org.mockito.Mockprivate LoginApi mLoginApi;/*** @InjectMocks** 通过这个注解,可实现自动注入mock对象。* Mockito首先尝试类型注入,如果有多个类型相同的mock对象,那么它会根据名称进行注入** 自动注入了LoginApi对象到LoginMgr*/@Spy @InjectMocksprivate LoginMgr mLoginMgr;@Captorprivate ArgumentCaptor<LoginCallback> mArgumentCaptor;@Beforepublic void setUp() throws Exception {/*** 使用Mock注解* 1. MockitoAnnotations.initMocks(testClass);* 2. 测试类开头@RunWith(MockitoJUnitRunner.class)*/
//        MockitoAnnotations.initMocks(this);}@Testpublic void testLoginSuccess() {// 调用登录方法mLoginMgr.login("lee", "123");// 验证LoginApi#login是否执行Mockito.verify(mLoginApi).login(Mockito.anyString(), Mockito.anyString(), mArgumentCaptor.capture());// 调用捕获到的回调onSuccess函数mArgumentCaptor.getValue().onSuccess();// 验证LoginMgr 回调中loginSuccess是否执行Mockito.verify(mLoginMgr).loginSuccess();// 流程验证无问题}}
测试例子-mock Android dependencies

Context是Android中特有的,我们Android中很多方法都会通过Context,你会发现你无法新建一个Context类,调用相应的方法也会抛出异常,因为默认情况下,Android Gradle for Gradle会针对android.jar库的修改版本执行本地单元测试,该库不包含任何实际代码(这些API仅由设备上的Android系统映像提供)。

从你的单元测试中调用Android类的方法会引发异常,这是为了确保您只测试您的代码,并且不依赖于Android平台的任何特定行为。

我们在单元测试就会很棘手,Mockito能很好的帮我们解决这个问题,我们看以下代码:

// 待测试的类
public class Mock {public String getString(Context context) {String str = context.getResources().getString(R.string.app_name);System.out.println(str);return str;}
}
@RunWith(MockitoJUnitRunner.class)
public class MockTest {@MockContext mContext;@MockResources mResources;@Testpublic void getString() {// Gradle运行Local Unit Test 所使用的android.jar里面所有API都是空实现,并抛出异常的// 只有apk安装到终端/模拟器 这些才是真正的实现Mockito.when(mContext.getResources()).thenReturn(mResources);Mockito.doReturn("mock string").when(mResources).getString(Mockito.anyInt());String result = mMock.getString(mContext);Assert.assertEquals(result, "mock string");}
}

这样我们可以避免调用Android代码发生的异常。

结论

到这里我们基本把Mockito框架介绍完了,Mockito结合JUnit4基本可以完成对任一对象进行单元测试的要求了。尤其是在使用MVP架构对P层的测试中,自动注入view module的mock对象,可以测试任何路径了。当然Mockito还是有它的缺陷:
1. Mockito无法对final类型、private类型以及静态类型的方法进行mock。
2. Android的支持还不够好

针对上面的问题1,可以使用 powermock 解决,因为和Mockito相似,这里就不多介绍了,有需要的同学请自行前往阅读。

未完待续

接下来我们要介绍神器“Robolectric”就是为Android而生的单元测试框架,由于篇幅问题分为两部分,未完待续……

参考链接

Fundamentals of Testing
Android单元测试之JUnit4
robolectric
使用Robolectric进行Android单元测试
Android 集成Robolectric下的一些坑
Robolectric使用教程

Android单元测试之Local unit tests(上)相关推荐

  1. Android 单元测试之Robolectric

    前言 在博客Android 单元测试之PowerMockito,主要介绍PowerMockito的使用和对Java测试用例的强大支持.但对于Android app开发来说,写起单元测试很痛苦:一方面单 ...

  2. Android 单元测试之Mockito

    在博客Android 单元测试之JUnit4中,我们简单地介绍了:什么是单元测试,为什么要用单元测试,并展示了一个简单的单元测试例子.在文章中,我们只是展示了对有返回类型的目标public方法进行了单 ...

  3. Android 单元测试之UI测试

    Android 单元测试之UI测试 UI测试 Espresso 官网地址 Espresso是Google官方的一个针对Android UI测试的库,可以自动化的进行UI测试. Espresso可以验证 ...

  4. Android单元测试之 Mockito

    1. 介绍 1.1 Mock介绍 在了解Mockito的概念之前,需要先了解Mock. mock是在测试过程中,对于一些不容易构造/获取的对象,用一个虚拟的Mock对象来创建以便测试的测试方法. 在平 ...

  5. 《Pragmatic Unit Testing In Java with JUnit》—单元测试之道读后感

    <Pragmatic Unit Testing In Java with JUnit>                                                    ...

  6. Android 仪器化单元测试(instrumented unit tests) Androidx kotlin版本

    前言 近期需要进行单元测试,测试内容需要真机环境,所以需要使用instrumented unit tests,用来在跑在真机上进行测试. 本blog用于记录. 简介 仪器化单元测试(instrumen ...

  7. [Test apps on Android] Build instrumented unit tests

    本文是翻译的Test apps on Android的官方文档 Build instrumented unit tests 本文不照搬每一个单词,理解有误请跳转原文链接. Build instrume ...

  8. Android Instrumented Unit Tests (AndroidTests)

    參考官方文件Build instrumented unit tests Cannot resolve symbol 'androidx.*' / 'org.junit.*' Android Studi ...

  9. Android自动化测试之MonkeyRunner MonkeyDevice MonkeyImage API使用详解 脚本编写 脚本录制回放

    MonkeyRunner 系列文章 MonkeyRunner简介 MonkeyRunner 三大模块 MonkeyRunner API MonkeyDevice API MonkeyImage API ...

最新文章

  1. 你可以把编程当做一项托付终身的职业
  2. [erlang] gen_tcp传输文件原型
  3. linux安装多个mysql数据库_linux下多个mysql5.7.19(tar.gz)安装图文教程
  4. CSS3中弹性盒布局的最新版
  5. 深入react技术栈(1):React简介
  6. U盘被写保护的解决办法
  7. OSChina 周六乱弹 —— 知道今天的乱弹为什么会迟发吗?
  8. 如何利用计算机截屏快捷键,电脑怎么截图 电脑选区域截图怎么截 电脑截图快捷键是什么...
  9. C#NPOI获取Excel的列名
  10. C盘清理软件-SpaceSniffer
  11. R数通杀思路分享-反部分混淆解析canvas和fonts指纹
  12. 陈小龙linux及服务器正文 配置rewrite
  13. 数字转换 LibreOJ - 10155
  14. kdj值应用口诀_KDJ指标神奇的操作方法详解
  15. Docker基础讲解狂神笔记(1/2)
  16. 请不要再记笔记了,四个词把人分为四类,最糟糕的一类人,颠覆了我们的认知。
  17. android中dalvik虚拟机参数
  18. 视频编码h264怎么看_你所要知道的音视频--04
  19. Mysql 常用 表操作
  20. Uber正式提交IPO文件:将在纽交所上市 高盛摩根士丹利等担任承销商

热门文章

  1. 【c语言】翻52张扑克牌问题
  2. 修改手游登录服务器,手游[有侠气]一键启动服务端+客户端+GM管理运营后台+VIP修改+启动教程等...
  3. video标签的使用
  4. 段码屏中液晶相与相变的含义?
  5. 如何给自己各种帐号编一个安全又不会忘记的密码?
  6. html中video视频播放
  7. 同时安装pytorch和TensorFlow等多种深度学习开发环境(1)
  8. iostextarea获取焦点_jquery – 在iOS上的Safari中针对textarea触发的不一致事件
  9. svn 插件选择 Subclipse与Subversive比较
  10. 超级账本HyperLedger的cello项目的部署和使用