Почему я считаю TDD практикой чистого кода и как правильно ее применять

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

Но, с моей точки зрения, чистый код не может существовать без тестирования, и для меня единственный способ тестирования — применение TDD.

Что такое TDD (разработка через тестирование)?

Проще говоря, TDD означает, что вы напишете тест перед кодированием соответствующего производственного кода. Когда этот код будет готов и пройден ваш тест, выполните рефакторинг, а затем, когда ваш тест снова пройден, напишите новый тест.

Почему это важно?

  • Покрытие тестами поможет вам провести рефакторинг кода. Поскольку у вас есть тесты, чтобы выяснить, не нарушил ли ваш рефакторинг ничего, вы можете делать это по своему усмотрению. Вы можете рискнуть провести рефакторинг без риска, потому что он проверен!
  • Написание теста перед написанием производственного кода заставит вас написать только ту часть кода, которая вам нужна, без мертвого кода. Если вы ограничиваете объем кода, вы, очевидно, делаете его яснее, чище.
  • В TDD ваши тесты основаны на намерениях. Это означает, что ваши тесты будут описывать поведение вашего кода, независимо от деталей реализации. У этого есть два преимущества: во-первых, рефакторинг не будет ломать все ваши тесты каждый раз (поскольку тесты не знают о реализации). Во-вторых, ваши тесты будут служить живой документацией по коду.

Что, если я буду тестировать без подхода TDD?

  • Написание теста после кода заставит вас написать мертвый код. Вы будете думать больше, чем вам действительно нужно, когда будете кодировать. Предвидя какое-то условие или абстракцию, которые никогда не будут существовать в продакшене, таким образом усложняя код.
  • Когда вы пишете тест после, вы можете (и чаще, чем вы думаете) забыть о цели вашего кода. Таким образом, ваш тест будет охватывать не намерение, а техническое мышление. Когда ваши тесты становятся технически ориентированными, они, очевидно, становятся более зависимыми от детальной реализации.
  • Когда ваши тесты становятся бессмысленными, они становятся долгом. Они теряют аспект документации, потому что описывают техническую реализацию, а не намерение. Они также теряют связь с реализацией, это означает, что когда вы будете рефакторить свой рабочий код, вы сломаете слишком много тестов.
  • Ваши тесты могут получить хорошее покрытие кода, но они не будут охватывать цель. Таким образом, мутационное тестирование, вероятно, покажет вам, насколько слабы ваши тесты. (Мутационное тестирование — это метод, который изменяет некоторые условия в вашем коде, например, инструкция «==» станет «!=», поэтому, если ваши тесты по-прежнему проходят после этих изменений, ваши тесты являются просто покрытием, они не проверяют реальный код/намерение)

Как это работает?

Я сделал краткое описание потока TDD в первой части. И это основной смысл TDD.

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

Ваши тесты должны быть основаны на намерениях, например, «банковский счет не может быть отрицательным» в большей степени, чем «Банковский репозиторий не может уменьшаться больше, чем доступно».

Почему? В первом случае у вас есть критерии приемки, ориентированные на пользователя. Это поможет вам понять, что вы будете тестировать, и связать это с бизнес-правилом.

Во втором случае, хотя это означает то же самое, вы не понимаете делового смысла теста. Поэтому, если этот тест не пройдет позже, из-за рефакторинга, вы можете не понять, почему он не работает, как это исправить… Хуже того, в тесте говорится о репозитории и доступном количестве. Это похоже на детали реализации. Это не так уж плохо, но это, естественно, заставит вас использовать эти понятия в своем тесте, делая его зависимым от реализации (свойство Repository и AvailableAmount).

Что я должен проверить?

Простой ответ - все. В TDD вы не можете писать код, если у вас нет фейл-теста. За исключением рефакторинга, но рефакторинг не добавляет новых функций, это просто упрощение существующего кода.

Тестирование вариантов использования

Обычно я начинаю с варианта использования. Это имеет смысл, поскольку это описание функций. В чистой архитектуре вариант использования — это команда, отвечающая на потребность пользователя. Это намерение основано!

Ждать?! Это именно то, что мы ищем в TDD. Тест также основан на намерениях! Так что эта часть проще, вы просто пишете тест для всех критериев приемлемости, которые у вас есть для этих функций.

Возьмем пример с этой простой функцией: «Клиент хочет отправить банковский перевод».

Критерии:

  • Клиент должен быть клиентом нашего банка
  • На банковском счете клиента должно быть больше денег, чем он отправляет
  • С банковского счета клиента должна быть списана сумма перевода

Итак, наш первый тест будет выглядеть так:

