Android 单元测试第六篇(Hamcrest 匹配器)

在单元测试过程结束后,我们期望编译器可以直接告诉我们是 fail 还是 success。那么如何判断某个 case 是否通过,就尤为重要,如果只是简单的使用 assertEqual、assertFalse、assertNull、assertNotEquals、assertSame,那么很多情况就判断不了,比如判断某个集合是否包含某个元素,某个字符串是否以"Man"开头,这个时候我们就需要搬出匹配器了。

匹配器简介

其实匹配器就是内部采用了特定的算法,来实现特定的业务判断,比如 startsWith("Man")返回的就是的就是一个用来判断某个字符串是否以Man 开头的字符串。日常中还是有很多匹配器是很常见的,所以就有这么一个包含大量常见匹配器的框架,叫做Hamcrest,该框架结合Junit用起来确实很棒,所以从Junit 4.11开始,Junit已经默认依赖了Hamcrest,以Junit4.12为例子,内部依赖的就是Hamcrest-core:1.3

如何在单元测试中使用Hamcrest呢?其实很简单,只要使用以下这个断言即可

public static <T> void assertThat(T actual, Matcher<? super T> matcher);
集成 Hamcrest

上面介绍了自从Junit 4.11开始,就已经自动依赖了,但是为什么本节还要讲集成呢?原因有下面两点

  1. 默认集成的是Hamcrest-core:1.3,常用的匹配器方法被封装在几十个类中,这样我们使用一些静态方法会很麻烦,需要一个一个导包,如图一所示:

    图一.png

  2. core只是包含了最常用的一些匹配器,像数组、字典、数值之类的大部分匹配器是没有的,但这类匹配器我们日常开发中使用到的场景也不少。

所以我们需要集成全部的匹配器,我们在app/build.gradle中添加如下依赖

testImplementation 'junit:junit:4.12'
testImplementation 'org.hamcrest:hamcrest-all:1.3'

然后在使用到的单元测试类或者测试基类中导入所有匹配器,这样我们就不需要想图一一样每用一个需要导入一个,而且你还需要准确的记得每个匹配器的名字,不然是没有智能提示的。

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
Hamcrest的使用

Hamcrest的匹配器用起来确实很简单,所以直接上常用几十匹配器的例子,大多数通过看名字就知道匹配器的作用,一些可能造成误解的我也都写了注释,有时间的伙伴可以敲一遍或者浏览一下,有个印象,等用到的时候可以再查看。

// hamcrest-core
@Test
public void testHamCrest() {

    //JUnit 4.11 and later 自动集成 core
    //基础操作
    //1. 字符串相关
    assertThat("myValue", startsWith("my"));
    assertThat("myValue", containsString("Val"));
    assertThat("myValue", endsWith("e"));
    assertThat("myValue", equalTo("myValue"));
    assertThat("myValue", anything()); //怎样都是通过
    assertThat("myValue", anything("怎样都是通过"));
    //2. 定义相关,比如某位是什么,不是什么,等于什么,不等于什么
    assertThat(1, equalTo(1));
    assertThat("myValue", instanceOf(String.class));
    assertThat(1, not(2));
    assertThat(null, nullValue());
    assertThat("myValue", notNullValue());
    assertThat("myValue", sameInstance("myValue")); //和 theInstance(T)一样

    //3. 集合相关 Iterable
    //3.1 everyItem 每个 item 都要符合条件
    assertThat(Arrays.asList("bar", "baz"), everyItem(startsWith("ba")));
    //3.2 hasItem 至少有一个 item 都符合条件,或者集合中有这个 item,参数可以是 T 也可以是匹配器
    assertThat(Arrays.asList("foo", "bar"), hasItem("bar"));
    assertThat(Arrays.asList("foo", "bar"), hasItem(startsWith("fo")));
    //3.3 hasItems 是hasItem复数版本,支持多 T 类型参数和多匹配器参数
    assertThat(Arrays.asList("foo", "bar", "baz"), hasItems("baz", "foo"));
    assertThat(Arrays.asList("foo", "bar", "baz"), hasItems(endsWith("z"), endsWith("o")));


    //组合匹配器,一般都支持多个参数,虽然下面的提供的是使用两个参数的例子
    //1. allOf 全部条件都需要满足
    assertThat("myValue", allOf(startsWith("my"), containsString("Val")));
    //2. anyOf 满足其中一个条件即可
    assertThat("myValue", anyOf(startsWith("foo"), containsString("Val")));
    //3. both().and() 满足两个条件,为 allOf 的真子集
    assertThat("fab", both(containsString("a")).and(containsString("b")));
    //4. either().or() 满足一个条件,为 anyOf 的真子集
    assertThat("fab", either(containsString("a")).or(containsString("b")));



    //辅助断言,对于机器来说没什么用,只是让语句读起来更加像自然语言
    //1. describedAs 增加断言辅助描述,增强可读性,一旦断言不通过,将直接打印描述内容到控制台。
    //比如下面这个例子,看完之后我们就知道这个断言辅助描述是想告诉我们为什么期待的值是2.
    //等同于 assertThat(2, equalTo(2));
    assertThat(2, describedAs("1 + 1 must equal 2", equalTo(2)));

    //2. is 又是一个语法糖,增加可读性而已
    assertThat("foo", is(equalTo("foo")));
    //2.0 如果里面的匹配器是 equalTo,则可以简写
    assertThat("foo", is("foo"));

    //3. isA 又是一个语法糖,不过参数只能是 Class<T>
    //其实就是assertThat("foo", is(instanceOf(String.class)))的简写
    assertThat("foo", isA(String.class));


    //自定义匹配器, 继承自CustomMatcher,实现 matches 方法即可
    Matcher<String> aNonEmptyString = new CustomMatcher<String>("a non empty string") {
        public boolean matches(Object object) {
            return (object instanceof String) && !((String) object).isEmpty();
        }
    };
    assertThat("foo", aNonEmptyString);

 }

