Hamcrest содержит прекрасную библиотеку матчеров, а их комбинирование позволяет творить чудеса. А когда их возможностей не хватает, можно написать свой собственный матчер.
Мне потребовалось проверять результат формирования параметров GET запроса:
arg1=val1&arg2=val2&arg3=val3"
И я столкнулся с тем, что во-первых это строка, во-вторых порядок элементов в строке не важен, поэтому сравнивать проверяемую строку с образцом нельзя. Чтобы не загромождать тест дополнительными преобразованиями, я решил написать матчер, сравнивающий строки GET параметров.
Собственный матчер
Матчер должен расширять класс BaseMatcher и переопределять его методы, но мы будем использовать типобезопасную версию TypeSafeMatcher
public class UrlParametersMatcher extends TypeSafeMatcher<String> { private final String expectedString; private final Map<String, String> expected; protected final Map<String, String> paramStringToMap(final String string) { return Arrays.stream(string.split("&")) .collect(Collectors.toMap( (String s) -> s.split("=")[0], (String s) -> s.split("=").length==2 ? s.split("=")[1] : "")); } /** * Constructs matcher. * @param e model string to match. */ public UrlParametersMatcher(final String e) { expectedString = e; expected = paramStringToMap(expectedString); } @Override protected final boolean matchesSafely(final String actualString) { return expected.equals(paramStringToMap(actualString)); } @Override public final void describeTo(final Description description) { description .appendText("Parameters should match: ") .appendValue(expectedString); } @Override protected final void describeMismatchSafely( final String item, final Description mismatchDescription) { Map<String, String> actual = paramStringToMap(item); if (expected.size() != actual.size()) { mismatchDescription .appendText("Actual number of parameters is ") .appendValue(actual.size()) .appendText(" while expecting ") .appendValue(expected.size()) .appendText(" parameters"); return; } for (Map.Entry<String, String> entry: expected.entrySet()) { if (!actual.containsKey(entry.getKey())) { mismatchDescription .appendText("Expected parameter ") .appendValue(entry.getKey()) .appendText(" is not found"); return; } if (!entry.getValue().equals(actual.get(entry.getKey()))) { mismatchDescription .appendText("For parameter ") .appendValue(entry.getKey()) .appendText(" actual value is ") .appendValue(actual.get(entry.getKey())) .appendText(" while expected value was ") .appendValue(entry.getValue()); return; } } } /** * Constructs ready-to-use matcher. * @param parameters model string. * @return Fully constructed matcher. */ public static UrlParametersMatcher parametersEqual(final String parameters) { return new UrlParametersMatcher(parameters); } }
В коде матчера можно выделить три основных части — обработку строки параметров, проверку и вывод результата и фабричный метод.
Обработка параметров
Образец для сравнения передаётся в конструктор. Здесь, используя paramStringToMap, его разбирают на части и сохраняют для дальнейшего использования.
private final String expectedString; private final Map<String,String> expected; protected Map<String,String> paramStringToMap(String string) { return Arrays.stream(string.split("&")) .collect(Collectors.toMap( (String s) -> s.split("=")[0], (String s) -> s.split("=").length==2 ? s.split("=")[1] : "")); } public UrlParametersMatcher(String e) { expectedString = e; expected = paramStringToMap(expectedString); }
Функция paramStringToMap разделяет строку параметров на отдельные параметры, которые складываются в хэш. Разделение строки на параметры реализовано на streams api Java 8.
Сравнение
Главная часть матчера — сюда передаётся фактическое значение и производится сравнение:
@Override protected boolean matchesSafely(String actualString) { return expected.equals(paramStringToMap(actualString)); }
Тестовая строка просто переводится в хэш и сравнивается с образцом. Гораздо интереснее функции вывода результатов:
@Override public final void describeTo(final Description description) { description .appendText("Parameters should match: ") .appendValue(expectedString); } @Override protected final void describeMismatchSafely( final String item, final Description mismatchDescription) { Map<String, String> actual = paramStringToMap(item); if (expected.size() != actual.size()) { mismatchDescription .appendText("Actual number of parameters is ") .appendValue(actual.size()) .appendText(" while expecting ") .appendValue(expected.size()) .appendText(" parameters"); return; } for (Map.Entry<String, String> entry: expected.entrySet()) { if (!actual.containsKey(entry.getKey())) { mismatchDescription .appendText("Expected parameter ") .appendValue(entry.getKey()) .appendText(" is not found"); return; } if (!entry.getValue().equals(actual.get(entry.getKey()))) { mismatchDescription .appendText("For parameter ") .appendValue(entry.getKey()) .appendText(" actual value is ") .appendValue(actual.get(entry.getKey())) .appendText(" while expected value was ") .appendValue(entry.getValue()); return; } } }
Функция describeTo описывает сам матчер, функция describeMismatch описывает, что матчеру не понравилось при сравнении. Содержимое этих функций и подробность отчёта целиком и полностью на совести разработчика. Я реализовал проверку всех шагов сравнения хэшей с подробным выводом различий.
Использование
Для удобства использования матчера в его код добавлен вспомогательный метод по созданию матчера:
public static UrlParametersMatcher parametersEqual(final String parameters) { return new UrlParametersMatcher(parameters); }
Теперь матчер можно использовать как:
assertThat("arg1=val1&arg2=val2&arg3=val3", parametersEqual("arg1=val1&arg2=val2&arg3=val3"));
Можно проверить, как выводятся ошибки:
public class UrlParametersMatcherTest { @Test public void assertMatches() { assertThat("arg1=val1&arg2=val2&arg3=val3", parametersEqual("arg1=val1&arg2=val2&arg3=val3")); } @Test public void assertLengthMismatch() { assertThat("arg1=val1&arg2=val2&arg3=val3&arg4=val4", parametersEqual("arg1=val1&arg2=val2&arg3=val3")); } @Test public void assertNamesMismatch() { assertThat("arg1=val1&arg22=val2&arg3=val3", parametersEqual("arg1=val1&arg2=val2&arg3=val3")); } @Test public void assertValuesMismatch() { assertThat("arg1=val1&arg2=val22&arg3=val3", parametersEqual("arg1=val1&arg2=val2&arg3=val3")); } }
Tests run: 4, Failures: 3, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec <<< FAILURE! - in org.satgate.filestream.UrlParametersMatcherTest assertNamesMismatch(org.satgate.filestream.UrlParametersMatcherTest) Time elapsed: 0 sec <<< FAILURE! java.lang.AssertionError: Expected: Parameters should match: "arg1=val1&arg2=val2&arg3=val3" but: Expected parameter "arg2" is not found at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20) at org.junit.Assert.assertThat(Assert.java:956) at org.junit.Assert.assertThat(Assert.java:923) at org.satgate.filestream.UrlParametersMatcherTest.assertNamesMismatch(UrlParametersMatcherTest.java:22) assertValuesMismatch(org.satgate.filestream.UrlParametersMatcherTest) Time elapsed: 0 sec <<< FAILURE! java.lang.AssertionError: Expected: Parameters should match: "arg1=val1&arg2=val2&arg3=val3" but: For parameter "arg2" actual value is "val22" while expected value was "val2" at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20) at org.junit.Assert.assertThat(Assert.java:956) at org.junit.Assert.assertThat(Assert.java:923) at org.satgate.filestream.UrlParametersMatcherTest.assertValuesMismatch(UrlParametersMatcherTest.java:27) assertLengthMismatch(org.satgate.filestream.UrlParametersMatcherTest) Time elapsed: 0 sec <<< FAILURE! java.lang.AssertionError: Expected: Parameters should match: "arg1=val1&arg2=val2&arg3=val3" but: Actual number of parameters is <4> while expecting <3> parameters at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20) at org.junit.Assert.assertThat(Assert.java:956) at org.junit.Assert.assertThat(Assert.java:923) at org.satgate.filestream.UrlParametersMatcherTest.assertLengthMismatch(UrlParametersMatcherTest.java:17)