Статьи

Уроки, извлеченные из JsonUnit

Я хотел бы поделиться некоторыми уроками, которые я извлек при работе с JsonUnit . Я начал проект почти три года назад, когда мне нужно было что-то сравнить структуры JSON в модульных тестах. Сегодня я рад, что начал проект — он идеально подходит для обучения. С одной стороны, он достаточно маленький, чтобы я мог вернуться через несколько месяцев и все еще помнить, что происходит. С другой стороны, он достаточно большой, чтобы не быть скучным. Более того, время от времени я получаю запрос на функцию, которая дает мне возможность играть.

Подумайте дважды, прежде чем добавлять новый метод assert *

Все началось с одного статического метода
assertJsonEquals (ожидаемого, фактического) . Но вскоре появились новые методы —
assertJsonPartEquals (ожидаемый, фактический, путь) для сравнения только части документа,
assertJsonStructureEquals для сравнения только структуры при игнорировании значений и, очевидно,
assertJsonPartStructureEquals, объединяющие два метода вместе.

Все идет нормально. Пока не поймешь, что было бы неплохо иметь отрицательные варианты. Что-то вроде 
assertPartNotEquals . Таким образом, вы просто создаете четыре новых метода с именем Not in, и вдруг у вас есть восемь методов. Каждая новая функция просто умножает количество методов.

А как насчет типов параметров? Мы будем поддерживать
String ,
Reader или
InputStream для ожидаемых и фактических значений? Количество возможных комбинаций просто поражает.

Это легко решить проблему типа параметра. Просто используйте старый добрый Object и разберитесь с типами внутри библиотеки. К счастью, нам не нужна безопасность типов при сравнении документа JSON. На самом деле, все наоборот, мы можем сравнить число с такой строкой

assertJsonPartEquals(
	2, 
	"{\"test\":[{\"value\":1},{\"value\":2}]}", 
	"test[1].value"
);

Но что делать с именем метода комбинаторного взрыва? Мне потребовалось довольно много времени, чтобы понять, что это можно решить и параметрами. Таким образом, вместо
assertStructureEquals, являющегося дополнительным методом, он может быть просто параметризацией
assertJsonEquals .

Используйте объекты для параметров

Довольно заманчиво добавить новый логический параметр для таких параметров. Например

assertJsonEquals(Object expected, Object actual, boolean structureOnly)

Но это не сильно поможет. Любой дополнительный параметр конфигурации снова вызовет взрыв API из-за количества комбинаций. Есть лучший подход. Мы можем использовать объекты и делать что-то вроде

assertJsonEquals(Object expected, Object actual, 
			Configuration configuration)

Теперь можно уменьшить количество методов и просто расширить класс Configuration. Кроме того, если вы предоставите хороший метод фабрики, код читается вполне естественно

assertJsonEquals(
		"[{\"test\":1}, {\"test\":2}]", 
		"[{\n\"test\": 1\n}, {\"test\": 4}]", 
		when(COMPARING_ONLY_STRUCTURE)
);

Спички Hamcrest отлично подходят для композиции

Проблема со сложностью взрыва вызвана тем, что невозможно комбинировать стандартные статические методы утверждения. Но если вы используете спички Hamcrest, вы можете это сделать. Вы даже можете комбинировать стандартные совпадения со своими

assertThat(
	asList("{\"test\":1}"), 
	not(contains(jsonEquals("{\"test\":2}")))
);

Методы
not () ,
contains () и
jsonEquals () каждый из разных JAR-файлов, но они хорошо играют вместе.

Более того,
jsonEquals — это в основном фабричный метод, который создает экземпляр. Это дает вам возможность расширить соответствие и делать такие вещи, как это.

assertThat("{\"test\":1.00001}", 
	jsonEquals("{\"test\":1}").withTolerance(0.001));

По сути, это то же самое, что и
класс
Configuration выше, но на этот раз реализовано на самом устройстве сопоставления.

Java-дженерики сумасшедшие

Вы понимаете дженерики Java? Если вы думаете, скажите, какой из следующих стандартных методов Hamcrest является правильным

<T> Matcher<java.lang.Iterable<? extends T>> 
		contains(Matcher<? super T> itemMatcher)

<T> Matcher<java.lang.Iterable<? super T>> 
		hasItem(Matcher<? super T> itemMatcher)

