自动化测试之—Hamcrest匹配器

时间:2021-12-18 17:47:54

自动化测试之—Hamcrest匹配器

本来计划要写一点Espresso方面知识的,但是想到Espresso中好多控件或者断言都是使用Hamcrest匹配器,另一方面则是Hamcrest匹配器相比JUnit的Assert则显得更加优雅、轻便,更容易阅读。

简述

Hamcrest是用于编写匹配器对象的框架,允许以声明方式定义“匹配”规则。有许多情况下匹配器是不可估量的,例如UI验证或数据过滤,但是在编写灵活测试的领域中,匹配器反而是最常用的。

Hamcrest还是相对比较简单的,API也相对比较少,就从一个小例子说起吧。

假如要你要测试一个集合中是否包含三个元素中的一个,如果包含则断言真,否则为假。把集合的初始化放在@Before中,则用JUnit的Assert断言写法如下:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.assertTrue;

/** * Created by star on 2017/8/3. */
@RunWith(JUnit4.class)
public class HamcrestTest {
    private List<String> hamcrestTestList;

    @Before
    public void setUp() {
        hamcrestTestList = new ArrayList<>();
        hamcrestTestList.add("first element");
        hamcrestTestList.add("second element");
        hamcrestTestList.add("third element");
    }

    @Test
    public void assertWithJunitTest() {
        assertTrue(hamcrestTestList.contains("first element")
                || hamcrestTestList.contains("second element")
                || hamcrestTestList.contains("third element"));
    }
}

assertWithJunitTest方法本身并难以理解,但是你第一眼看到它很可能不太明白它是做什么的,而且代码也不简练,而Hamcrest则正是为了简化断言,可以构建测试表达式的匹配器库。Hamcrest的书写方法如下:

import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItem;

...

@Test
public void assertWithHamcrestTest(){
    assertThat(hamcrestTestList, hasItem(anyOf(equalTo("first element"), equalTo("second element"), 
                                            equalTo("third element"))));
}

常用API

hamcrest的匹配器都看起来很好理解,并且还提供了自定义匹配器的接口,可以满足编写代码的需要。主要的API接口有如下:

  • 核心
    • anything -绝对匹配,无论什么情况下都会匹配成功;
    • describedAs -添加自定义失败描述
    • is -是否的意思,仅用来改善断言语句的可读性;
  • 逻辑
    • allOf -检查是否包含所有的匹配器,相当于与(&&);
    • anyOf -检查是否包含匹配器中的一个,相当于(||);
    • not - 检查是否与匹配器相反,相当于非(!);
  • 对象
    • equalTo -检查两个对象是否相等;
    • hasToString - 检查Object.toString;
    • instanceOf,isCompatibleType -检查对象是否是兼容类型;
    • notNullValue,nullValue -检查是否是null值;
    • sameInstance -检查对象是否是相同的类型;
  • Beans
    • hasProperty -检查对象是否有某种属性;
  • 集合
    • array -检查array的元素是否和匹配器描述的相同;
    • hasEntry,hasKey,hasValue -测试给定的Map是否有特定的实体、键或者值;
    • hasItem,hasItems -测试集合是否有一个或者多个元素;
    • hasItemInArray -测试数组中是否有某一元素;
  • 数字
    • closeTo给定的数字是否接近于给定的值;
    • greaterThan,greaterThanOrEqualTo,lessThan,lessThanOrEqualTo -给定的数字是否大于、大于等于、小于、小于等于给定的值;
  • 文本
    • equalToIgnoringCase -检查给定的字符串是否与另一字符串在忽略大小写的情况下相同;
    • equalToIgnoringWhiteSpace -检查给定的字符串是否与另一字符串在忽略空格的情况下相同;
    • containsString -检查给定的字符串是否包含某一字符串;
    • endsWith -检查给定的字符串是否以某一字符串结尾;
    • startsWith -检查给定的字符串是否以某一字符串开头;

这些匹配器除了单独使用外,还可以组合使用,提供更佳精确的匹配。例如

自定义匹配器

hamcrest除了可以使用上述匹配器以外,还可以自己编写合适的匹配器,更好的进行测试。自定义匹配器需要实现Mather接口和一个适当的工厂方法。自定义匹配器一方面可以达到合适的匹配,另一方面也可以简化断言语句。

要想定义自定义匹配器,则需要实现BaseMatcher<T>或者TypeSafeMatcher<T>,需要在build.gradle文件中dependencies闭包中引入对应的包:

dependencies { androidTestImplementation 'org.hamcrest:hamcrest-core:1.3' }

自定义匹配器有以下两种实现方法:

实现BaseMatcher<T>接口

如果实现了BaseMatcher<T>,那么需要重写matches方法,matches方法内部实现我们的逻辑,即要满足什么条件时匹配器将匹配各种条件,并确定是否匹配成功。比如如果要实现自定义匹配器,判断给定的字符串是否以ham开头,并且以java结尾,则自定义匹配器实现方式如下:

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;

/** * Created by star on 2017/8/4. */

public class IsExceptedStringMatcher extends BaseMatcher<String> {
    @Factory
    public static <T> Matcher<String> isExceptedString() {
        return new IsExceptedStringMatcher();
    }

    @Override
    public boolean matches(Object item) {
        String str = (String) item;
        if (str != null && str.length() >= 7 && str.startsWith("ham") && str.endsWith("java")) {
            return true;
        }
        return false;
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("a string that start with \"ham\" and end with \"java\"");
    }
}

之后就可以在断言中直接使用自定义匹配器:

@Test(expected = AssertionError.class)
public void isExceptedStringTest(){
    String str="hamcrestjava";
    assertThat(str,isExceptedString());
    str=null;
    assertThat(str,isExceptedString());
}

实现TypeSafeMatcher<T>接口

如果实现了TypeSafeMatcher <T>,那么需要重写matchesSafely方法,

protected boolean matchesSafely(T item)

matchesSafely方法内部同样实现我们的逻辑,该方法绝不能接受一个null对象,它总会接受一个类型为T的参数,该参数已被检查是否为null且永远不能为null。如果我们要在自定义匹配器内部进行是否为null检查,则要继续实现BaseMatcher<T>接口,如果要自定义一个密码检查的匹配器,要求密码必须包含1个特殊符号(!、”、#、$、%、&、’、(、)、*、+、-、.、/)和1个数字,并且密码长度至少为6,则自定义匹配器实现方式如下:

import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;

/** * Created by star on 2017/8/4. */

public class IsStrongPassword extends TypeSafeMatcher<String> {
    @Factory
    public static <T> Matcher<String> isStrongPassword() {
        return new IsStrongPassword();
    }

    @Override
    protected boolean matchesSafely(String item) {
        if (containsSymbol(item) && containsDigit(item) && item.length() >= 6) {
            return true;
        }
        return false;
    }

    private boolean containsDigit(String password) {
        for (char ch : password.toCharArray()) {
            if (Character.isDigit(ch)) {
                return true;
            }
        }
        return false;
    }

    private boolean containsSymbol(String password) {
        for (char ch : password.toCharArray()) {
            if ((int) ch >= 33 && (int) ch <= 47) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("a string which a strong password");
    }
}

接下来你同样可以在测试代码中使用如下的方式使用该匹配器:

@Test(expected = AssertionError.class)
public void IsStrongPasswordTest() {
    String password;
    password = "!1s2dxs";
    assertThat(password, isStrongPassword());
    password = "123dsda";
    assertThat(password, isStrongPassword());
}

Over…如果有不对的地方欢迎批评指正 :)