Pull to refresh

Comments 29

Как ни странно TDD мне лучше всего помогало в простых проектах типа простого CMS, или небольшого интернет магазина и т.п. мелочам.

В последнем рабочем проекте в начале пытались использовать TDD в чистом виде. Выводы довольно банальные и в то же время неоднозначные. Немного про особенности проекта — в основном логика простая, но сущности довольно сложные — множество древовидных структур данных до 4-5 вложенными узлами и до 10-15 свойствами в ветке. Сущности генерируются и сохраняются в кеше, некоторые сущности поставляется внешним (тоже находящимся в разработке) сервисом, а некоторые из них необходимо впоследствии сохранять и считывать в БД. Также структуры передаются и принимаются через несколько веб-сервисов. Во время разработки очень часто (раз в неделю — две недели) менялись требования и интерфейсы как на выходе так и сервисов поставляющих данные. (Это изначально было известно — потому лучшим решением казалось Agile с TDD)

Из плюсов:
TDD очень часто вынуждал писать качественный код — приходилось вместо быстрой имплементации функции делать отдельные модули и вместо использования DateTime.UtcNow создавать и использовать отдельные интерфейсы для работы со временем и т.д. и т.п.

Из минусов:

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

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

На одну функцию по конвертированию структуры приходилось до 30-40 тестов валидации корректности конвертации и проверки ошибок (потом сократили до 4-5 «плохих» тестов — которые содержали множество Assert-ов)

Львиная доля ошибок и проблем приходилось на слой с БД где юнит тесты в чистом виде очень сложно было применить, тесты работали ОЧЕНЬ медленно и очень много ошибок юнит тесты не способны были обнаружить. (так как количество возможных ошибок переваливало за несколько дестяков и заранее невозможно было все предусмотреть). Так как структура данных менялась координально множество раз — поддерживать юнит тесты для БД было очень сложно и затратно.

В итоге, так как требования от заказчика менялись ОЧЕНЬ часто и очень часто меняли интерфейсы сервисов поставляющих данные — в какой то момент, поддержка, рефакторинг и сопровождение тестов стало занимать 95% рабочего времени и тем не менее это никак не спасало от множества багов (для которых создавалось еще больше тестов — которые устаревали при очередном изменении). По этой причине постепенно перешли от TDD к BDD и интеграционным тестам — когда тесты писали не на отдельные функции а на поведение системы и целые сценарии + множество мелких юнит тестов (но гораздо меньше чем при полном TDD) — проверяющих основную функциональность модулей. Интеграционные тесты обнаруживают всевозможные виды ошибок, которые возникают уже собранной системе, как и на машине разработчика, так и на развернутой системе, которые невозможно было обнаружить при тестировании «идеальных условий» в юнит тестах. (такие как падение сервиса поставщика данных, неправильная настройка БД и т.п.). Скорость разработки (изменение и добавление новых функции и исправления найденых ошибок, рефакторинга тестов) повысилось в 5-10 раз.

Угу, схожий опыт. Добавлю от себя: модульным тестом лучше протестировать только изолированные модули. Но все равно останутся не изолированные модули. Ну у примеру, слой сервисов как ни крути зависит от слоя dao, а введение интерфейса ничего принципиально не меняет (с точки зрения корректности важна именно взаимосвязь реальных имплементаций). Искусственно изолировать сервисы от dao моками не только бесполезно, но и вредно. Интеграционные тесты рулят.
Помоему, изначально Вы писали тесты функций, а не поведения. В этом и проблема.

У автора статьи есть фраза:

введение иерархий: методы инкапсулируются в классы, классы в пакеты, пакеты в библиотеки

Каждая из этих частей — есть модуль, пусть и разного уровня. Тесты, по моему, надо писать на тот уровень, где строятся требования — не выше (будет сложно их делать), не ниже (постоянно будет происходить описанный эффект домино).

А «до 30-40 тестов» вообще похоже на coverage 100%
Опять статья про тестирование в которой перемешаны понятия тестирования в целом, модульного тестирования, интеграционного тестирования и самого TDD. ТDD это test-first методология. Любой разработчик обязан писать тесты. Но делать в TDD стиле это не обязательно. На самом деле TDD имеет недостатки. Классический (mockist-style) подход порождает кучу тестов, которые очень сильно зависят от интерфейсов (и от реализации).
По большому счету многие из этих тестов должны быть просто удалены, поскольку в будущем их хрукость только мешать будет. Фактически единственное преимущество TDD это модульность, которую он пораждает. Но тут тоже не все так гладно, с одной стороны такой подход защитит вас от монолитных классов и совсем плохой архитектуры, но с другой стороны хороший дизайн от TDD сам не появится. TDD поощряет single responsibility principle, но тут легко дойти до крайности и обнаружить, что модель предметной области забита классами-сервисами без состояния. На мой взгляд о TDD лучше отказаться в пользу BDD (причем c широким циклом: пишем приемочный тест, кодим пока он не прошел). Модульные тесты при этом используются прагматически: тестируем ими действительно сложную логику + то что на интеграционном уровне простестировать сложно. В общем покрытие вашего кода должно быть высоким, но обеспечивать оно должно интеграционными тестами, они и регрессионность обеспечат и позволят вам задуматься о фичах с точки зрения требований, да и проект они неплохо документируют. Огромный минус — медленно. Но что уж сделаешь, идеала не бывает.
Разработка на основе только приёмочных тестов, имхо, мало отличается от разработки на основе спецификаций/ТЗ в плане влияния на архитектуру. То есть они никак не влияют.