Оба они принимают сопоставление и создают другое сопоставление, которое можно использовать в Iterable. Но один тип возврата содержит
<? расширяет T>, а другой
<? супер T> .
Правило PECS гласит, что производитель расширяется, а потребитель подразумевает супер, Matcher — потребитель, поэтому
hasItem верен. Или нет? Применимо ли здесь правило PECS? Я не скажу вам решение, но оно описано
здесь .

К сожалению, результатом этой разницы является то, что первая строка следующего кода компилируется, а вторая — нет. И мне не удалось заставить их скомпилировать оба

// compiles
assertThat(asList("{\"test\":1}"), contains(jsonEquals("{\"test\":1}")));

// does not compile
assertThat(asList("{\"test\":1}"), hasItem(jsonEquals("{\"test\":1}")));

Свободные утверждения велики

В то время как совпадения Hamcrest отлично подходят для композиции, мой предпочтительный стиль утверждений — свободный. Если вы используете
AssertJ , вы знаете, о чем я говорю. В контексте утверждений JSON это может выглядеть так

assertThatJson("{\"test\":[1,2,3]}").node("test").isEqualTo(new int[]{1, 2, 3});

Я люблю этот стиль как пользователь. Мне нужно только написать assertThatJson, а остальное намекается и автоматически заполняется IDE. Больше не нужно помнить, создается ли совпадение равенства с помощью
equalTo () ,
eq () или
is () . Больше не надо бороться с этим надоедливым статическим импортом.

Более того, мне также нравится этот подход как автора библиотеки. Существует только один статический метод, а остальное — обычное объектно-ориентированное программирование. С ним действительно легко работать. Интересно, почему это не выбор по умолчанию для утверждений.

Если это больно, вы делаете что-то не так

Java — разговорчивый язык, и иногда он даже стоит у вас на пути. Но чаще всего это просто намек на то, что вы делаете что-то не так. Язык пытается вам что-то сказать. Это случилось со мной, когда я начал использовать EnumSet для управления опциями. Проблема с наборами Java заключается в их изменчивости. Если вы хотите сохранить неизменность, вам нужно написать три строки, чтобы добавить один элемент.

EnumSet<Option> optionsWith = EnumSet.copyOf(options); // copy the original set
optionsWith.add(option); // add option
options = optionsWith; // replace the original set with a new value

Вы должны повторять эти строки в каждом месте, где вы хотите добавить опцию. Больно. По крайней мере, до тех пор, пока вы не поймете, что не стоит использовать простой EnumSet. У нас есть объектно-ориентированный язык, поэтому давайте создадим объект и скроем детали реализации внутри

public class Options {
    private final EnumSet<Option> options;
 
    ...
    public Options with(Option option) {
        EnumSet<Option> optionsWith = EnumSet.copyOf(options);
        optionsWith.add(option);
        return new Options(optionsWith);
    }
}

Три уродливые линии все еще там. Но только один раз. Я не только скрыл их, но и вынудил меня представить новый объект и осознать, что существуют другие методы, принадлежащие классу Options. Теперь код стал более читабельным и понятным. Если бы EnumSet был проще в использовании, я бы никогда не понял, я должен представить объект.

Характеристика покрытия покрытия является королем

Поскольку я работаю над библиотекой тестирования, неудивительно, что у меня есть модульные тесты. Я не знаю свое тестовое покрытие, но я думаю, что у меня есть 100% покрытие. Другими словами, для каждой известной мне функции есть тест. И это здорово. Я не боюсь делать радикальный рефакторинг, и если тесты пройдут, я вполне уверен, что ничего не сломал. Но к этому сложно привыкнуть. Если тесты пройдут после большого рефакторинга, я просто не верю своим глазам. Я по глупости снова запускаю их, чтобы увидеть, как они снова проходят. И иногда я даже чувствую себя немного разочарованным. Я просто ожидаю, что получу удовольствие от долгой охоты за ошибками, и чертов код просто работает Где веселье?

Pet проекты отличные

Я хочу закончить к последнему уроку, который я выучил. Здорово иметь проект с домашним животным. На работе у меня есть все эти сроки и давление, так что это не легко учиться. Обычно я просто использую первое решение, которое работает. Работая над
JsonUnit, я чувствую, что все еще учусь. Большинство запрашиваемых функций удивляют и требуют переосмысления проекта снова и снова с новыми и интересными способами. И это то, что мне нравится в программировании.