Comments 18
Мне кажется, что вы описали нечто подобное тестированию контрактов. Есть специальный софт для этого https://pactflow.io/
Есть даже понятие snapshot testing
Спасибо огромное за статью. Я давно задавался вопросом, почему такой способ тестирования редко применяется на практике.
Подскажите, был ли у вас опыт использования RnR тестов в end-to-end сценариях?
Я так подозреваю, что сложность управления записанными сценариями перевешивает пользу.
Нет, в end-to-end опыта применения подхода нет.
Хотя некоей принципиальной невозможности я здесь не вижу. Смысл же в том, что при запуске с отключенной кассетой (или первой записи кассеты) это вполне себе high level тест, вовлекающий все компоненты. Т. е. в теории, например, написали вы тест на Selenium, который проверяет некий бизнес-сценарий, используя ваш UI, соединенный с вполне настоящим бэкендом, который, в свою очередь, взаимодействует с вполне настоящими микросервисами, базой, message broker'ом и т. п. Т. е. проверяется весь процесс целиком. Это вполне end-to-end.
Но вот дальше возникает вопрос: а на каких соединениях между компонентами у вас воткнут прокси обеспечивающий логику "в первый раз записали, потом воспроизводим"? Ведь сама идея теста в режиме воспроизведения, что с целью экономии мы выключаем какие-то или все зависимости подменив их на штуковину, которая просто воспроизводит записанные ранее ответы. Т. е. мы точно хотим, чтобы в каких-то компонентах реально выполнялся код, а до каких-то даже дело не доходило. Это уже точно не end-to-end. Хотя все еще может быть полезным. Например у нас в архитектуре есть часто меняющийся сервис и много других, которые меняются редко. В режиме воспроизведения мы отсекаем редко-меняющиеся сервисы.
Оно применяется (и довольно активно). Просто о нём особо не говорят, потому что это "тестирование для бедных".
о да, юнит-тесты, тестирующие имплементацию, а не контракт – это, к сожалению, классика. И они не помогают рефакторить, а только мешают. И бывает очень тяжело объяснить людям, что пользы от таких тестов практически нет, а время на их написание и поддержание уходит.
Причем на поддержку времени уходит сильно больше, чем на поддержку нормально написанных тестов. Изменил детали реализации, которые никак не меняют внешнее поведение модуля - лезешь править упавшие тесты, просто потому что они жестко фиксируют реализацию.
На текущем проекте таких тестов наверное процентов 80, и это реальная головная боль.
Я flaky перевожу как "неустойчивые". Т.е. те, которые при многократном повторении, (при прочих равных) могут давать результат, то в одну, то в другую сторону.
Но это не самый ад. Их хотя бы видно. Самый ад, это ложно отрицательные результаты. Т.е. те, которые дают зеленый свет, хотя не должны. Найти их в куче других тестов практически невозможно, если только случайно не наткнешься на код. Поэтому ревью тестов - важно. Это пожалуй единственный и последний шанс их отловить. Ну или вводить fuzzy testing. Но это уже находится часто за гранью целесообразности.
Может все-таки ложно-положительные, если "зеленый свет" дают? И защита от них мутационное тестирование. А fuzzy testing - это про вариацию входных параметров. Он тоже может выявить плохо спроектированные тесты, но у него все-таки изначально другое назначение.
В медицине отрицательный результат теста говорит об отсутствии болезни. Т.е. что "все хорошо". Зеленый тест в автоматизации тоже говорит, что "все хорошо". Я придерживаюсь именно этой терминологии. Сам иногда путаюсь, поэтому применяю медицинскую аналогию.
Да, я именно мутационное тестирование имел ввиду, спасибом что поправили.
Может вы уже видели тесты такого рода в своем проекте, а может и сами такие пишете. Вот достаточно типичный юнит-тест из живого проекта (слегка модифицированный ради соблюдения NDA), который я взял в качестве примера. Во-первых на что я сразу хочу обратить ваше внимание — это соотношение количества кода в тесте к количеству тестируемого кода. Как видите, примерно 1:1
Интересно было бы услышать у автора, что он хочет сказать соотношением кода 1 к 1?
Что это много? У меня тесты на фабрики объектов из json намного больше самих фабрик - потому что нужно проверить каждый пункт валидации данных. И что?
Я, конечно, могу только гадать, как устроены ваши тесты, но давайте погадаю. Полагаю я бы сделал такие проверки валидаций неким параметрическим тестом, в котором на вход в общем-то одного тестового метода подается много пар "json" <-> "результат валидации". Допустим, ваши тесты устроены именно так. И что же мы имеем, много кода или мало? Хотя пары значений и могут быть записаны прямо в кодовой базе, фактически это не код, а ресурсы. Например, мы легко бы могли сделать их externalized и считывать из какого-нибудь файла.
Так вот говоря о соотношении тестируемого кода к коду тестов я не считаю нужным принимать в расчет ресурсы. В конце концов и в том же RnR получается очень много ресурсов. Каждый файл кассеты, содержащий все пары response/request для всего внешнего взаимодействия в рамках бизнес-сценария - это весьма немало.
Что же мы имеем в моем негативном примере? Там код - это именно код, а не ресурсы. И это имеет значение, потому-что ресурсы, насколько я могу судить по опыту, это дешевле, чем код. Из-за регулярной структуры их проще поддерживать и проще писать. Допустим, в вашем .json появилось новое обязательное для всех поле. Его добавление во все пары будет довольно простым механическим действием. Его даже можно автоматизировать, если таких пар реально много. В конце концов, если это externalized, этот мартышкин труд можно даже поручить не-программисту.
Напротив, появление какого-нибудь нового параметра, который нужно добавлять в payload в представленном sendReadyForDeliveryEvent потребует модификаций именно в коде теста. Поэтому здесь сильная связность тестов с тестируемым кодом, соотношение 1 к 1 и все другие перечисленные недостатки. А в вашем примере связность слабая и соотношение далеко не 1 к 1.
Как-то так я это понимаю.
Там код - это именно код, а не ресурсы. И это имеет значение, потому-что ресурсы, насколько я могу судить по опыту, это дешевле, чем код. Из-за регулярной структуры их проще поддерживать и проще писать
Нельзя ли организовать регулярную структуру в коде?
Напротив, появление какого-нибудь нового параметра, который нужно добавлять в payload в представленном sendReadyForDeliveryEvent потребует модификаций именно в коде теста. Поэтому здесь сильная связность тестов с тестируемым кодом, соотношение 1 к 1 и все другие
Общие вещи можно вынести в общие методы. Тогда связь будет не 1:1. В ресурсах это не получится. Так же в ресурсах, в особенности записанных автоматически, нельзя будет понять откуда взялось значение и как именно оно связано с условиями теста.
Одно дело
Tax.Should().Be(0.12)
другое дело
Tax.Should().Be(Round(OrderTotal * Discount * TaxRate, CurrencyRounding))
Поэтому здесь сильная связность тестов с тестируемым кодом, соотношение 1 к 1 и все другие перечисленные недостатки
Код проверяет функционал работы программы. Если проверяет полноценно - тесты хорошие, если нет - то нет.
Саму же красоту кода (как функционала, так и тестов) при желании можно рефакторить - я вообще не понимаю, как это относится к теме полезности или бесполезности unit-тестов.
Ну, юнит-тесты нужны не для тестирования алгоритмов сортировки, а для тестирования дыр в системе типов. Нуль-пойнтеры, непустые коллекции, валидация полей, off-by-one, business exceptions, cross-field validations - пример вашего кода не проверяет из этого ничего, кроме того, что вы таки передали параметры в send. Иначе юнит тестов стало бы в 5-10 раз больше - как юнит-тестам и положено.
Метод convertContentToEntityDescriptor можно вызвать отдельно и передать его результат в основной метод, тогда и Mockito не понадобится.
Заменять юнит-тесты на RnR нет никакой необходимости - мы не можем записывать отдельную кассету для каждого нуль-пойнтера.
У RnR безусловно есть ниша, но она не совсем там, где вы описываете. Я вижу два, как минимум кейса.
1) - легаси API. Например, в вашем случае если нам надо послать все через CrappyGenericLegacyService, который два бизнес-поля превращает в сто полей, содержит private static void methods, половину ошибок глотает, а половину выплевывает во внешний сервис, который содержит в качестве зависимости. Проверять все ассертами долго, юнит тестами не подлезть. Да мы часто и не знаем, что именно проверять, а знать, что из него послано - хотелось бы. В этом случае RnR подходит идеально - легаси меняется редко и его поведение, как правило, детерминировано.
2) - матмодели. Если на вход подается два датасета и десять параметров, а на выходе еще один датасет значений в 1000 - сетапить тяжело, и отдельными ассертами неудобно тестировать.
И в том, и в другом случае RnR не столько тестирует (у него нет given-when-then), сколько подтверждает поведение системы, поэтому достаточно happy path записать.
Ну и в имплементации тоже.
"банальная регулярка" для отсеивания недетерминированных фрагментов - сама по себе источник багов. Практичнее, хотя и дольше, действительно ручками вытащить и записать нужные фрагменты, и иметь полностью детерминированную чистую кассету.
вместо человеко-читаемости кассету гораздо удобнее читать машиной. Для длинной кассеты assertEquals неинформативен. Кассета должна быть diffable и automatable, и дифф просто возвращает номер первой неверной строки.
- матмодели. Если на вход подается два датасета и десять параметров, а на выходе еще один датасет значений в 1000 - сетапить тяжело, и отдельными ассертами неудобно тестировать.
Это уже рекординг аутпута, наверное, а не депенденси. Типа https://approvaltests.com/
Ещё у кассет есть 2 большие проблема - их можно записать на существующей системе и существующих данных, что существенно снижает их применимость.
Я пользуюсь pyVCR и yaml формат кассет более 3 лет. Подход интересный, но ограниченный. Если нет готового окружения под рукой - проще делать моки. Для неожиданных данных - тоже моки. Да и без моков легко получить кассету в 180мб, и окончательно потеряться.
Кстати, большинство юнитов с кассетами используются повторно как интеграционные, но уже без кассет (они просто удаляются).
Record-and-Replay тестирование — сочетание достоинств юнит и интеграционных тестов