Классов без состояний в домене как-то получается избегать, возможно из-за активного использования DDD. Можно пример, как можно дойти до такой крайности?

P.S. А вот принципиального отличия BDD от TDD так и не понял :(
BDD — это, фактически, эволюция TDD. В современном понимании (Rspec + Cucumber) с внешним и внутренним циклами пользовать не пробовали, но вот некоторые практики очень успешно применяем в xUnit среде. Хорошая статья на тему.
Разработка на основе приемочных тестов влияет на процесс разработки. Например, вы после каждого изменения обновляете в браузере, смотрите, что так, а что не так. А здесь – вы разрабатываете с закрытым браузером (и лучше его не открывать, пока не будет фича доведена до конца). И тест вам сам подсказывает где именно затык и до какого этапа в разработке вы дошли и какой следующий шаг. Можете посмотреть каст на тему cucumber.
Тут есть принципиальное отличие, но оно не в коде (конкретной реализации теста), а в области мышления.
С TDD вы думаете «мой код должен вернуть тру при таких-то входных параметрах» и на основе этого пишете тесты.
При BDD вы думаете «мой код должен выполнить такой-то юз кейс» (или даже «мой код должен решить такую-то бизнес задачу пользователя»).
Путаница в этих понятиях, я думаю, связана с тем, что многие говорят о BDD в контексте юнит тестирования. Но бизнес требования — это высокоуровневая вещь и чем тесты ближе к реализации (а ближе, чем юнит тесты и не бывает), тем сильнее разница подходов размывается. А многие считают, что раз используют Rspec и пишут it should значит это BDD, что если их тесты тестируют поведение (а не интерфейсы и реализацию) класса (что, конечно, хорошо) — то это BDD. BDD это когда конечный пользователь читает название теста, и говорит: да — это то, что я хочу от программы.
Насчет влияния на архитектуру… Я считаю что влияние на архитектуру — это сверхпереоцененное качество тестов. Это их побочное явление. Главное в тестах это — отловить регрессии, служить эталоном верного поведения кода. Тогда они развяжут вам руки при рефакторинге и дадут возможность экспериментировать с дизайном. TDD же намертно вбивает один дизайн (закрепляя его на уровне тестов интерфейсов и протоколов взаимодействия) еще до написания кода. Это будет не самый плохой на свете дизайн (вот именно это превозносят сторонники TDD). Но этот дизайн будет очень далек от наилучшего (хотя бы потому, что еще не написав никакого кода, вы не понимаете предметную область). TDD будет лучше работать если вы напишете тесты, напишите код, а потом сотрете нафиг большую часть тестов, а другие напишите нормально (то есть не так близко к реализации, в стиле тестирования поведения). Но ведь так никто не делает. Подсознательно сложно заставить себя стереть тест. В TDD не хватает обратной связи код -> тесты…
Еще один топик на хабре, где автор путает TDD и юнит-тестирование…
Вот второй абзац:

При взгляде новичка на тесты сразу возникает вопрос: а зачем вообще писать лишний код? Вроде как преимущества TDD никто не отрицает, но находятся какие нибудь причины: «да, я слышал что TDD полезен в больших проектах, но у нас проект маленький», «в нашем проекте слишком много изменений, поэтому тесты для нас слишком большая обуза» и так далее. Попробую рассказать как модульные тесты помогают мне в работе и поделиться опытом использования.

Из него ясно, что для автора TDD == писать_тесты.
Ещё один «плюс» который экономит время (особенно если это например Java-разработка) — «отлаживаться» быстрей в окружении автотеста (неважно насколько он «большой», но чем ближе он к модульным — тем быстрей): не надо запускать всю систему, не надо вручную проходить все шаги работы с системой чтобы обеспечить вызов нужного метода в нужном контексте.
Я на проекте встречаюсь с таким проблемами при написании юнит тестов (мы используем Moq):
1. Тестируемый метод (ТМ) прост для понимая, но он использует другой метод с глубиной переходов в другие методы других модулей со сложной логикой затрудняет тестирование. Особенно если метод статический. Приходится реализовывать всю инфраструктуру.
2.Тестируемый метод содержит другие приватные методы класса (логика фильтра оказалась в методе репозитория), которые вызывают обращение к базе. Обращение не замокать. Приватный метод мешает
3.Логика такова, что тестируемый метод подготавливает данные из 7-10 таблиц. Если чуток поменялась структура данных, все тесты сразу упадут. Их восстановление занимает уйму времени и как правило приводит к написанию заново тестов.

Пожалуйста, помогите решить эти проблемы практическими советами.
Не используйте моки :) Вместо них поднимайте БД в памяти и тестируйте интеграционно. Я люблю писать тесты на целые фасады.
Не использовать моки?! Так это же и есть юнит-тестирование в своей сути. Поднять БД это значит реализовать для 10 таблиц ещё 20-30 связей! Это ненужная работа, на мой взгляд.
К тому же это ни как не решает описанных мной проблем 1,2,3.
Если у вас класс работает с 10 таблицами — это проблема, и решать стоит ее. Любой проект, разрабатываемый по TDD, отличается от остальных большим количеством маленьких классов (до 200 строк) с миниатюрными методами. И вполне очевидно, почему так.

