Pull to refresh

Comments 29

Интересный подход, но приверженцы TDD в пролёте.
По-моему все это очень даже в духе TDD. Тесты никуда не уходят, их только создавать и поддерживать становится легче. Схема такая: пишем сначала тест, формирующий API бизнес-логики, и подготавливаем для него входные данные (тест не проходит); пишем основной код приложения; проверяем код через запуск теста в режиме инициализации и генерацию эталона; rinse & repeat. Можно, конечно, и эталоны генерить руками, только зачем? Главное понимать, какие данные должны быть на выходе (сформулировать для себя эталонный вывод теста), а формально файлик пусть компьютер создает. Ну и коммитить тест и результаты теста вместе с основным бизнес-кодом.
С одной стороны, звучит логично. С другой стороны, ошибиться при проверке данных легче, чем при написании их с нуля. Ошибки при ctrl+C/ctrl+V из этой оперы, например.
P.S. мне понравилась идея; просто проверяю её на прочность и ищу скрытые подводные камни.

Подводный камень — то, что получающйся тест плохой.


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

Если делать pinning test, то можно и таким пользоваться

Ничто не мешает добавлять комментарии в тестовые данные в любом удобном виде. Тогда понятно, зачем нужна именно такая комбинация данных.


Можно поспорить, что считать хорошим или плохим тестом; с традиционными юнит-тестами я сталкивался как раз с тем, что в юнит-тесте воспроизводился кусок бизнес-логики, создающий объекты и подготавливающий данные, просто потому что это было легко. В итоге тест был плохой, потому что ошибка в бизнес-коде воспроизводилась и в тесте, и тест проходил на ура. Что мне ноавится в подходе с эталонными данными, это то, что код теста там гарантированно другой, и результат всегда нагляден (можно посмотреть, обсудить и прокомментировать в системе code review), и шансов скопипастить багу из основногл кода в тестовый сильно меньше из-за другой природы тестов.

Ничто не мешает добавлять комментарии в тестовые данные в любом удобном виде. Тогда понятно, зачем нужна именно такая комбинация данных.

Мне удобен вид, когда каждое данное представляет собой сонстанту. И вход связан с выходом. И лишние для текузего кейза данные вынесены в отдельное место и не дублируются.


то в юнит-тесте воспроизводился кусок бизнес-логики, создающий объекты и подготавливающий данные, просто потому что это было легко. В итоге тест был плохой, потому что ошибка в бизнес-коде воспроизводилась и в тесте, и тест проходил на ура.

Это устранение дублирование. Просто должны быть еще небольшое количество интеграционных тестов + тесты покрывающие само создание данных


Какие книжки по тестированию вы читали?

Согласен, ошибку в эталонном файле можно ненароком пропустить. И к тому же данный способ провоцирует на создание большего количества тестов, что приводит к большему количеству эталонных данных (которые все надо тщательно проверять). With great power comes great responsibility...

Это, разумеется, data driven tests, но только которые сами генерят выходные данные — в этом основная суть подхода.

Дык approval tests же. Примерно то же самое. Есть еще intellitest который геренирует входные данные

Да, approval tests — это тот же принцип.

UFO just landed and posted this here

Да, очень-очень близко. Спасибо за ссылку!

Мне всегда казалось, что основная суть тестов это имплементация ТЗ вторым независимым путем. Именно это дает уверенность в правильности программы и именно это, мне кажется, имел ввиду ApeCoder

Генерировать данные тестом — это валидно для отслеживания изменения поведения, но не более.
Второй раз бизнес-логику в тестах все равно никто не пишет. Все выливается в итоге в подготовку fixtures, запуск куска основного бизнес-кода и в сравнение каких-то параметров выходных данных (при котором легко что-то забыть сравнить). Здесь же сраниваются снэпшоты данных целиком. Да, они генерятся тестами, но после этого их надо проверять, иначе нет смысла все это затевать. Но сам код теста (сравнение снэпшотов данных) как раз по определению отличается от кода бизнес-логики, и чтение готового снэпшота данных, проверенного и отвечающего условиям ТЗ, и дает этот второй независимый путь.
Вторая имплементация ТЗ <> второе написание бизнес логики. Когда ты готовишь тест ты придумываешь входные данные, потом берешь ТЗ и на бумажке вычисляешь выходные данные. Это и есть вторая миплементация ТЗ для конкретной (дискретной) точки.

Предполагается, что если в куче дискретных точек первая и вторая миплементации совпали, то все сделано по ТЗ.
Здесь все так же, за исключением того, что не нужно руками сериализовать выходные данные. Достаточно их сгенерировать и заверить.

Вычисление данных на бумажке в некоторых случаях (сложные структуры данных) сложно или просто невозможно. Пример: парсеры, где на входе и на выходе может быть объемный документ или дерево. если все делать руками, начинаются упрощения, попытки обойтись тестовыми данными поменьше и попроще. В случае же, когда вручную готовить нужно только входные данные, задача упрощается.

Работа со слепками данных позволяет автодокументировать работу как отдельных компонентов, так и всей системы, и выявлять такие ошибки, которые на бумажке не обнаружишь. Пример: функция должна выдавать некую структуру с полями A, B и C. На бумажке все хорошо, в тесте все хорошо — мы явно проверяем наличие этих трех полей и их значения. И все совпадает с ТЗ. А посмотрев на слепок данных вдруг оказывается, что с какого-то момента функция начинает еще возвращать и поле D. В ТЗ это не прописано, но такая ошибка может быть совсем не безобидной (приводить к утечке конфиденциальных данных, например).
Вычисление данных на бумажке в некоторых случаях (сложные структуры данных) сложно или просто невозможно. Пример: парсеры, где на входе и на выходе может быть объемный документ или дерево. если все делать руками, начинаются упрощения, попытки обойтись тестовыми данными поменьше и попроще.

Это и правильно — тестовые данные должны быть максимально независимы и понятны.


Иначе при падении тестов будет непонятно, какое требование нарушилось

В теории — правильно (иметь минимально достаточный объем тестов, полностью покрывающий все сценарии). На практике могут быть случаи, когда проблема может проявиться только на каком-то особом наборе данных. Так что я считаю, что лучше поймать ошибку так (с помощью избыточных тестов), чем не поймать ее вообще. От падения такого теста уже можно плясать и смотреть, какого максимально независимого и понятного теста не хватает.

Да, в качестве небольшого количества тщательно подобранных тестов это можно использовать

Проверка уже существующих данных, которые на 99% правильные не так эффективна, как независимый расчет без подсказок.
Режим инициализации: тест производит вычисление выходных данных и сохраняет эти данные в файл-эталон
Режим тестирования: тест производит вычисление выходных данных, читает ранее сохраненные эталонные данные и сравнивает их; данные отличаются — тест провален.

Аналогичный подход применяется в библиотеках тестирования VCR, Betamax. Но там это делается для создания mock'ов над внешними вызовами, чтобы по время тестов не звать внешний сервис.

В вашем же случае, мне видится полезный кейс только в случае большого количества тестовых сценариев. Как пример, разрабываем браузер, берем определенную версию, натравливаем на 10 тыс сайтов, сохраняем их скриншоты. Берем следующую версию браузера, получаем новые скриншоты, сравниваем с предыдущими, выявляем разницу. Т.е. имеем условный черный ящик с нетривиальной логикой, хотим иметь оценку, насколько следующая версия сильно отличается от предыдущей.
Подход можно использовать для реализации разных типов тестов:
  • При тестировании юнитов на небольшом наборе изолированных и специально подготовленных тестов получается, по сути, обычное табличное юнит-тестирование, с тем лишь отличием, что данные находятся в файлах (со всеми плюсами и минусами хранения данных отдельно от тестов; см. data-driven testing и TableDrivenTests), и что эталонные данные не надо писать руками (но обязательно заверять и относиться к ним так, как если бы вы их писали руками).
  • При тестировании больших кусков кода (типа проверки работы API и возвращаемых им стурктур данных) или даже приложения целиком (запускаем приложение с входными параметрами, получаем что-то на выходе, сравниваем выход) получаем интеграционное тестирование. В этом случае входные и выходные данные могут быть более объемными, чем при юнит-тестировании, так что их хранение во внешних файлах более оправданно, как и автогенерация эталонного вывода с последующей его заверкой.
  • При использовании избыточного количества входных данных теста и незаверенных выходных данных получаем pinning tests (спасибо ApeCoder за правильную терминологию). В этом случае мы тестируем не правильность исходной работы приложения, а лишь фиксируем изменения его поведения (что можно делать как, например, отчет в рамках автоматической сборки билдов).

