Команда Spring АйО перевела статью эксперта Михаила Поливахи о том, почему правило о единственном assert’е на тест иногда можно и нужно нарушать.
Я искренне верю, что большинство людей не совершают зла намеренно (хотя некоторые — да, совершают). Многие проблемы современного мира возникают из-за недопонимания и различных точек зрения на те или иные вопросы, усугублённых человеческими эмоциями, культурными различиями и другими факторами.
В частности, я считаю, что афоризм:
«Дорога в ад вымощена благими намерениями»
весьма точен. Одно из его возможных толкований заключается в том, что, навязывая определённые нормы поведения или политики, которые, как нам кажется, будут полезны для большинства, мы зачастую можем непреднамеренно усугубить общую ситуацию.
Например, так называемое «правило единственного assert’а», изложенное в книге Clean Code, я нередко нахожу запутанным и даже непрактичным во многих случаях, связанных с разработкой программного обеспечения в целом.
Это правило иногда трактуется так, что тест должен падать по единственной причине. Однако, что именно подразумевается под этой «единственной причиной», остаётся довольно расплывчатым, поэтому спорить с этим утверждением бывает непросто.
Тем не менее, многие разработчики понимают это правило буквально — как требование иметь только один оператор assert в тесте. И мой совет вам — не следовать этому правилу слишком строго и отходить от него, если того требует ситуация.
Почему?
Потому что программное обеспечение — это крайне разнообразная и сложная сфера. Давайте представим, что мы пишем фабричный класс, примерно вот такой:
public class ConfigurationFactory { public static Configuration createInstance() { String javaVersion = System.getProperty("java.version"); String user = System.getenv("USER"); int cpusAvailable = Runtime.getRuntime().availableProcessors(); return new Configuration(javaVersion, user, cpusAvailable); } static class Configuration { String javaVersion; String user; int cpusAvailable; // constructor, getters etc. } }
Приведённый выше пример — это Java-код, реализующий фабрику, которая создаёт объект, предварительно настраивая его. Этот шаблон, по крайней мере с точки зрения семантики, довольно распространён в разработке в целом.
Теперь допустим, что мы хотим протестировать метод createInstance() этой фабрики. И вот возникает вопрос — какое именно поведение мы хотим протестировать?
Очевидно, мы хотим убедиться, что создаваемый объект инициализируется в состоянии, которое считается корректным в рамках текущего тестового окружения. Звучит логично.
Но тогда возникает следующий вопрос — как именно мы будем проверять это состояние?
Следуя правилу
Если следовать «правилу единственного assert’а», то мне пришлось бы ограничиться единственной проверкой внутри теста. И тут становится очевидным: если я не хочу жертвовать качеством тестирования, мне нужно вручную создать экземпляр Configuration в рамках теста. А затем сравнить, совпадает ли внутреннее состояние этой вручную сконструированной Configuration с той, которую возвращает фабрика.
Проблема №1
В приведённом выше примере объект намеренно является достаточно компактным, т.к. служит для целей примера. Но если объект большой, то его ручное создание с, например, 30 полями будет абсолютно:
-
Скучным (а это куда более серьёзная проблема, чем может показаться на первый взгляд)
-
Подверженным ошибкам
-
Трудным в сопровождении (представьте, как «весело» будет добавлять 31-е поле).
Суть этой сложности в том, что объект изначально не задумывался для ручного создания. Просто не был. Именно поэтому мы и используем фабрику. Не говоря уже о том, что конструктор мог быть не public.
Есть и другая проблема: мне может потребоваться проверить только часть полей на точное соответствие. Например, если фабрика генерирует случайный UUID, я просто не смогу вручную создать тот же самый UUID в тесте — мне нужно лишь убедиться, что UUID:
-
установлен фабрикой,
-
и соответствует спецификации.
Проблема №2
Хорошо, допустим, что объект достаточно лёгкий, и мы можем позволить себе создать его вручную и сравнивать без особых затрат. Но тут возникает другая проблема.
Видите ли, я не знаю, на каком языке программирования вы пишете, но в Java по умолчанию при сравнении двух ссылок на объекты проверяется лишь то, указывают ли они на один и тот же участок памяти в heap-е Java процесса в рамках ОС.
А в нашем случае очевидно, что создание отдельного объекта вручную нам не поможет, поскольку это будет другой объект, пусть даже с теми же значениями в полях.
Что можно сделать? Мы можем переопределить методы equals() и hashCode() в Java, чтобы сравнение происходило по содержимому полей, а не по ссылкам:
static class Configuration { String javaVersion; String user; int cpusAvailable; // constructor, getters etc. @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Configuration that = (Configuration) o; return cpusAvailable == that.cpusAvailable && Objects.equals(javaVersion, that.javaVersion) && Objects.equals(user, that.user); } @Override public int hashCode() { return Objects.hash(javaVersion, user, cpusAvailable); } }
Так что, хорошо ли это?
Нет. Это ужасно.
Почему всё так плохо? Потому что наш тестовый код начинает диктовать, как должен выглядеть продуктивный код. А этого никогда не должно происходить.
В более общем смысле, проблема в том, что такой подход заставляет абстракции в коде развиваться неестественным образом, и в конечном итоге это приводит к появлению странного, неудобного и противоречивого API для пользователей. Поэтому такой сценарий следует избегать любой ценой.
Решение
Просто пишите несколько assert’ов, если считаете это уместным. Не бойтесь.
Вас за это не посадят, честное слово.
В этом нет ничего сложного:
@Test void testCreateInstance() { Configuration instance = ConfigurationFactory.createInstance(); assertSoftly(softAssertions -> { softAssertions.assertThat(instance.getUser()).isEqualTo("my_user"); softAssertions.assertThat(instance.getJavaVersion()).isEqualTo("17.0.4.1"); }); }
В результате вам не только не нужно создавать объект вручную, но и можно проверить только те поля, которые действительно вас интересуют, и на тех условиях, которые имеют смысл (вспомните пример с UUID). Вы не вынуждаете ваш класс Configuration переопределять equals()/hashCode() или подстраиваться под какие-либо особенности, нужные только для тестов.
Так что, как это часто бывает в жизни, просто руководствуйтесь здравым смыслом, принимая решения.
Хорошего дня!

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
ссылка на оригинал статьи https://habr.com/ru/articles/913130/
Добавить комментарий