Что такое модульное (Unit) тестирование и зачем оно нужно?

«Что такое модульное (Unit) тестирование и зачем оно нужно?» — вопрос из категории Тестирование, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Модульное (Unit) тестирование — это уровень тестирования программного обеспечения, при котором проверяется корректность работы минимальных изолированных частей приложения — модулей (юнитов). В контексте ООП юнитом обычно является отдельный класс или метод.

Ключевые цели unit-тестирования:

  1. Обеспечение корректности: Проверка, что каждая часть программы работает в соответствии с техническим заданием (спецификацией) при различных входных данных (включая граничные случаи).
  2. Раннее обнаружение дефектов: Ошибки выявляются на этапе разработки, что значительно дешевле их исправления на поздних стадиях или в production.
  3. Облегчение рефакторинга: Набор unit-тестов (тестовый набор) служит "страховочной сеткой", позволяя безопасно изменять и улучшать код, будучи уверенным, что существующая функциональность не сломана.
  4. Живая документация: Тесты демонстрируют, как должен использоваться модуль и какое поведение от него ожидается.
  5. Улучшение дизайна: Написание тестируемого кода часто приводит к более модульной, слабосвязанной архитектуре.

Пример unit-теста с использованием JUnit 5 и Mockito:

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

class PaymentServiceTest {
    @Test
    void processPayment_ShouldReturnTrue_WhenPaymentIsSuccessful() {
        // 1. ARRANGE (Подготовка)
        // Создаем mock-объект зависимого сервиса
        PaymentGateway mockGateway = mock(PaymentGateway.class);
        // Задаем поведение mock: при вызове pay() с любыми аргументами вернуть true
        when(mockGateway.pay(anyDouble())).thenReturn(true);

        // Создаем тестируемый сервис, инжектим mock-зависимость
        PaymentService service = new PaymentService(mockGateway);

        // 2. ACT (Действие)
        boolean result = service.processPayment(100.50, "order_123");

        // 3. ASSERT (Проверка)
        assertTrue(result); // Проверяем ожидаемый результат
        // Проверяем, что метод pay был вызван ровно один раз с конкретным аргументом
        verify(mockGateway, times(1)).pay(100.50);
    }

    @Test
    void calculateDiscount_ShouldApplyTenPercent_ForAmountOver1000() {
        // Тест чистой функции без зависимостей
        PaymentService service = new PaymentService(null);
        double result = service.calculateDiscount(1500.0);
        assertEquals(150.0, result, 0.001); // Проверка с дельтой для double
    }
}

Основной принцип: Один тест проверяет один конкретный сценарий (use case) для одного модуля.