Android单元测试(一)-基础

一、什么是单元测试

单元测试是测试某个类的某个方法能否正常工作的一种手段。

二、单元测试目的

  • 验收(改动和重构)
  • 快速验证逻辑
  • 优化代码设计

三、Android单元测试工具链

google推荐:junit4+mockito+rebolectric
最佳实战选择:junit4+mockito+powermock+rebolectric

工具介绍:

  • Junit4 Java单元测试框架。
  • Mockito 针对Java的mocking框架,解决测试类对其他类的依赖问题。
  • Powermock mockito功能的延伸,且完美兼容mockito。
  • Rebolectric 模拟android接口,让单测跑在jvm上,去掉对android环境的依赖。

注:
mockito功能有点弱,很多功能受限。powermock可以跟mockito兼容而且功能更强。与mockito比,支持修改和mock静态类或对象的私有方法/成员,还支持很多反射方法,非常实用。

四、如何在Android项目中运行单元测试

案例:

public class SimpleClass {
   public int add(int a, int b) {
       return a + b;
   }
}

public class SimpleClassTest {
   @Test
   public void testAdd() {
       SimpleClass simpleClass = new SimpleClass();
       int sum = simpleClass.add(1, 2);
       Assert.assertEquals(3, sum);
   }
}

新建单元测试:

运行:

运行结果

(验证通过):

(验证失败):

查看覆盖率:

五、单测基本规范与验收标准

代码规范:

  • 测试类命名:被测类名+Test;
  • 测试类被测方法命名:test+首字母大写的被测方法名,如果一个被测方法有多个测试用例,则在其后添加“_+序号”;
  • 测试类必现有setup()、cleanup()方法,用于初始化和释放资源;
  • 测试用例必现有断言。

验收标准:

  • 代码覆盖率:行覆盖,函数覆盖,分支覆盖。目前google、facebook等国外一线大厂单测覆盖率要求是82%左右。

六、单测框架及其使用

6.1 JUnit4

JUnit4 :控制执行顺序 + 验证结果

控制执行顺序:

注解名 含义
@Test 表示此方法为测试方法
@before 每个测试方法前执行,可做初始化操作
@After 在每个测试方法后执行,可做释放资源操作
@Ignore 忽略的测试方法
@BeforeClass 在类中所有方法前运行。此注解修饰的方法必须是static void
@AfterClass 在类中最后运行。此注解修饰的方法必须是static void
@RunWith 指定该测试类使用某个运行器
@Parameters 指定测试类的测试数据集合
@Rule 重新制定测试类中方法的行为
@FixMethodOrder 指定测试类中方法的执行顺序

验证结果:

1)Assert

方法名 方法描述
assertNotEquals 断言传入的预期值与实际值是不相等的
assertNotEquals 断言传入的预期值与实际值是不相等的
assertArrayEquals 断言传入的预期数组与实际数组是相等的
assertNull 断言传入的对象是为空
assertNotNull 断言传入的对象是不为空
assertTrue 断言条件为真
assertFalse 断言条件为假
assertSame 断言两个对象引用同一个对象,相当于“==”
assertNotSame 断言两个对象引用不同的对象,相当于“!=”
assertThat 断言实际值是否满足指定的条件

2)Assert.assertThat 作用跟Assert类似。

匹配器 说明 例子
is 断言参数等于后面给出的匹配表达式
not 断言参数不等于后面给出的匹配表达式
equalTo 断言参数相等
equalToIgnoringCase 断言字符串相等忽略大小写
containsString 断言字符串包含某字符串
startsWith 断言字符串以某字符串开始
endsWith 断言字符串以某字符串结束
nullValue 断言参数的值为null
notNullValue 断言参数的值不为null
greaterThan 断言参数大于
lessThan 断言参数小于
greaterThanOrEqualTo 断言参数大于等于
lessThanOrEqualTo 断言参数小于等于
closeTo 断言浮点型数在某一范围内
allOf 断言符合所有条件,相当于&&
anyOf 断言符合某一条件,相当于或
hasKey 断言Map集合含有此键
hasValue 断言Map集合含有此值
hasItem 断言迭代对象含有此元素

举例:

public class Junit4Example {
   public int plus(int a, int b) {
       return a + b;
   }
}

public class Junit4ExampleTest {
   @BeforeClass
   public static void beforeClass() throws Exception {
   }

   @Before
   public void setUp() throws Exception {
   }

    //两种验证写法:
    //Assert
   @Test
   public void testPlus_1() {
       Junit4Example junit4Example = new Junit4Example();
       Assert.assertEquals(3, junit4Example.plus(1, 2));
   }

    //assertThat
   @Test
   public void testPlus_2() {
       Junit4Example junit4Example = new Junit4Example();
       Assert.assertThat(3, equalTo(junit4Example.plus(1, 2)));
   }

   @After
   public void tearDown() throws Exception {
   }

   @AfterClass
   public static void afterClass() throws Exception {
   }
}

生命周期:

com.stan.androidtest.Junit4ExampleTest beforeClass
com.stan.androidtest.Junit4ExampleTest setUp
com.stan.androidtest.Junit4ExampleTest testPlus_1
com.stan.androidtest.Junit4ExampleTest tearDown
com.stan.androidtest.Junit4ExampleTest setUp
com.stan.androidtest.Junit4ExampleTest testPlus_2
com.stan.androidtest.Junit4ExampleTest tearDown
com.stan.androidtest.Junit4ExampleTest afterClass
6.2 Mockito

Mock的作用:解决测试类对其他类的依赖问题。Mock的类所有方法都是空,所有变量都是初始值。
Mockito 使用操作主要分如下三步:
mock/spy对象 + 打桩 + 验证行为

mock/spy对象
区别:
mock: 所有方法都是空方法,非void方法都将返回默认值,比如int方法返回0,对象方法将返回null,而void方法将什么都不做。
spy:跟正常类对象一样,是正常对象的替身。

适用场景
mock:类对外部依赖较多,只关新少数函数的具体实现;
spy:跟mock相反,类对外依赖较少,关心大部分函数的具体实现。

打桩

方法名 方法描述
thenReturn(T value) 设置要返回的值
thenThrow(Throwable… throwables) 设置要抛出的异常
thenAnswer(Answer<?> answer) 对结果进行拦截
doReturn(Object toBeReturned) 提前设置要返回的值
doThrow(Throwable… toBeThrown) 提前设置要抛出的异常
doAnswer(Answer answer) 提前对结果进行拦截
doCallRealMethod() 调用某一个方法的真实实现
doNothing() 设置void方法什么也不做

验证行为

方法名 方法描述
after(long millis) 在给定的时间后进行验证
timeout(long millis) 验证方法执行是否超时
atLeast(int minNumberOfInvocations) 至少进行n次验证
atMost(int maxNumberOfInvocations) 至多进行n次验证
description(String description) 验证失败时输出的内容
times(int wantedNumberOfInvocations) 验证调用方法的次数
never() 验证交互没有发生,相当于times(0)
only() 验证方法只被调用一次,相当于times(1)

参数匹配

方法名 方法描述
anyObject() 匹配任何对象
any(Class<T> type) 与anyObject()一样
any() 与anyObject()一样
anyBoolean() 匹配任何boolean和非空Boolean
anyByte() 匹配任何byte和非空Byte
anyCollection() 匹配任何非空Collection
anyDouble() 匹配任何double和非空Double
anyFloat() 匹配任何float和非空Float
anyInt() 匹配任何int和非空Integer
anyList() 匹配任何非空List
anyLong() 匹配任何long和非空Long
anyMap() 匹配任何非空Map
anyString() 匹配任何非空String
contains(String substring) 参数包含给定的substring字符串
argThat(ArgumentMatcher <T> matcher) 创建自定义的参数匹配模式

其他方法

方法名 方法描述
reset(T … mocks) 重置Mock
spy(Class<T> classToSpy) 实现调用真实对象的实现
inOrder(Object… mocks) 验证执行顺序
@InjectMocks注解 自动将模拟对象注入到被测试对象中

Mockito局限性:不支持private方法、匿名类、final类、static方法。

举例:

Mockito 常规操作:mock/spy对象 + 打桩 + 验证行为

mock对象 + 打桩配合:

mock + doCallRealMethod()

//mock当前类的空实现