Мы используем построитель контекста, разделяя детальную реализацию и тесты. Также рекомендуется сделать тест более вербальным. Читая этот код, каждый может понять, в каком состоянии будет выполняться этот тест. Итак, цель теста ясна!

Затем вы пишете производственный код. Вероятно, это команда с репозиторием интерфейса, которому предоставлен банковский счет клиента. Таким образом, ваши тесты будут внедрять некоторую фиктивную реализацию этого репозитория или использовать какую-либо библиотеку, например Substitute, для создания среды, описываемой тестом.

Хорошо, тест пройден! Поздравляю ! Поскольку это первый шаг, который мы написали, нам нечего рефакторить, но если мы думаем, что делаем это слишком быстро и слишком уродливо, пришло время рефакторинга! Когда тест снова пройдет, вы сделали!

Теперь напишите новый тест!

Как видите, мы добавили в мой контекст новый метод для указания текущей доступной суммы клиента. Конечно, по сравнению с первым тестом, на этот раз клиент уже существующий. Конечно, снова тест провален на данный момент! Итак, давайте напишем производственный код!

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

Мы добавили новый метод в объект «Счет», чтобы узнать, можно ли списать сумму. И наш тест снова зеленый!

Последний тест:

На этот раз контекст описывает существующего клиента с достаточным количеством денег. Я ожидаю, что клиентский счет должен быть дебетован. Это связанный производственный код:

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

Наши тесты еще зеленые! Поздравляю! Но не забудьте сделать свой тест таким же чистым, как и ваш производственный код! Итак, остался последний шаг!

Это снова небольшое изменение, но благодаря созданию новой переменной для имени ожидаемого значения цель теста становится более ясной.

Несколько слов о «конструкторе тестового контекста»:

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

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

Подводя итог, отметим два основных преимущества:

  • Более чистый тестовый код, тестовый код раскрывает намерение, как и название теста.
  • Тесты отделены от деталей производственного кода. Если какая-то реализация изменится, вам не нужно менять все свои тесты! Просто конструктор контекста.

Тестирование инфраструктуры

Чтобы протестировать свою инфраструктуру, вам просто нужно протестировать репозиторий интерфейсов, определенный в вашем домене! Поскольку именно контракт домена будет реализовывать вашу инфраструктуру, и поскольку вы не хотите, чтобы вас связывали с деталями реализации, вам просто нужно протестировать эти контракты!

Я не буду показывать «ритм», как это делаю в части прецедентов. Думаю, вы поняли, «как это работает». Итак, я расскажу о методах тестирования уровня инфраструктуры.

  • Насмешка над ответом инфраструктуры: например, ответ внешнего API или результат запроса к базе данных. На мой взгляд, лучший вариант в TDD. Поскольку на самом деле нам не нужны интеграционные тесты, нам нужны БЫСТРЫЕ тесты. Тесты, которые выполняются очень быстро, должны выполняться часто. Тесты, которые раскрывают намерение контракта интерфейса, а не проверяют, действительно ли доступна какая-либо база данных.
  • Выполнение тестов с помощью контейнера: используя некоторую библиотеку, например «контейнер для тестов», вы можете создать тестовую среду и применить свои тесты. Это довольно хороший вариант, тесты относительно быстрые, могут выполняться локально, если у разработчика есть экземпляр докера. И есть хороший аспект реального интеграционного теста.
  • Выполнение тестов в тестовой среде: это интеграционный тест! Я думаю, что проведение некоторых интеграционных тестов, чтобы проверить, например, нет ли у вас ошибок конфигурации, является хорошей практикой, но это не должно быть вашим способом проверить контракт интерфейса! Это медленно, вы можете не выполнить их при разработке, и это основано не на намерениях, а на технической основе.

Чтобы продолжить, смоделируйте ответ инфраструктуры, чтобы ваши тесты были быстрыми и содержательными. Вы можете добавить несколько «контейнерных тестов» для проверки определенного кода инфраструктуры. Как сложный запрос. Затем добавьте немного интеграционных тестов, просто чтобы убедиться, что все ваше окружение выглядит нормально (я говорю «посмотрите», потому что лучшие тесты окружения не могут гарантировать вам, что вы готовы к развертыванию).

Тестирование API/исполняемого файла:

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

Пример переразбивки тестов

Домен не является «хорошо протестированным», потому что он должен быть охвачен вариантом использования (который использует объект домена). Таким образом, тесты домена - это просто тесты для конкретного случая, когда ваш объект домена слишком сложен, чтобы его можно было протестировать/объяснить с помощью тестов вашего варианта использования.

Конечно, это пример перераспределения. Но вы понимаете глобальную идею позади.