Android单元测试(二)

在上一篇文章中我们介绍了Android单元测试入门所需了解的内容,本文接上文继续学习单元测试相关框架。本文介绍了AssertJ、AssertJ-Android、Hamcrest和Robolectric框架的使用,Robolectric生命周期及Robolectric和PowerMock配合使用。

本文首发:http://yuweiguocn.github.io/
新浪微博:@于卫国

《月下独酌》
花间一壶酒,独酌无相亲。
举杯邀明月,对影成三人。
月既不解饮,影徒随我身。
暂伴月将影,行乐须及春。
我歌月徘徊,我舞影零乱。
醒时同交欢,醉后各分散。
永结无情游,相期邈云汉。
-唐,李白

前言

本文要介绍的框架:

  • AssertJ:JAVA 流式断言器,支持一条断言语句对实际值同时断言多个校验点
  • AssertJ-Android:扩展自Assert,旨在让它更容易测试Android
  • Hamcrest:Matchers匹配器
  • Robolectric:用于mock Android框架相关类

AssertJ

仓库地址:https://github.com/joel-costigliola/assertj-core

AseertJ:JAVA 流式断言器,什么是流式,常见的断言器一条断言语句只能对实际值断言一个校验点,而流式断言器,支持一条断言语句对实际值同时断言多个校验点。

添加依赖:

testCompile 'org.assertj:assertj-core:3.8.0'
  
//or for Java 7 projects
testCompile 'org.assertj:assertj-core:2.8.0'

添加静态导入:

import static org.assertj.core.api.Assertions.*;
  
//或者如果你喜欢这样的话:
import static org.assertj.core.api.Assertions.assertThat;  // main one
import static org.assertj.core.api.Assertions.atIndex; // for List assertions
import static org.assertj.core.api.Assertions.entry;  // for Map assertions
import static org.assertj.core.api.Assertions.tuple; // when extracting several properties at once
import static org.assertj.core.api.Assertions.fail; // use when writing exception tests
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; // idem
import static org.assertj.core.api.Assertions.filter; // for Iterable/Array assertions
import static org.assertj.core.api.Assertions.offset; // for floating number assertions
import static org.assertj.core.api.Assertions.anyOf; // use with Condition
import static org.assertj.core.api.Assertions.contentOf; // use with File assertions
  
//对于android使用这个静态导入
import static org.assertj.core.api.Java6Assertions.*;
// 静态导入所有assertThat和实用方法
import static org.assertj.core.api.Assertions.*;
 
// 基础断言
assertThat(frodo.getName()).isEqualTo("Frodo");
assertThat(frodo).isNotEqualTo(sauron);
 
// 指定字符串链式断言
assertThat(frodo.getName()).startsWith("Fro")
                           .endsWith("do")
                           .isEqualToIgnoringCase("frodo");
 
// 指定集合断言
// in the examples below fellowshipOfTheRing is a List<TolkienCharacter>
assertThat(fellowshipOfTheRing).hasSize(9)
                               .contains(frodo, sam)
                               .doesNotContain(sauron);
 
// as() 用于指定错误信息
assertThat(frodo.getAge()).as("check %s's age", frodo.getName()).isEqualTo(33);
 
// Java 8 异常断言
assertThatThrownBy(() -> { throw new Exception("boom!"); }).hasMessage("boom!");
// ... or BDD style
Throwable thrown = catchThrowable(() -> { throw new Exception("boom!"); });
assertThat(thrown).hasMessageContaining("boom");
 
// using the 'extracting' feature to check fellowshipOfTheRing character's names (Java 7)
// 使用提取特性检查字符串的名称
assertThat(fellowshipOfTheRing).extracting("name")
                               .contains("Boromir", "Gandalf", "Frodo", "Legolas")
// same thing using a Java 8 method reference
//和java8的方法引用一样
assertThat(fellowshipOfTheRing).extracting(TolkienCharacter::getName)
                               .doesNotContain("Sauron", "Elrond");
 
// 抽取多个值到一个组 in tuples (Java 7)
assertThat(fellowshipOfTheRing).extracting("name", "age", "race.name")
                               .contains(tuple("Boromir", 37, "Man"),
                                         tuple("Sam", 38, "Hobbit"),
                                         tuple("Legolas", 1000, "Elf"));
 
// 断言之前过滤一个集合 in Java 7 ...
assertThat(fellowshipOfTheRing).filteredOn("race", HOBBIT)
                               .containsOnly(sam, frodo, pippin, merry);
// ... or in Java 8
assertThat(fellowshipOfTheRing).filteredOn(character -> character.getName().contains("o"))
                               .containsOnly(aragorn, frodo, legolas, boromir);
 
// 结合过滤和抽取功能
assertThat(fellowshipOfTheRing).filteredOn(character -> character.getName().contains("o"))
                               .containsOnly(aragorn, frodo, legolas, boromir)
                               .extracting(character -> character.getRace().getName())
                               .contains("Hobbit", "Elf", "Man");
 
// and many more assertions : iterable, stream, array, map, dates (java 7 and java 8), path, file, numbers, predicate, optional ...
 

对要断言的对象输入一个点会显示所有可用的断言。