Моки разумно использовать на начальной стадии разработки, когда вам еще нечего предложить классу, кроме мока: интерфейс есть, а реализации нет. Чрезмерное увлечение моками, особенно в отсутствии функциональных тестов, обычно ничем хорошим не заканчивается.

Что касается БД: зачем тестам знать, где и как ваш класс хранит данные? Способ хранения — это детали реализации, а в тестах они раскрываться не должны. Именно поэтому тесты у вас получаются хрупкими.
т.е. если у меня есть некий алгоритм, который подразумевает обращение к различным репозиториям:
получили платёж, получили его тип, получили его отрпавителя, его счёт, получили что-то ещё — вот уже 4 объекта. Да, это всё 4 класса и у них 4 разных репозитория, но все эти репозитории я использую в куске бизнес логики. Что тут можно разделить? Далее эти поля присваются, сраниваются и выполняются шаги алгоритма. Выносить (1+2*3) из формулы ((1+2*3)*4+5)? или я не понял вас.
Таблица у нас конечно не 10, их порядка 100, может больше. Один бизнес объект, как платёж может связи иметь прямы с 10-15 таблицам, а те в свою очередь ещё по 10 связей. И это при нормализованной базе данных.

Если бы не моки, то заполнить данные для каждого нового тест-класса нужно было бы по 8-12 часов: учесть все связи not null, сгенерировать объекты и т.д.
Фиг его знает, по-моему лучше замокать. Так быстрее новые тесты пишутся и не лень их делать не по 3-4 на метод, а по 10.

Мы у себя отказались от моков, а так же от от «дамповых» фикстур т.е. когда данные тем или иным образом напрямую добавляются в БД. Тестовые данные создаем при помощи реальных объектов. Например, в тестах хозяйственной операции проводки создаются просто через класс проводок.

Если вам для какого-нибудь калькулятора или процессора нужен платеж, отправитель, счет — создайте в сетапе эти объекты да используйте в тестах. Чтоб избежать дублирования, связанного с инициализацией тестовых данных, воспользуйтесь паттерном Mother Object либо базовым тестом для группы тестов.

В общем, попробуйте в тестах абстрагировать от БД и работать с данными через классы модели. Интерфейсы довольно постоянны, а реализация меняется очень часто. Поэтому тесты, которые завязаны на реализацию, очень хрупкие. Кстати, когда мы активно использовали моки, они часто хотя бы немного знали о реализации настоящих классов, что тоже только добавляло хрупкости тестам. Насколько мне известно, это проблема, с которой сталкивались не только мы.
Вижу, что я не получил ответа на свои вопросы, потому что те кто отвечают сами привержены классического подхода с кучей всех его проблем, которые решают моки. С мок — фреймворками я разработку тестов ускоряю раз в 5 и то приходится.
А то что вы говорите, если я правильно понял про создание объектов в памяти — и есть самописные моки, которыми нужно заполнять таблицы в памяти, а потом из них выборку делать… так еще и писать в 2 раза больше обращений к данным через тот же linq.

Я наоборот пришёл к мок-фреймворку от описанного вами подхода по той причине, что на реально большом проекте связи эти построить и держать их логику в голове практически не возможно. Более опытные товарищи помогли с переходом. Может это только для меня, но я так не думаю.
Это позволяет тестить только логику, без создания и выборки объектов где- либо. Я просто говорю, что метод Repository.Get вернет мне такой-то объект, если ему передать такие-то данные и всё.
А вот тестирование методов репозитория, связей и т.д., это уже другой набор тестов — интеграционный. Там база, сессия, ролбек и все такое.

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

