在测试中使用匹配器
我們被迫在測試代碼中寫太多斷言行的日子已經一去不復返了。 鎮上有一個新的警長: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的匹配器的用法。
但是,當我開始在現實生活中使用它時,尤其是在使用遺留代碼時,我意識到故事還有很多。 這是使用匹配器時遇到的一些問題:
我使用了一些約定的匹配器層次結構克服了這些問題,并且知道哪些匹配器要應用以及比較或忽略哪個字段。 此層次結構的根是擴展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方法嘗試使用兩種約定來找出應該返回哪個匹配器
使用此策略,我成功使用了一個匹配器: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
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
- 上一篇: 古墓丽影崛起流畅设置(古墓丽影崛起最低配
- 下一篇: 苹果怎么设置家人共享(苹果怎么设置家人共