AssertJ-Android

仓库地址:https://github.com/square/assertj-android

扩展自Assert,旨在让它更容易测试Android。

Assertj-Android和Junit和AssertJ之间的对比:

//ASSERTJ ANDROID
assertThat(view).isGone();
  
//REGULAR JUNIT
assertEquals(View.GONE, view.getVisibility());
  
//REGULAR ASSERTJ
assertThat(view.getVisibility()).isEqualTo(View.GONE);
  
//当断言失败,你可以直接看到失败的原因
//Expected visibility <gone> but was <invisible>.
  
  
  
//ASSERTJ ANDROID
assertThat(layout).isVisible()
    .isVertical()
    .hasChildCount(4)
    .hasShowDividers(SHOW_DIVIDERS_MIDDLE);
  
//REGULAR JUNIT
assertEquals(View.VISIBLE, layout.getVisibility());
assertEquals(VERTICAL, layout.getOrientation());
assertEquals(4, layout.getChildCount());
assertEquals(SHOW_DIVIDERS_MIDDLE, layout.getShowDividers());
  
//REGULAR ASSERTJ
assertThat(layout.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(layout.getOrientation()).isEqualTo(VERTICAL);
assertThat(layout.getChildCount()).isEqualTo(4);
assertThat(layout.getShowDividers()).isEqualTo(SHOW_DIVIDERS_MIDDLE);

断言包含几乎所有你想要测试的对象,从LinearLayout到ActionBar、Fragment及MenuItem。以及support类库所有东西。

添加依赖:

//Android module:
androidTestCompile 'com.squareup.assertj:assertj-android:1.1.1'
 
//support-v4 module:
androidTestCompile 'com.squareup.assertj:assertj-android-support-v4:1.1.1'
 
//Google Play Services module:
androidTestCompile 'com.squareup.assertj:assertj-android-play-services:1.1.1'
 
//appcompat-v7 module:
androidTestCompile 'com.squareup.assertj:assertj-android-appcompat-v7:1.1.1'
 
//mediarouter-v7 module:
androidTestCompile 'com.squareup.assertj:assertj-android-mediarouter-v7:1.1.1'
 
//gridlayout-v7 module:
androidTestCompile 'com.squareup.assertj:assertj-android-gridlayout-v7:1.1.1'
 
//cardview-v7 module:
androidTestCompile 'com.squareup.assertj:assertj-android-cardview-v7:1.1.1'
 
//recyclerview-v7 module:
androidTestCompile 'com.squareup.assertj:assertj-android-recyclerview-v7:1.1.1'
 
//pallete-v7 module:
androidTestCompile 'com.squareup.assertj:assertj-android-pallete-v7:1.1.1'

静态导入:

//android
import static org.assertj.android.api.Assertions.assertThat;
  
//support-v4
import static org.assertj.android.support.v4.api.Assertions.assertThat;
 
//Google Play Services
import static org.assertj.android.playservices.api.Assertions.assertThat;
 
//appcompat-v7
import static org.assertj.android.appcompat.v7.api.Assertions.assertThat;
 
//mediarouter-v7
import static org.assertj.android.mediarouter.v7.api.Assertions.assertThat;
 
//gridlayout-v7
import static org.assertj.android.gridlayout.v7.api.Assertions.assertThat;
 
//cardview-v7
import static org.assertj.android.cardview.v7.api.Assertions.assertThat;
 
//recyclerview-v7
import static org.assertj.android.recyclerview.v4.api.Assertions.assertThat;
 
//pallete-v7
import static org.assertj.android.pallete.v4.api.Assertions.assertThat;

Hamcrest

仓库地址:https://github.com/hamcrest/JavaHamcrest

//添加依赖
testCompile "org.hamcrest:hamcrest-all:1.3"
  
  
//静态导入
import static org.hamcrest.MatcherAssert.assertThat; 
import static org.hamcrest.Matchers.*; 

Hamcrest 是一个测试的框架,它提供了一套通用的匹配符 Matcher,灵活使用这些匹配符定义的规则,程序员可以更加精确的表达自己的测试思想,指定所想设定的测试条件。比如,有时候定义的测试数据范围太精 确,往往是若干个固定的确定值,这时会导致测试非常脆弱,因为接下来的测试数据只要稍稍有变化,就可能导致测试失败(比如 assertEquals( x, 10 ); 只能判断 x 是否等于 10,如果 x 不等于 10, 测试失败);有时候指定的测试数据范围又不够太精确,这时有可能会造成某些本该会导致测试不通过的数据,仍然会通过接下来的测试,这样就会降低测试的价值。 Hamcrest 的出现,给程序员编写测试用例提供了一套规则和方法,使用其可以更加精确的表达程序员所期望的测试的行为。

Hamcrest 常用的匹配器:

核心

  • anything - 总是匹配,如果你不关心测试下的对象是什么是有用的
  • describedAs - 添加一个定制的失败表述装饰器
  • is - 改进可读性装饰器 - 见下 “Sugar”

逻辑

  • allOf - 如果所有匹配器都匹配才匹配, short circuits (很难懂的一个词,意译是短路,感觉不对,就没有翻译)(像 Java &&)
  • anyOf - 如果任何匹配器匹配就匹配, short circuits (像 Java ||)
  • not - 如果包装的匹配器不匹配器时匹配,反之亦然

对象

  • equalTo - 测试对象相等使用Object.equals方法
  • hasToString - 测试Object.toString方法
  • instanceOf, isCompatibleType - 测试类型
  • notNullValue, nullValue - 测试null
  • sameInstance - 测试对象实例

Beans

  • hasProperty - 测试JavaBeans属性

集合

  • array - 测试一个数组元素test an array’s elements against an array of matchers
  • hasEntry, hasKey, hasValue - 测试一个Map包含一个实体,键或者值
  • hasItem, hasItems - 测试一个集合包含一个元素
  • hasItemInArray - 测试一个数组包含一个元素

数字

  • closeTo - 测试浮点值接近给定的值
  • greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo - 测试次序

文本

  • equalToIgnoringCase - 测试字符串相等忽略大小写
  • equalToIgnoringWhiteSpace - 测试字符串忽略空白
  • containsString, endsWith, startsWith - 测试字符串匹配
// 比较50是否和50相等 
assertThat(50, equalTo(50)); 
// 50是否大于30并且小于60 
assertThat("错误",50, allOf(greaterThan(30), lessThan(60))); 
// 判断字符串是否以.txt结尾 
assertThat("错误", "abc.txt", endsWith(".txt")); 

Robolectric

仓库地址:https://github.com/robolectric/robolectric

添加依赖:

testCompile "org.robolectric:robolectric:3.3.2"

android的开发和编译环境是JVM,需要的依赖SDK中的android.jar包。而android.jar包底层的方法都是stub的,没有具体的实现。所以我们的项目编译打包之后运行在device上没问题,是因为device上有运行所需要的delvik环境,但是test运行的环境是JVM这时候如果用到了android.jar原生的方法就会导致异常java.lang.RuntimeException: Stub!
这时候就需要用到robolectric这个框架,这个框架通过对原生的类的替换,在调用到这些方法的时候拦截掉原有的调用,并用自己的实现替换被调用的方法。

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class MyActivityTest {
 
  @Test
  public void clickingButton_shouldChangeResultsViewText() throws Exception {
    Activity activity = Robolectric.setupActivity(MyActivity.class);
 
    Button button = (Button) activity.findViewById(R.id.press_me_button);
    TextView results = (TextView) activity.findViewById(R.id.results_text_view);
 
    button.performClick();
    assertThat(results.getText().toString(), equalTo("Testing Android Rocks!"));
  }
}

如果你使用的是Mac,你可能需要配置默认的Junit test runner,编辑Junit配置,修改working directory为$MODULE_DIR$,每个用到robolectric的junit都需要此配置。

Robolectric生命周期

首先,robolectric默认会从AndroidManifest.xml找到指定的application用于加载:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.myapp">
  <application android:name=".App"/>
</manifest>

这会加载com.myapp.App类,你可以在test目录下创建用于测试的AndroidManifest.xml,并创建用于测试的Application,你可以实现TestLifecycleApplication接口,监听重要事件的回调。

public class TestApp extends Application implements TestLifecycleApplication{
 
    @Override
    public void beforeTest(Method method) {
 
    }
 
    @Override
    public void prepareTest(Object test) {
 
    }
 
    @Override
    public void afterTest(Method method) {
 
    }
}
  
// src/test/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.myapp">
  <application android:name=".TestApp"/>
</manifest>
  
  
//在注解上使用manifest指定测试清单文件
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class,manifest = "src/test/AndroidManifest.xml")

