Разделы портала

Онлайн-тренинги

.
Уроки TDD глазами тестировщика
22.04.2026 00:00

Автор: Арун Вишуанатан (Arun Vishwanathan)
Оригинал статьи
Перевод: Ольга Алифанова

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

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

Отличный опыт с разработкой через тестирование

Первые впечатления

Сначала TDD казался мне нелогичным — противоположным традиционному подходу, когда сначала пишется код, а тестирование проводится позже.

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

Преимущества для планирования тестов и стратегии

Команда занималась планированием тестов и стратегией тестирования уже на раннем этапе цикла выпуска.

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

Такое планирование также способствовало концепции Red-Green-Refactor в TDD, которая заключается в следующем:

  • Red: написать тест, который изначально будет падать и определять желаемое поведение
  • Green: написать ровно столько кода, чтобы тест прошел
  • Refactor: улучшать код, сохраняя успешное выполнение всех тестов

Время и понятность

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

Это изменило тон проекта с реактивного на проактивный. Мы не просто реагировали на изменения функциональности в последнюю минуту; напротив, мы активно формировали систему, держа в уме четкие, тестируемые результаты.

Надежная документация решает

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

Возможности для плодотворного сотрудничества

TDD требует эффективного взаимодействия, и наша команда с энтузиазмом обсуждала важные вопросы, способствуя общему пониманию целей дизайна и качества. Мы могли распределять рабочую нагрузку, особенно когда знание технических деталей в команде было на высоте. Разработчики не придерживались позиции «Я напишу весь код, а тестировщики могут проверять потом». Ровно наоборот.

Проблемы тест-ориентированной разработки в условиях сжатых сроков

Перейдем к моему опыту на текущей работе в компании FAANG. Здесь основной акцент делается на быструю реакцию на конкуренцию и выпуск коммерчески жизнеспособных продуктов.

Тут я заметил, что хотя концепция TDD могла бы быть применена, она сталкивалась бы с рядом трудностей:

Частые изменения функциональности и высокая скорость разработки препятствуют внедрению TDD

Изменения функциональности в нашей команде происходят очень быстро. Людей подталкивают к быстрому продвижению функций. Разработчики открыто сопротивлялись внедрению TDD: работа с тестировщиками над тест-ориентированным проектированием функций воспринималась как «замедление» разработки. Соотношение сил и ценности подвергалось командой сомнению. Вместо этого разработчики просто пишут несколько unit-тестов, чтобы проверить свои изменения перед слиянием, и это позволяет пайплайну двигаться быстро.

Как выяснилось, около 80 процентов функциональности продукта можно было протестировать после разработки, и это считалось достаточным.

Функциональность при меняющихся и нестабильных требованиях

Одной из проблем, с которой мы столкнулись при использовании TDD, были изменения требований к функциональности в процессе разработки.

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

Это показало, что хотя TDD способствует ясности на раннем этапе, оно также может потребовать значительной переработки тестов при изменении предположений.

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

Хотя проблема частых обновлений не уникальна для TDD, здесь она усиливается и требует итеративного подхода. Команда разработки не была настроена на создание поведенческих тестов для нестабильных проектов только для того, чтобы потом возвращаться и перерабатывать их.

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

Частые изменения из-за сложных зависимостей

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

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

Требования к интеграционному тестированию

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

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

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

Разработчики не полностью понимали процессы TDD

Хотя разработчики и писали юнит-тесты, вначале они создавали код традиционным образом. Время на изучение и эффективное использование TDD воспринималось как помеха, и разработчики, естественно, не хотели рисковать ценным временем.

Когда разработчики не знакомы с TDD, они могут неправильно понять его основной процесс Red-Green-Refactor. Пропуск или неправильная реализация любого этапа может привести к неэффективным тестам. Именно так было в нашей команде. Вместо того чтобы создавать тесты, определяющие ожидаемые результаты для определённых граничных случаев, попытки сосредотачивались на чрезмерно упрощённых сценариях, которые не покрывали реальные проблемы с данными.

Баланс между TDD и традиционным тестированием

В ситуациях, подобных продукту в FAANG, кажется естественным и очевидным вернуться к традиционному подходу: сначала писать код, а затем тестировать. Хотя это прагматичный подход, у него есть свои сложности. Например, график тестирования и тестовая матрица должны быть тесно согласованы с быстрыми изменениями функционала, чтобы проблемы выявлялись сразу, на этапе разработки… а не клиентом в продакшене.

Так возможно ли усидеть на обоих стульях?

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

Хотя TDD может не дать официального подтверждения полного покрытия качества, оно помогает мыслить с точки зрения пользователя и начинать проектирование «от печки».

Начинайте планировать и обсуждать на ранних этапах процесса

На начальных стадиях планирования функциональности обсуждение принципов TDD может значительно повлиять на качество дизайна. Для этого требуется тесное сотрудничество между разработчиками и тестировщиками. Необходим кооперативный настрой и готовность эффективно исследовать практику.

Используйте гибридные подходы

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

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

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

Признавайте ценность традиционных пайплайнов интеграционного тестирования

По мере приближения сроков релиза традиционные методы тестирования становятся критически важными. Установление ночных, еженедельных и ежемесячных пайплайнов — охватывающих юнит-, системное и интеграционное тестирование — обеспечивает надёжную страховку. Высокая частота изменений требует внимательного контроля, чтобы выявлять регрессии в системе и оценивать их влияние. Особенно во время код-фриза: тогда традиционное интеграционное тестирование будет вашей последней линией защиты.

Автоматизируйте тестирование по максимуму

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

Множество моих инструментов для тестирования основано на идеях кода, полученных с помощью AI.

TDD на базе AI набирает популярность, но мы всё ещё далеки от надёжного и широкого применения генеративного искусственного интеллекта для тестирования.

В заключение

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

Дополнительная информация