Реализовывал схожую систему, но не для юнит тестирования, а для функционального (чтобы не соврать с терминологией — программа является черным ящиком, тесты общаются с ней исключительно через UI). Необходимо было протестировать множество отчетов на разных наборах данных.

Остановился на таком формате тестов — тестовый скрипт, содержащий набор данных, настроек и названия отчетов. При первом прогоне собирались эталонные отчеты. Эталоны проверялись руками, или не проверялись вовсе (когда количество данных было запредельным и смысл был только в отлове изменений, суть есть регрессионное тестирование). При последующих запусках эталон и текущий отчет скармливались diff-у и при наличии изменений тест помечался как имеющий различия, но не упавший. Тест падал только при невозможности выпустить отчет или ввести данные.
Для ручного тестирования, вместо того чтобы выполнять ввод исходных данных и выпуск отчета, было проще написать новый тест (не содержащий эталонных данных), запустить его, подождать пару часов (думаете отчеты быстро выпускаются? И как только раньше руками тестировал...), получить результаты, проверить и добавить новый тест в базу тестов.
Такой же подход применяется при тестировании олимпиадных задач. Пишется модельное решение и тестируется вручную. Затем генерируются тесты (генератором), и правильные ответы (модельным решением). После этого, результаты, полученные с помощью модельного тестирования, сравниваются с разультатами участника при помощи чекера (а в простейшем варианте при помощи diff-а).

Рассмотрим такой простой кейс: нужно протестировать функцию к-ая валидирует некоторое значение и выдает список ошибок, если они есть.
Как протестировать ее используя ваш подход с учетом того, что сам порядок ошибок в списке не имеет значения?

Если порядок в списке не имеет значения, то список нужно отсортировать, чтобы выход был стабильным. Или (если мы говорим конкретно о Go-реализации), положить ошибки в map[string]string — в этом случае при сериализации в JSON ключи будут идти в сортированном порядке автоматически.
Имхо, самая большая опасность при таком подходе — в человеческом факторе. Предположим, что у нас есть какой-то сервис, возвращающий UserInfo — структуру из, скажем, пяти полей. Предположим также, что нам понадобилось добавить шестое поле. Добавляем и… все тесты падают — надо добавить новое поле в эталонные данные. И вот тут-то и возникает человеческий фактор: если тестов много, можно легко не заметить, что в одном из эталонов кроме добавления нового поля еще и изменилось значение одного из старых полей… и «исправить» эталон на неверный.
Согласен, опасность такая есть (как и при любом другом подходе к тестированию, в котором есть человеческий фактор). В моей практике такого пока не случалось; все эталонные данные, которые я сохраняю в файлы, всегда структурированы, поэтому при git diff легко обнаружить такие аномалии. Все изменения шаблонов, например, затрагивают одну и ту же строчку, а в одном эталоне картина изменений другая. Конечно, если какое-то изменение затрагивает очень много эталонных данных (сотни файлов), то шанс что-то пропустить сильно увеличивается. С другой стороны, по самому количеству изменившихся эталонов сразу видно, насколько сильно изменение кода затрагивает поведение системы, и что его надо тестировать тщательнее. То же самое и с code review. Представьте коммит, который включает изменение одного шаблона и коммит, в котором включены десятки изменившихся шаблонов. Понятно, что ко второму внимание всех остальных разработчиков будет более пристальным.
Sign up to leave a comment.

Articles