Comments 23
Про случайные числа не согласен: как минимум существует randomized testing, которое в итоге имеет бОльшее покрытие. Впрочем там не так, чтобы уж и случайные числа.
Можете привести пример, когда рандомное число выявит баг, который был бы пропущен параметризованным тестом из 4-5 кейсов?
В том и фишка, рандомизированного тестирования, что случайные числа показывают проблемы за рамками ранее описанных кейсов:
самый известный пример — unicode символы, для которых как правило тесты пишутся только latin1 — но когда на вход приходит французский (или любой другой не latin1) — то появляются неожиданные результаты.
После, конечно, добавляют этот кейс, но рандомизированное может показать это значительно раньше, чем их встретишь
как правило тесты пишутся только latin1
Плохое правило, если есть поддержка любых языков и символов :) В таком случае и рандомизатор напишут скорее всего для latin1, и ничего он не найдет. А если будут осознанно запихивать в тест рандомизатор любых UTF-символов, то сразу же решат проверить и кейсы с многобайтными символами, и снова приходим к тому, что лучше просто добавить в data provider теста таких сложных символов, а не надеяться на рандом.
Я тоже так думал. Но рандомизированные тесты надо уметь писать. Если их написать неправильно, то в лучшем случае они будут падать через раз, а в худшем локально проходить, а билд перед продом падать. Я для себя решил не писать рандомизированные тесты — так проще. И не было случая когда я без них не обходился
Как ни крути, но нельзя получить чистого случайного числа (без использования доп. физических девайсов) — почти все они псевдослучайные, а это значит, что если ты знаешь начальный seed — то вся последующая последовательность уже вычислима. На этом и строятся многие фреймворки рандомизированного тестирования — при падении он так же сообщает тебе и начальный seed и что на CI, что локально есть воспроизводимость.
Во-первых, спасибо за статью и хотелось бы выразить негодование по поводу тегов и добавления статьи в хаб "Kotlin". Единственное упоминание этого языка есть только в самом конце, в блоке с саморекламой автора.
Во-вторых, сам автор, как я понял, слабо знаком с экосистемой этого языка, так как по ссылке предлагается все тот же JUnit, тогда как под Kotlin намного рациональнее использовать Spek / Kotest. По примерам уже видно, что обе эти библиотеки позволяют довольно просто сделать те же параметризованные тесты произвольной вложенности и т.д. Аналогично и про моки — тот же mockk синтаксически удобнее аналогов из Java.
В-третьих, в пункте "Используйте заданные значения вместо случайных" опущен прием с "вшитым seed'ом". То есть, просто класс Random
использовать нежелательно, так как тесты будет трудно воспроизвести, тут все верно. Однако, зачастую необходимы разные данные на вход. И в этом случае можно создавать класс Random
просто с заранее вшитым seed
. Подобная практика очень удобная в интеграционных тестах, когда надо создавать много однотипных объектов в условной базе, однако они не должны повторяться на протяжении тестов.
Хорошая статья. Только для того, чтобы убрать «лишний» код в виде конструкторов необязательно переходить на Котлин. Достаточно использовать Ломбок.
Мы внутри себя считаем, что Lombok — это плохо, привносит лишнюю магию и плохо отражается на читаемости. Но если вам с ним ок — то да.
В некотором виде, Kotlin можно рассматривать как "lombok на стеройдах", если знать во что он превращается в итоге. Можно посмотреть подробнее на пост Kotlin, компиляция в байткод и производительность
Насчет использования Instant напрямую в бизнес логике — всегда использовал и не было проблем с тестированием У AssertJ есть метод проверки что-то типа isBeforeOrEqual. А так же я переводил instant в epochSeconds и опять же у assertj есть метод проверки long(?) и double на примерное совпадение, где можно указать процент допустимой разницы между двумя значениями. Но ваш способ через Clock кажется изящнее. Попробую его
Читаешь про тесты: все всегда одно и то же, так пиши, так не пиши. По итогу все равно стремные тесты получаются. Имхо, всему виной процедурная натура подхода к тестированию а иногда и к написанию кода, в итоге зачастую имеешь портянку всяких Mockito.when, не пойми как сгенеренных данных лишь бы работало и кучу ассертов на все на свете на всякий случай.
Если бы кто то предложил красивый декларативный подход (я видел пару библиотек, но меня не убедили), код был бы разбит настолько гранулярно, что можно было бы отделаться парочкой моков (а ещё лучше без них), ну или если бы девелоперы руководствовались принципом «1 тест — 1 ассерт», то все это было бы не нужно
От себя добавлю что:
— Использование статических импортов для общеизвестных методов и констант существенно упрощает чтение и позволяет не утопать в символах;
— Для легкой навигации по участкам Given, When, Then также добавляю комметарии
@Test
public void findProduct() {
// given
...
// when
...
// then
...
}
Позволяет быстро ориентироваться в тесте, если тот содержит больше пары строк кода в одном из участков
и вы вставляете такой текст с названиями методов 'filterByDateCreated', 'filterByCategory'. Это плохие имена методов.
Глагол 'filter' не отвечает на вопросы «кто» и «что тестируется». Прибавка «ByDateCreated» не даёт мне нужной информации.
Добавлю, что в ряде случаев, когда объекты запросов / expected ответов большие, удобно их не собирать прямо тут, а всё-таки вынести в какой-нибудь .json в ресурсах, и вспомогательными методами (jackson-ом) читать, когда надо. Это в поддержку того, чтобы не использовать продуктовые мапперы.
А я вот не люблю AssertJ, и предпочитаю Hamcrest. По базовому функционалу у них паритет, но на мой вкус написание собственных проверок у Hamcrest куда проще, и, в отличие от AssertJ, не требуется писать (или по новой моде — кодогенерировать) своих версий assertThat
для кастомных типов и прочей обвязки.
За статью большое спасибо. Всё очень четко.
Про параметризированные тесты накину кейз неправильного использования, который достаточно часто меня расстраивает.
Это попытка разработчика загнать ВСЁ в параметризированные кейзы. Точнее в один параметризованный тест. Это приводит к тому, что этот параметризированный тест всё чаще становится всё крупнее и сложнее (к тому же для некоторых кейзов делает что-то лишнее).
Я бы в этом случае подумал о серии мелких простых тестов, или серии параметризированных тестов более простых, чем одна большая enum-ка и монструозный тест. Либо о комбинации. Основные тесты - параметризированы (возможно, несколько параметризированных), а особые случаи, которые не легли в общую схему достаточно легко, выделены в отдельные тесты.
Передовой опыт тестирования в Java