TDD в Java с нуля: как тесты меняют код и спасают нервы

Онлайн-тренажер Python для начинающих

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

Начать курс

Почему TDD — это не про «написать тесты», а про «думать иначе»

Представьте, что вы строите дом. У вас есть чертежи, материалы и бригада. Но вы начинаете класть кирпичи, не проверив фундамент. Через месяц выясняется, что стены пошли трещинами. Знакомо? В программировании это называется «потом исправим». И «потом» обычно наступает в 3 часа ночи перед деплоем.

Разработка через тестирование (TDD — Test-Driven Development) — это техника, которая переворачивает процесс с ног на голову. Вы не пишете код, а потом тесты. Вы сначала пишете тест — и только потом код, который этот тест проходит. Звучит безумно? На практике это экономит часы, нервы и делает код чище, чем утренний воздух в горах.

В этой статье мы разберем TDD в Java на конкретных примерах. Никакой воды — только код, цикл Red-Green-Refactor и реальные сценарии. Поехали.

Что такое TDD и как работает цикл Red-Green-Refactor

TDD базируется на трёх простых шагах, которые повторяются циклически:

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

Кажется, что это замедляет разработку. На самом деле — ускоряет. Вы перестаёте гадать, «а работает ли это?». Вы знаете: работает, потому что тест зелёный.

Главные принципы TDD

  • Не пиши ни строчки продакшн-кода без теста.
  • Тест должен быть максимально простым и проверять ровно одно поведение.
  • Код пишется ровно настолько, чтобы тест прошёл. Никаких «а давай ещё и это на всякий случай».

Теперь давайте перейдём к Java и JUnit 5 — самому популярному фреймворку для модульного тестирования.

Настройка проекта: Maven, JUnit 5 и структура

Для примеров будем использовать Maven. Добавьте в pom.xml зависимость:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

Структура папок стандартная для Maven:

src/
  main/java/com/example/
  test/java/com/example/

Все тестовые классы кладём в src/test/java. Имя тестового класса обычно совпадает с именем тестируемого класса + суффикс Test. Например, Calculator → CalculatorTest.

Первый TDD-цикл: пишем калькулятор с нуля

Задача: создать метод add(int a, int b), который возвращает сумму. По TDD мы начинаем не с класса, а с теста.

Шаг 1: Red — пишем тест, который упадёт

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class CalculatorTest {

    @Test
    void shouldAddTwoNumbers() {
        Calculator calc = new Calculator();
        int result = calc.add(2, 3);
        assertEquals(5, result);
    }
}

Пытаемся запустить — компилятор ругается: класса Calculator не существует. Отлично, красный свет горит.

Шаг 2: Green — пишем минимальный код

Создаём класс Calculator с методом, который просто возвращает сумму:

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

Запускаем тест — зелёный. Минимально возможная реализация сработала.

Шаг 3: Refactor

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

Сложнее: TDD для сервиса с зависимостями (Mockito)

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

Представим, что у нас есть сервис UserService, который сохраняет пользователя и отправляет приветственное письмо. Мы не хотим реально отправлять письма в тестах.

Шаг 1: Red — тест для сохранения пользователя

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private EmailService emailService;

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void shouldSaveUserAndSendEmail() {
        User user = new User("test@example.com");
        when(userRepository.save(user)).thenReturn(user);

        User saved = userService.registerUser(user);

        assertEquals(user, saved);
        verify(emailService).sendWelcomeEmail(user.getEmail());
    }
}

Тест не скомпилируется — классов UserService, EmailService и UserRepository нет. Красный.

Шаг 2: Green — минимальная реализация

Создаём интерфейсы и классы. Важно: не пишем логику отправки писем, только то, что нужно для прохождения теста.

public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;

    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }

    public User registerUser(User user) {
        User saved = userRepository.save(user);
        emailService.sendWelcomeEmail(saved.getEmail());
        return saved;
    }
}

Тест зелёный. Мы не писали реальную отправку писем — это сделаем позже, когда дойдёт очередь до EmailServiceImpl. TDD заставляет нас фокусироваться на текущей задаче, а не на всех сразу.

Рефакторинг: как не бояться менять код

Самое страшное в рефакторинге — сломать что-то, что работало. TDD даёт суперсилу: вы можете переписывать внутренности метода, не боясь, что внешнее поведение изменится. Тесты — ваша страховка.

Пример: мы хотим добавить кэширование результатов в Calculator. Пишем новый тест, потом реализацию. Старые тесты всё ещё про

Блоги

Рекомендаций по книгам