我们被迫在测试代码中写太多断言行的日子已经一去不复返了。 镇上有一个新的警长:assertThat和他的代理人:匹配者。 好吧,这不是什么新东西,但是无论如何,我想向您介绍匹配器的使用方式,然后对匹配器概念进行扩展,我发现这对于为代码开发单元测试非常有用。
首先,我将介绍匹配器的基本用法。 当然,您可以直接从其作者那里完整地了解hamcrest匹配器功能:
https://code.google.com/p/hamcrest/wiki/Tutorial 。
基本上,匹配器是定义两个对象何时匹配的对象。 通常,第一个问题是您为什么不使用等于? 好吧,有时您不想在它们的所有字段上都匹配两个对象,而只是在其中的某些字段上匹配,如果您使用旧代码,则会发现equals实现不存在或不符合您的预期。 另一个原因是使用assertThat为您提供了一种更一致的“断言”方法,并且可以说是更具可读性的代码。 因此,例如,而不是编写:
int expected, actual;
assertEquals(expected, actual);
你会写
assertThat(expected, is(actual));
其中“ is”是静态导入的org.hamcrest.core.Is.is
并没有太大的区别……。 但是Hamcrest为您提供了许多非常有用的匹配器:
- 对于数组和映射:hasItem,hasKey,hasValue
- 数字:closeTo –一种指定相等性的方法,其边距误差大于,大于,小于…
- 对象:nullValue,sameInstance
现在我们正在取得进步……Hamcrest匹配器的功能仍然是您可以为对象编写自己的匹配器。 您只需要扩展BaseMatcher <T>类。 这是一个简单的自定义匹配器的示例:
public class OrderMatcher extends BaseMatcher<Order> {private final Order expected;private final StringBuilder errors = new StringBuilder();private OrderMatcher(Order expected) {this.expected = expected;}@Overridepublic boolean matches(Object item) {if (!(item instanceof Order)) {errors.append("received item is not of Order type");return false;}Order actual = (Order) item;if (actual.getQuantity() != (expected.getQuantity())) {errors.append("received item had quantity ").append(actual.getQuantity()).append(". Expected ").append(expected.getQuantity());return false;}return true;}@Overridepublic void describeTo(Description description) {description.appendText(errors.toString());}@Factorypublic static OrderMatcher isOrder(Order expected) {return new OrderMatcher(expected);}
}
与旧的断言方法相比,这是一个全新的联盟。
因此,这简而言之就是Hamcrest的匹配器的用法。
但是,当我开始在现实生活中使用它时,尤其是在使用遗留代码时,我意识到故事还有很多。 这是使用匹配器时遇到的一些问题:
- 匹配器的结构可能非常重复且无聊。 我需要一种将DRY原理应用于匹配器代码的方法。
- 我需要一种统一的方式来访问匹配器。 默认情况下,框架应选择正确的匹配器。
- 我需要比较引用了另一个对象的对象,这些对象应该已经与匹配器进行了比较(对象引用可以根据需要进行深入处理)
- 我需要使用匹配器检查对象集合,而无需迭代该集合(也可以使用数组匹配器……但我想要更多的J)
- 我需要一个更灵活的匹配器。 例如,对于同一对象,我需要检查一组字段,但在另一种情况下,则需要检查另一组。 开箱即用的解决方案是为每种情况配备一个匹配器。 不喜欢那样
我使用了一些约定的匹配器层次结构克服了这些问题,并且知道哪些匹配器要应用以及比较或忽略哪个字段。 此层次结构的根是扩展BaseMatcher <T>的RootMatcher <T>。
为了处理#1问题(重复代码),RootMatcher类包含所有匹配器的通用代码,例如用于检查实际值是否为null或与预期对象具有相同类型,甚至是它们是否相同的方法。同一实例:
public boolean checkIdentityType(Object received) {if (received == expected) {return true;}if (received == null || expected == null) {return false;}if (!checkType(received)){return false;}return true;}private boolean checkType(Object received) {if (checkType && !getClass(received).equals(getClass(expected))) {error.append("Expected ").append(expected.getClass()).append(" Received : ").append(received.getClass());return false;}return true;}
这将简化匹配器的编写方式,我不必考虑null或恒等角情况; 所有这些都在根类中解决了。
预期的对象和错误也位于根类中:
public abstract class RootMatcher extends BaseMatcher {protected T expected;protected StringBuilder error = new StringBuilder("[Matcher : " + this.getClass().getName() + "] ");
这样,您可以在扩展RootMatcher之后立即进入match方法的实现,而对于错误,只需将消息放入StringBuilder中即可。 RootMatcher将处理将它们发送到JUnit框架以呈现给用户的情况。
对于问题2(自动查找匹配项),解决方案采用其工厂方法:
@Factorypublic static Matcher is(Object expected) {return getMatcher(expected, true);}public static RootMatcher getMatcher(Object expected, boolean checkType) {try {Class matcherClass = Class.forName(expected.getClass().getName() + "Matcher");Constructor constructor = matcherClass.getConstructor(expected.getClass());return (RootMatcher) constructor.newInstance(expected);} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {}return (RootMatcher) new EqualMatcher(expected);}
如您所见,factory方法尝试使用两种约定来找出应该返回哪个匹配器
- 对象的匹配器具有对象名称+字符串Matcher
- 匹配器与要匹配的对象位于同一包中(建议位于同一包中,但在测试目录中)
使用此策略,我成功使用了一个匹配器:RootMatcher.is,它将为我提供所需的确切匹配器
为了解决对象关系(第3个问题)的递归性质,在检查对象字段时,我使用了RootManager中的方法来检查将使用匹配器的相等性:
public boolean checkEquality(Object expected, Object received) {String result = checkEqualityAndReturnError(expected, received);return result == null || result.trim().isEmpty();}public String checkEqualityAndReturnError(Object expected, Object received) {if (isIgnoreObject(expected)) {return null;}if (expected == null && received == null) {return null;}if (expected == null || received == null) {return "Expected or received is null and the other is not: expected " + expected + " received " + received;}RootMatcher matcher = getMatcher(expected);boolean result = matcher.matches(received);if (result) {return null;} else {StringBuilder sb = new StringBuilder();matcher.describeTo(sb);return sb.toString();}}
但是集合(问题4)呢? 为了解决这个问题,您要做的就是为扩展RootMatcher的集合实现匹配器。
因此,唯一剩下的问题是#5:使匹配器更加灵活,能够告诉匹配器它应该忽略哪个字段以及应该考虑哪个字段。 为此,我介绍了“ ignoreObject”的概念。 当匹配器在模板(期望的对象)中找到对其的引用时,该对象将忽略该对象。 它是如何工作的? 首先,在RootMatcher中,我提供了用于返回任何Java类型的ignore对象的方法:
private final static Map ignorable = new HashMap();static {ignorable.put(String.class, "%%%%IGNORE_ME%%%%");ignorable.put(Integer.class, new Integer(Integer.MAX_VALUE - 1));ignorable.put(Long.class, new Long(Long.MAX_VALUE - 1));ignorable.put(Float.class, new Float(Float.MAX_VALUE - 1));}/*** we will ignore mock objects in matchers*/private boolean isIgnoreObject(Object object) {if (object == null) {return false;}Object ignObject = ignorable.get(object.getClass());if (ignObject != null) {return ignObject.equals(object);}return Mockito.mockingDetails(object).isMock();}@SuppressWarnings("unchecked")public static M getIgnoreObject(Class clazz) {Object obj = ignorable.get(clazz);if (obj != null) {return (M) obj;}return (M) Mockito.mock(clazz);}@SuppressWarnings("unchecked")public static M getIgnoreObject(Object obj) {return (M) getIgnoreObject(obj.getClass());}
如您所见,被忽略的对象将是被模拟的对象。 但是对于无法模拟的类(最终类),我提供了一些不太可能出现的任意固定值(可以对J进行改进)。 为此,开发人员必须使用RootMatcher中提供的equals方法:checkEqualityAndReturnError,它将检查是否忽略了对象。 使用我去年提出的这种策略和构建器模式( http://www.javaadvent.com/2012/12/using-builder-pattern-in-junit-tests.html ),我可以轻松地对复杂的结构做出断言宾语:
import static […]RootMatcher.is;
Order expected = OrderBuilder.anOrder().withQuantity(2).withTimestamp(RootManager.getIgnoredObject(Long.class)).withDescription(“specific description”).build()
assertThat(order, is(expected);
如您所见,我可以轻松地指定应忽略时间戳记,这使我可以将同一匹配器与要验证的一组完全不同的字段一起使用。
确实,此策略需要进行大量准备,使所有的构建者和匹配者成为可能。 但是,如果我们要拥有经过测试的代码,并且要使测试成为主要关注应涵盖的测试流程的工作,那么我们需要这样的基础和这些工具来帮助我们轻松地建立前提条件和建立我们的预期状态。
当然,可以使用注释来改进实现,但是核心概念仍然存在。
我希望本文能帮助您改善测试风格,如果有足够的兴趣,我会尽力将完整的代码放在公共存储库中。
谢谢。
翻译自: https://www.javacodegeeks.com/2013/12/using-matchers-in-tests.html