Кажется, вы не поняли ни что советовал я, ни что советовал bendingunit22.

Не нужно ничего руками заполнять в базе. Любое состояние к которому в принципе может прийти БД достигается вызовом бизнес-методов. Все записи, связи и тому подобное должны создаваться вашим рабочим кодом и думать и помнить обо все не нужно.
С вставкой фикстур в БД все верно пишете.

А по поводу моков — не всё же целесообразно тестировать без них. Ну класический DI пример про логгер, нет смысла тестировать вывод на принтер, достаточно видеть что класс работает правильно вплоть до вывода. Ну и кроме этого, ведь моки помогают выявить/устранить высокую связанность. Или я заблуждаюсь?
ну выявлять недостатки архитектуры они очень хорош умеют.
>нет смысла тестировать вывод на принтер, достаточно видеть что класс работает правильно вплоть до вывода
так и делают мок фреймвокрки. всё вплоть до вывода метода репозитория (ну или команды write из примера)
Я писал о моках в контексте проблем, описанных в первом комментарии ветки (т.е. не следует читать «мы у себя отказались от моков» как «мы у себя вообще отказались от моков» :)

Что касается сквозного функционала, вроде упомянутого вами логгера, декораторов, цепочек фильтров и т.д. — тут использование моков, конечно, более чем разумно. Ну и если с помощью инверсии зависимостей пытаться устранить излишнюю связанность в коде, то и в тестах следует придерживаться этого же принципа и, как верно было подмечено, использовать моки.
кстати, хоть я не решил пока проблему со статическими методами, но нашёл корень зла, который очень хорошо описан в этом топике (открыл так сказать для себя истину :))
wiki agiledev ru/doku php?id=tdd:tests_affect_architecture:static_calls

и более научно:
blog byndyu ru/2009/12/blog-post html
(точки доставить в ссылках)

PS Очень советую почитать всем, в том числе и тем кто попробовав моки от них отказался. Это примеры того, когда проблема не в моках а в незнании принципов.
Правильный ресурс читаете, хотел написать комментарий с ссылкой на него :)

Кстати, Павел и Сергей, основатели эджайлдева, еще в 2005 году читали отменные доклады по XP и TDD на phpconf, лично я очень многое узнал именно благодаря им. Жаль, что у них поменялись интересы в жизни :(

По делу: если вы нашли время для теории, почитайте там же статьи "Запахи тестового кода" (Чрезмерное доверие мокам, Недостаточное доверие мокам) и "Мок-объекты в модульном тестировании. Как избежать проблем". Там, в принципе, более детально описано все то, что вам писали в комментариях.
>Так это же и есть юнит-тестирование в своей сути

Тут выше справедливо заметили, что модульные тесты не всегда уместны. Изоляция ради изоляции это борьба с ветряными мельницами. Почитайте Фаулера: martinfowler.com/articles/mocksArentStubs.html

>Поднять БД это значит реализовать для 10 таблиц ещё 20-30 связей!

Не понимаю что значит «реализовать»? У вас же есть рабочая схема БД? Вот и используйте ее для базы в памяти.

>К тому же это ни как не решает описанных мной проблем 1,2,3.
Ну как же? Давайте по пунктам. Представим что будет, если вы выкините моки и будете тестировать интеграционно.

>1… Приходится реализовывать всю инфраструктуру.

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

>2… Обращение не замокать. Приватный метод мешает

Нету моков, нету проблемы — ничего не нужно мокать.

>3… Их восстановление занимает уйму времени и как правило приводит к написанию заново тестов.

Не понял честно говоря до конца проблему. Я лично делаю так: я использую ORM и схема генерится по моделям и накатывается на базу в памяти при старте тестов. Соответственно про рефакторинге моделей и кода обработки схема автоматически поменяется. Конечно, sql запросы нужно будет ручками подправить и тесты тоже. Но переписывать тесты 100% не придется.
Практика показывает, что в 90% случаев, если возникают сложности с написанием теста — проблема в архитектуре. Часто решить проблему помогает выделение класса.
>>1. Упрощение поддержки кода
>>Много воды…
>>Советовать всегда писать тесты не буду.
>>Универсального метода я не вижу.

>>2. Четкое разделение структуры на модули
>>Бла бла бла…
>>Проблема сложная, однозначного решения я не вижу

Вы рассказываете лишь о проблемах TDD, не предлагая при этом решений этих проблем.
О чем статья?
Sign up to leave a comment.

Articles