MockitoExample example = Mockito.mock(MockitoExample.class);

//让类的空方法恢复成真实实现

Mockito.doCallRealMethod().when(example).plus(Mockito.anyInt(), Mockito.anyInt());

spy + doNothing()/doReturn()

//spy当前类的真实实现

MockitoExample example = Mockito.spy(MockitoExample.class);

//让某些方法不执行真实逻辑

//有返回值的

Mockito.doReturn(0).when(example).function(Mockito.anyInt());

//没返回值的

Mockito.doNothing().when(example).function(Mockito.anyInt());

验证行为:

//验证方法被调用

Mockito.verify(example, Mockito.times(1)).function(1);

//验证方法执行结果

Assert.assertEquals(3, example.function(1, 2));

特殊验证:

//一个参数确定,另一个是匹配器

Mockito.verify(example, Mockito.times(1)).function(Mockito.eq(1),Mockito.anyInt());

这里确定参数不能直接写,需要eq包一下。

//匹配非基础类型

Mockito.verify(example, Mockito.times(1)).function(Mockito.any(类名.class));
6.3 PowerMockito

PowerMock是Mockito的扩展增强版,支持mock private、static、final方法和类,还增加了很多反射方法可以方便修改静态和非静态成员等。功能比Mockito增加很多。

@RunWith(PowerMockRunner.class)//使用PowerMockRunner时是兼容Mockito的,Mockito中的各种方法也是可以正常使用到
@PrepareForTest()//要mock的类需要写在这个注解里
public class PowerMockitoExampleTest {
    @Test
   public void testGetPrivateField() {
   ...
   }
}

针对对象操作

功能 实现
读取对象私有成员 WhiteBox.getInternalState
修改对象私有成员 WhiteBox.setInternalState 或 MemberModifier.field
verify对象私有方法 PowerMockito.verifyPrivate
Invoke对象私有方法 Whitebox.invokeMethod
修改对象私有函数 PowerMockito.replace
调用私有构造方法 Whitebox.invokeConstructor
跳过方法执行 PowerMockito.suppress

针对静态类

功能 实现
读取对象私有成员 WhiteBox.getInternalState
修改对象私有成员 WhiteBox.setInternalState
调用静态私有方法 Whitebox.invokeMethod
替换静态函数 PowerMockito.replace
verify公有静态方法 PowerMockito.verifyStatic
verify私有静态方法 PowerMockito.verifyPrivate

针对final类

mock final类举例:

public final class FinalClassA {
   public final String finalMethod() {
       return "finalMethod";
   }
}

@Test
public final void mockPowerFinalClassTest() {
       FinalClassA instance = new FinalClassA();
       FinalClassA mock = PowerMockito.mock(FinalClassA.class);
       Mockito.when(mock.finalMethod()).thenReturn("mock method");
       Assert.assertNotEquals(instance.finalMethod(), mock.finalMethod());
}

当然PowerMockito也不是万能的,它目前解决不了匿名类的场景。

七、测试框架-Robolectric

单测运行在真机或模拟器上,跑起来非常耗时,而且依赖于Android环境不是很方便。如果直接运行在JVM就方便很多,但是直接运行在JVM,代码依赖的Android SDK的api 这些接口在android.jar中,获取不到就会抛RuntimeException(“stub!!”)。

Robolectric作为一个测试框架,通过实现一套JVM能运行的Android代码,然后在unit test运行的时候去截取android相关的代码调用,然后转到他们的他们实现的代码去执行这个调用的过程。解决了上面的问题。

@RunWith(RobolectricTestRunner.class)  //Robolectric需要使用RobolectricTestRunner这个runner
@Config(constants = BuildConfig.class, application = ApplicationStub.class, sdk = 21)
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" }) //PowerMock有自己的ClassLoader,叫做MockClassLoader,Robolectric也有自己的ClassLoader,SandBoxClassLoader,如果一个类被两个加载了,使用时会报错。这里对一些类不要PowerMock,避免冲突
@PrepareForTest(StaticClass.class)
public class MyTest {
   //这个rule一定要有,解决powermock和robolectric兼容性
   @Rule
   public PowerMockRule rule = new PowerMockRule();
...
}
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容