单元测试——Hamcrest匹配器框架

一、Hamcrest是什么?

Hamcrest is a library of matchers, which can be combined in to create flexible expressions of intent in tests.

Hamcrest 是一个为了测试为目的,且能组合成灵活表达式的匹配器类库。

二、为什么要用Hamcrest匹配器框架

Hamcrest的目标是使测试尽可能的提高可读性.例如is()方法其实就是equalTo()的包装方法.

三、常用方法介绍

    @Test
    public void testHamcrestMatchers() {
        // 核心匹配
        // allOf: 所有条件都必须满足,相当于&&
        assertThat("myname", allOf(startsWith("my"), containsString("name")));
        // anyOf: 其中一个满足就通过, 相当于||
        assertThat("myname", anyOf(startsWith("na"), containsString("name")));
        // both: &&
        assertThat("myname", both(containsString("my")).and(containsString("me")));
        // either: 两者之一
        assertThat("myname", either(containsString("my")).or(containsString("you")));
        // everyItem: 每个元素都需满足特定条件
        assertThat(Arrays.asList("my", "mine"), everyItem(startsWith("m")));
        // hasItem: 是否有这个元素
        assertThat(Arrays.asList("my", "mine"), hasItem("my"));
        // hasItems: 包含多个元素
        assertThat(Arrays.asList("my", "mine", "your"), hasItems("your", "my"));
        // is: is(equalTo(x))或is(instanceOf(clazz.class))的简写
        assertThat("myname", is("myname"));
        assertThat("mynmae", is(String.class));
        // anything(): 任何情况下,都匹配正确
        assertThat("myname", anything());
        // not: 否为真,相当于!
        assertThat("myname", is(not("you")));
        // nullValue(): 值为空
        String str = null;
        assertThat(str, is(nullValue()));
        // notNullValue(): 值不为空
        String str2 = "123";
        assertThat(str2, is(notNullValue()));
 
        
        // 字符串匹配
        // containsString:包含字符串
        assertThat("myname", containsString("na"));
        // stringContainsInOrder: 顺序包含,“my”必须在“me”前面
        assertThat("myname", stringContainsInOrder(Arrays.asList("my", "me")));
        // endsWith: 后缀
        assertThat("myname", endsWith("me"));
        // startsWith: 前缀
        assertThat("myname", startsWith("my"));
        // isEmptyString(): 空字符串
        assertThat("", isEmptyString());
        // equalTo: 值相等, Object.equals(Object)
        assertThat("myname", equalTo("myname"));
        assertThat(new String[] {"a", "b"}, equalTo(new String[] {"a", "b"}));
        // equalToIgnoringCase: 比较时,忽略大小写
        assertThat("myname", equalToIgnoringCase("MYNAME"));
        // equalToIgnoringWhiteSpace: 比较时, 首尾空格忽略, 比较时中间用单个空格
        assertThat(" my \t name ", equalToIgnoringWhiteSpace(" my name "));
        // isOneOf: 是否为其中之一
        assertThat("myname", isOneOf("myname", "yourname"));
        // isIn: 是否为其成员
        assertThat("myname", isIn(new String[]{"myname", "yourname"}));
        // toString() 返回值校验
        assertThat(333, hasToString(equalTo("333")));
       
        
        // 数值匹配
        // closeTo: [operand-error, operand+error], Double或BigDecimal类型
        assertThat(3.14, closeTo(3, 0.5));
        assertThat(new BigDecimal("3.14"), is(closeTo(new BigDecimal("3"), new BigDecimal("0.5"))));
        // comparesEqualTo: compareTo比较值
        assertThat(2, comparesEqualTo(2));
        // greaterThan: 大于
        assertThat(2, greaterThan(0));
        // greaterThanOrEqualTo: 大于等于
        assertThat(2, greaterThanOrEqualTo(2));
        // lessThan: 小于
        assertThat(0, lessThan(2));
        // lessThanOrEqualTo: 小于等于
        assertThat(0, lessThanOrEqualTo(0));
        
        
        
        // 集合匹配
        // array: 数组长度相等且对应元素也相等
        assertThat(new Integer[]{1, 2, 3}, is(array(equalTo(1), equalTo(2), equalTo(3))));
        // hasItemInArray: 数组是否包含特定元素
        assertThat(new String[]{"my", "you"}, hasItemInArray(startsWith("y")));
        // arrayContainingInAnyOrder, 顺序无关,长度要一致
        assertThat(new String[]{"my", "you"}, arrayContainingInAnyOrder("you", "my"));
        // arrayContaining:  顺序,长度一致
        assertThat(new String[]{"my", "you"}, arrayContaining("my", "you"));
        // arrayWithSize: 数组长度
        assertThat(new String[]{"my", "you"}, arrayWithSize(2));
        // emptyArray: 空数组
        assertThat(new String[0], emptyArray());
        // hasSize: 集合大小
        assertThat(Arrays.asList("my", "you"), hasSize(equalTo(2)));
        // empty: 空集合
        assertThat(new ArrayList<String>(), is(empty()));
        // isIn: 是否为集合成员
        assertThat("myname", isIn(Arrays.asList("myname", "yourname")));
       // Map匹配
        Map<String, String> myMap = new HashMap<String, String>();
        myMap.put("name", "john");
        // hasEntry: key && value匹配
        assertThat(myMap, hasEntry("name", "john"));
        // hasKey: key匹配
        assertThat(myMap, hasKey(equalTo("name")));
        // hasValue: value匹配
        assertThat(myMap, hasValue(equalTo("john")));
    }