当运行每个test时,其生命周期为:
1.创建application。
2.调用application.onCreate()。
3.调用application.beforeTest()。
4.调用application.prepareTest()。
5.运行测试用例。
6.调用application.onTerminate()。
7.调用application.afterTest()。

如果你打算在现有项目引入单元测试,建议添加用于测试的Application,这会你让少走很多弯路。

Robolectric和PowerMock配合使用

//添加依赖
testCompile "org.robolectric:robolectric:3.3"
  
testCompile "org.powermock:powermock-module-junit4:1.6.4"
testCompile "org.powermock:powermock-module-junit4-rule:1.6.4"
testCompile "org.powermock:powermock-api-mockito:1.6.4"
testCompile "org.powermock:powermock-classloading-xstream:1.6.4"
  
  
  
  
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" })
@PrepareForTest(Static.class)
public class DeckardActivityTest {
 
    @Rule
    public PowerMockRule rule = new PowerMockRule();
 
    @Test
    public void testStaticMocking() {
        PowerMockito.mockStatic(Static.class);
        Mockito.when(Static.staticMethod()).thenReturn("hello mock");
 
        assertTrue(Static.staticMethod().equals("hello mock"));
    }
}

PowerMockRule是用于代替@RunWith注解开启PowerMock,因为我们已经使用RobolectricTestRunner指定了注解值。
@PowerMockIgnore用于忽略Mockito和Robolectric类库,因为我们不应该mock它们自己。还有android的类,因为我们已经使用Robolectric处理了。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容