Как написать собственный hamcrest matcher

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"));
    }
}
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *