Comments 29
P.S. мне понравилась идея; просто проверяю её на прочность и ищу скрытые подводные камни.
Подводный камень — то, что получающйся тест плохой.
- в коде теста все данные, не относящиеся к данному тестовому случаю дожны быть спрятаны
- в тесте должно быть понятно, к какому кейзу это относится и почему так
- входные данные в коде теста должны быть связаны с выходными
Если делать pinning test, то можно и таким пользоваться
Ничто не мешает добавлять комментарии в тестовые данные в любом удобном виде. Тогда понятно, зачем нужна именно такая комбинация данных.
Можно поспорить, что считать хорошим или плохим тестом; с традиционными юнит-тестами я сталкивался как раз с тем, что в юнит-тесте воспроизводился кусок бизнес-логики, создающий объекты и подготавливающий данные, просто потому что это было легко. В итоге тест был плохой, потому что ошибка в бизнес-коде воспроизводилась и в тесте, и тест проходил на ура. Что мне ноавится в подходе с эталонными данными, это то, что код теста там гарантированно другой, и результат всегда нагляден (можно посмотреть, обсудить и прокомментировать в системе code review), и шансов скопипастить багу из основногл кода в тестовый сильно меньше из-за другой природы тестов.
Ничто не мешает добавлять комментарии в тестовые данные в любом удобном виде. Тогда понятно, зачем нужна именно такая комбинация данных.
Мне удобен вид, когда каждое данное представляет собой сонстанту. И вход связан с выходом. И лишние для текузего кейза данные вынесены в отдельное место и не дублируются.
то в юнит-тесте воспроизводился кусок бизнес-логики, создающий объекты и подготавливающий данные, просто потому что это было легко. В итоге тест был плохой, потому что ошибка в бизнес-коде воспроизводилась и в тесте, и тест проходил на ура.
Это устранение дублирование. Просто должны быть еще небольшое количество интеграционных тестов + тесты покрывающие само создание данных
Какие книжки по тестированию вы читали?
Согласен, ошибку в эталонном файле можно ненароком пропустить. И к тому же данный способ провоцирует на создание большего количества тестов, что приводит к большему количеству эталонных данных (которые все надо тщательно проверять). With great power comes great responsibility...
Это data driven tests и approval tests недостаток такой, что непонятно, из текста теста почему данные должны быть именно такими.
Дык approval tests же. Примерно то же самое. Есть еще intellitest который геренирует входные данные
Предполагается, что если в куче дискретных точек первая и вторая миплементации совпали, то все сделано по ТЗ.
Вычисление данных на бумажке в некоторых случаях (сложные структуры данных) сложно или просто невозможно. Пример: парсеры, где на входе и на выходе может быть объемный документ или дерево. если все делать руками, начинаются упрощения, попытки обойтись тестовыми данными поменьше и попроще. В случае же, когда вручную готовить нужно только входные данные, задача упрощается.
Работа со слепками данных позволяет автодокументировать работу как отдельных компонентов, так и всей системы, и выявлять такие ошибки, которые на бумажке не обнаружишь. Пример: функция должна выдавать некую структуру с полями A, B и C. На бумажке все хорошо, в тесте все хорошо — мы явно проверяем наличие этих трех полей и их значения. И все совпадает с ТЗ. А посмотрев на слепок данных вдруг оказывается, что с какого-то момента функция начинает еще возвращать и поле D. В ТЗ это не прописано, но такая ошибка может быть совсем не безобидной (приводить к утечке конфиденциальных данных, например).
Вычисление данных на бумажке в некоторых случаях (сложные структуры данных) сложно или просто невозможно. Пример: парсеры, где на входе и на выходе может быть объемный документ или дерево. если все делать руками, начинаются упрощения, попытки обойтись тестовыми данными поменьше и попроще.
Это и правильно — тестовые данные должны быть максимально независимы и понятны.
Иначе при падении тестов будет непонятно, какое требование нарушилось
Режим инициализации: тест производит вычисление выходных данных и сохраняет эти данные в файл-эталон
Режим тестирования: тест производит вычисление выходных данных, читает ранее сохраненные эталонные данные и сравнивает их; данные отличаются — тест провален.
Аналогичный подход применяется в библиотеках тестирования VCR, Betamax. Но там это делается для создания mock'ов над внешними вызовами, чтобы по время тестов не звать внешний сервис.
В вашем же случае, мне видится полезный кейс только в случае большого количества тестовых сценариев. Как пример, разрабываем браузер, берем определенную версию, натравливаем на 10 тыс сайтов, сохраняем их скриншоты. Берем следующую версию браузера, получаем новые скриншоты, сравниваем с предыдущими, выявляем разницу. Т.е. имеем условный черный ящик с нетривиальной логикой, хотим иметь оценку, насколько следующая версия сильно отличается от предыдущей.
- При тестировании юнитов на небольшом наборе изолированных и специально подготовленных тестов получается, по сути, обычное табличное юнит-тестирование, с тем лишь отличием, что данные находятся в файлах (со всеми плюсами и минусами хранения данных отдельно от тестов; см. data-driven testing и TableDrivenTests), и что эталонные данные не надо писать руками (но обязательно заверять и относиться к ним так, как если бы вы их писали руками).
- При тестировании больших кусков кода (типа проверки работы API и возвращаемых им стурктур данных) или даже приложения целиком (запускаем приложение с входными параметрами, получаем что-то на выходе, сравниваем выход) получаем интеграционное тестирование. В этом случае входные и выходные данные могут быть более объемными, чем при юнит-тестировании, так что их хранение во внешних файлах более оправданно, как и автогенерация эталонного вывода с последующей его заверкой.
- При использовании избыточного количества входных данных теста и незаверенных выходных данных получаем pinning tests (спасибо ApeCoder за правильную терминологию). В этом случае мы тестируем не правильность исходной работы приложения, а лишь фиксируем изменения его поведения (что можно делать как, например, отчет в рамках автоматической сборки билдов).
Остановился на таком формате тестов — тестовый скрипт, содержащий набор данных, настроек и названия отчетов. При первом прогоне собирались эталонные отчеты. Эталоны проверялись руками, или не проверялись вовсе (когда количество данных было запредельным и смысл был только в отлове изменений, суть есть регрессионное тестирование). При последующих запусках эталон и текущий отчет скармливались diff-у и при наличии изменений тест помечался как имеющий различия, но не упавший. Тест падал только при невозможности выпустить отчет или ввести данные.
Для ручного тестирования, вместо того чтобы выполнять ввод исходных данных и выпуск отчета, было проще написать новый тест (не содержащий эталонных данных), запустить его, подождать пару часов (думаете отчеты быстро выпускаются? И как только раньше руками тестировал...), получить результаты, проверить и добавить новый тест в базу тестов.
Рассмотрим такой простой кейс: нужно протестировать функцию к-ая валидирует некоторое значение и выдает список ошибок, если они есть.
Как протестировать ее используя ваш подход с учетом того, что сам порядок ошибок в списке не имеет значения?
git diff
легко обнаружить такие аномалии. Все изменения шаблонов, например, затрагивают одну и ту же строчку, а в одном эталоне картина изменений другая. Конечно, если какое-то изменение затрагивает очень много эталонных данных (сотни файлов), то шанс что-то пропустить сильно увеличивается. С другой стороны, по самому количеству изменившихся эталонов сразу видно, насколько сильно изменение кода затрагивает поведение системы, и что его надо тестировать тщательнее. То же самое и с code review. Представьте коммит, который включает изменение одного шаблона и коммит, в котором включены десятки изменившихся шаблонов. Понятно, что ко второму внимание всех остальных разработчиков будет более пристальным.
А пусть тесты сами себя и поддерживают