详细请看: Hamcrest API

四、自定义Hamcrest匹配器

1.通过FeatureMatcher自定义Hamcrest匹配器

创建Hamcrest匹配器

我们自定义一个为String提供长度的匹配器,需要利用FeatureMatcher类,封装一个现有的匹配器,用来决定给定的被测对象的哪个字段匹配,并且提供丰富的错误信息.FeatureMatcher的构造函数有下列参数:

  • 我们想要包装的匹配器
  • 对我们测试的功能的描述(在错误信息会有体现)
  • 测试功能的名字(在错误信息会有体现)

我们必须重写featureValueOf(T actual),它的返回值将传入matchesSafely()方法进行匹配判断.

public static Matcher<String> length(Matcher<? super Integer> matcher) {
    return new FeatureMatcher<String, Integer>(matcher,
            "a String of length that", "length") {
        @Override
        protected Integer featureValueOf(String actual) {
            return actual.length();
        }
    };
}
测试

使用你刚才创建的自定义匹配器验证"Gandalf"的长度为8

@Test
public void fellowShipOfTheRingShouldContainer7() {
    assertThat("Gandalf", length(is(8)));
}

使用TypeSafeMatcher自定义匹配器

我们可以对TypeSafeMatcher进行扩展.与BaseMatcher相比TypeSafeMatcher可以自动的检查null值, 在被委派到matchesSafely()方法之前检查类型并进行适当的转换.下面定义了一个检查一个字符串是否匹配正则关系的匹配器.

public class RegexMatcher extends TypeSafeMatcher<String> {
    private final String regex;
    public RegexMatcher(final String regex) {
        this.regex = regex;
    }
    @Override
    public void describeTo(final Description description) {
        description.appendText("matches regular expression=`" + regex + "`");
    }
    @Override
    public boolean matchesSafely(final String string) {
        return string.matches(regex);
    }
    // matcher method you can call on this matcher class
    public static RegexMatcher matchesRegex(final String regex) {
        return new RegexMatcher(regex);
    }
}
测试
@Test
public void testRegularExpressionMatcher() throws Exception {
    String s ="aaabbbaaaa";
    assertThat(s, RegexMatcher.matchesRegex("a*b*a*"));
}

自定义组合匹配器

为什么要自定义组合匹配器

Hamcrest有内置的组合匹配器,但是它的可读性太差!

下面就是一个案例:

@Test
public void testCombining() {
    List<Integer> list = new ArrayList<>();
    assertThat(list, both(hasSize(1)).and(contains(42)));
}

可读性差,无法准确描述错误信息.

创建自定义组合匹配器

我们可以继承BaseMatchers类使用它提供对外连接的方法(matches),本身再提供一个添加方法(add).将匹配器链接起来.并保存在集合中.

public class MatcherCombinator<T> extends BaseMatcher<T> {
    private final List<Matcher<? super T>> matchers = new ArrayList<>();
    private final List<Matcher<? super T>> failedMatchers = new ArrayList<>();

    private MatcherCombinator(final Matcher<? super T> matcher) {
        matchers.add(matcher);
    }

    public MatcherCombinator<T> and(final Matcher<? super T> matcher) {
        matchers.add(matcher);
        return this;
    }

    @Override
    public boolean matches(final Object item) {
        boolean matchesAllMatchers = true;
        for (final Matcher<? super T> matcher : matchers) {
            if (!matcher.matches(item)) {
                failedMatchers.add(matcher);
                matchesAllMatchers = false;
            }
        }
        return matchesAllMatchers;
    }

    @Override
    public void describeTo(final Description description) {
        description.appendValueList("\n", " " + "and" + "\n", "", matchers);
    }

    @Override
    public void describeMismatch(final Object item, final Description description) {
        description.appendText("\n");
        for (Iterator<Matcher<? super T>> iterator = failedMatchers.iterator(); iterator.hasNext();) {
            final Matcher<? super T> matcher = iterator.next();
            description.appendText("Expected: <");
            description.appendDescriptionOf(matcher).appendText(" but ");
            matcher.describeMismatch(item, description);
            if (iterator.hasNext()) {
                description.appendText(">\n");
            }
        }
    }

    public static <LHS> MatcherCombinator<LHS> matches(final Matcher<? super LHS> matcher) {
        return new MatcherCombinator<LHS>(matcher);
    }
}
测试
@Test
public void testCustomCombining() {
    List<Integer> list = new ArrayList<>();
    assertThat(list, MatcherCombinator.matches(hasSize(1)).and(contains(42)));
}

引用:
引用1
引用2
引用3

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