Как писать код, который не ломается: гайд по TDD от эксперта Эйч Навыки

Владислав Гайденко, эксперт Эйч Навыки и бэкенд-разработчик в Авито, рассказывает, как TDD помогает избежать ловушек нестабильного кода, сократить время на исправление багов и контролировать процесс разработки.

1К открытий9К показов
Как писать код, который не ломается: гайд по TDD от эксперта Эйч Навыки

Разработка ПО редко идет без приключений: требования меняются, ошибки всплывают в самый ненужный момент, а исправления одного бага, как смещение таблицы в Word, может сломать вообще всё. К счастью, есть подход TDD (Test-Driven Development), который минимизирует эти риски.

Влад Гайденко, ментор Эйч Навыки и бэкенд разработчик в Авито, где он с командой разрабатывает платформу «Гарантии запчастей» в рамках Авито Авто, расскажет, как применять TDD без стресса и писать код, который не будет ломаться.

Что такое TDD

TDD (Test-Driven Development) часто считается крайне сложным подходом, но на самом деле все довольно просто. Представьте, что вы строите дом: вы же сначала нарисуете план, и только потом начнете строить, верно? В TDD все работает точно так же, только с кодом: сначала мы пишем тесты, описывающие, как должен работать наш код, а потом уже пишем сам код, чтобы эти тесты проходили.

Как писать код, который не ломается: гайд по TDD от эксперта Эйч Навыки 1

Другими словами, здесь полностью меняется логика разработки: обычно мы пишем код, потом тестируем и финалим баги. В TDD все идет от обратного — схема примерно такая:

  1. Определяем функциональность, которую нужно реализовать. Условно: вам нужно при запросе «300» возвращать «спартанцев».
  2. Под эту функциональность пишем тест — в нем вы описываете, как должна работать будущая программа. Спойлер: при запуске тест провалится, потому что тестировать пока нечего.
  3. Пишете самый простой (даже «глупый») код, после которого тест просто перестанет выдавать ошибку.
  4. Если тесты больше не падают, поздравляем, вы великолепны. Теперь можно делать рефакторинг кода и пилить «по красоте». 

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

Почему TDD работает

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

  • Как считать стоимость для разных типов товаров (легких, тяжелых и хрупких)
  • Как учитывать расстояние до адреса доставки
  • Как применять скидки и акции

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

Что это реально дает

Когда начинаешь работать с TDD, понимаешь, насколько это мощный инструмент. Ниже о его суперсилах.

Надежды как швейцарские часы

Допустим, через какое-то время нам нужно добавить новый тип доставки — курьером. Без тестов мы бы постоянно переживали: «А вдруг что-то сломалось?» С тестами же мы можем спокойно вносить изменения, зная, что если что-то пойдет не так, тесты нас предупредят.

Актуальная документация

Тесты становятся отличной «живой» документацией. Новый разработчик в команде? Просто покажите ему тесты, и он быстро поймет, как все работает. Это намного удобнее, чем читать многостраничные документы, которые часто устаревают.

Экономят время на поиске багов

Помните ситуации, когда нужно срочно исправить ошибку, а вы не знаете, где искать? С TDD все проще: написал тест, воспроизводящий проблему, исправил код, убедился, что тест проходит — и готово!

Где TDD реально полезен

Отлично подходит для:

  • Бизнес-логики: все эти CRUD-операции, валидации, обработка ошибок — самое то!
  • Утилитных модулей: математические операции, форматирование данных, работа с коллекциями

Где лучше подумать дважды:

  • Интеграция с внешними сервисами
  • Работа с файлами и сетью
  • Настройка окружения

С чего начинать TDD

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

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

Например, для задачи по добавлению нового типа товара в доставку с признаком акциз:

  • Моторные масла с типом акциз до 5 л включительно объема доступны для доставки
  • Тип доставки «Лошадью»
  • Логистическая компания «Рога и копыта»

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

Когда задача сформулирована с критериями приемки, разработчик вместе с QA-инженером могут начать создавать тест-кейсы прямо в Jira. Эти тест-кейсы будут отражать различные сценарии использования, которые должны быть протестированы.
Владислав Гайденкоментор Эйч Навыки

Например, для задачи по добавлению нового типа товара, тест-кейсы могут быть такими:

1. Проверить, что типы товара доступны для доставки в «Лошадью» в «Рога и копыта»:

  • моторные масла
  • с акцизой
  • объем (1, 2, 5 л)

2. Проверить, что типы товара недоступны для доставки «Лошадью» в «Рога и копыта»:

  • моторные масла
  • без акцизы
  • объем (1, 2, 5 л)

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

Очень важно, чтобы тест-кейсы, определенные в Jira, просматривались на встречах по оценки задач (PBR). Это позволяет:

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

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

В итоге, начав с правильной постановки задач в Jira и определения тест-кейсов, разработчики получают надежный фундамент для применения TDD. Это помогает создавать качественный код, который точно соответствует требованиям пользователей.

Как писать тесты еще лучше

Используйте принцип «Светофора»:

Как писать код, который не ломается: гайд по TDD от эксперта Эйч Навыки 2
  • Красный: пишете тест, он падает
  • Зеленый: пишете код, тест проходит
  • Рефакторинг: улучшаете код, тесты все еще зеленые

Делайте тесты читаемыми:

  • Давайте понятные названия
  • Структурируйте код теста
  • Используйте вспомогательные функции для часто повторяющихся действий

Вот пример плохого теста:

			func Test1(t *testing.T) {
    result := calculateDelivery(10, "fragile")
    if result != 150 {
        t.Fail()
    }
}

		

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

А вот хороший пример:

			func TestCalculateDelivery(t *testing.T) {
    tests := []struct {
        name       string
        distance   int
        itemType   string
        wantCost   int
    }{
        {"fragile item, 10 km", 10, "fragile", 150},
        {"standard item, 5 km", 5, "standard", 50},
        {"large item, 20 km", 20, "large", 300},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := calculateDelivery(tt.distance, tt.itemType)
            if got != tt.wantCost {
                t.Errorf("calculateDelivery(%d, %q) = %d; want %d", tt.distance, tt.itemType, got, tt.wantCost)
            }
        })
    }
}

		

Почему это хороший пример?

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

Типичные сложности и как с ними справиться

«Это же долго!»

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

«Не знаю, с чего начать»

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

«У нас legacy-код»

Начните с нового кода! Не пытайтесь сразу покрыть тестами весь старый код. Добавляйте тесты постепенно, когда прикасаетесь к старому коду для внесения изменений.

Подготовка проекта

Например, в Go есть отличные инструменты для тестирования. Я рекомендую:

  • Стандартный пакет testing для базового функционала
  • Testify — набор полезных ассертов и мок-объектов, лично используем на проекте
  • Ginkgo — BDD-стиль написания тестов
  • Встроенные инструменты для измерения покрытия кода:
			```go
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
```

		

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

Удачи в практике TDD! А если у вас остались вопросы — велком в комменты.

А вы используете TDD в своих проектах?
Да, это лучшее, что придумало человечество. Наконец-то избавились от миллиона багов
Ага, конечно, а еще что? 
Следите за новыми постами
Следите за новыми постами по любимым темам
1К открытий9К показов