以上是core部分,意思就是使用Junit 4.12自带依赖的Hamcrest即可使用,不过需要手动导包,而且是很多包,下面补充一下其它常用的匹配器,属于core之外的了。

// hamcrest-all
@Test
public void hamcrestAll() throws Exception {
    //array,针对数组每一项进行测试 each matcher[i] is satisfied by array[i],条件成立仅当匹配器个数等于数组元素个数,且每个匹配器都通过
    //数组类型
    assertThat(new Integer[]{1,2,3}, is(array(equalTo(1), equalTo(2), equalTo(3))));
    //包含所有内容,不需要按照顺序,和array不一样
    assertThat(new Integer[]{1,2,3}, arrayContainingInAnyOrder(3, 2, 1));
    assertThat(new String[] {"foo", "bar"}, hasItemInArray(startsWith("ba")));
    assertThat(new Integer[]{1,2,3}, arrayWithSize(3)); //对应 Collection 类型的 hasSize()
    assertThat(new String[0], emptyArray()); //对应 Collection 的 empty()

    //Iterable类型也有上面相应的 API,下面举两个例子
    assertThat(Arrays.asList("foo", "bar"), hasSize(2));
    assertThat(new ArrayList<String>(), is(empty()));

    //map类型
    HashMap<String, String> map = new HashMap<>();
    map.put("bar", "foo");
    map.put("name", "Mango");
    //是否包含特定键值对
    assertThat(map, hasEntry("bar", "foo"));
    assertThat(map, hasEntry(equalTo("bar"), equalTo("foo")));
    assertThat(map, hasKey(equalTo("bar")));
    assertThat(map, hasValue(equalTo("foo")));

    //期望的值是否属于某个集合
    assertThat("foo", isIn(Arrays.asList("bar", "foo")));

    //double 类型
    //误差在正负0.04内算通过
    assertThat(1.03, is(closeTo(1.0, 0.04)));
    assertThat(2, greaterThan(1));
    assertThat(1, greaterThanOrEqualTo(1));
    assertThat(1, lessThan(2));
    assertThat(1, lessThanOrEqualTo(1));

    //text 类型
    assertThat("Foo", equalToIgnoringCase("FOO"));
    assertThat("   my\tfoo  bar ", equalToIgnoringWhiteSpace(" my  foo bar"));
    assertThat("", isEmptyString());
    assertThat(null, isEmptyOrNullString());
}

例子到这里就结束了,最后再附上官方文档

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

推荐阅读更多精彩内容