В чем разница между модульным (unit) и интеграционным тестированием?

Ответ

Модульное (unit) и интеграционное тестирование — это два разных уровня в пирамиде тестирования, каждый из которых преследует свои цели.

Модульные тесты (Unit Tests)

  • Цель: Проверить корректность работы наименьшей изолированной части кода — "юнита" (обычно это одна функция или метод).
  • Область: Очень маленькая и сфокусированная. Тестируется только логика внутри юнита.
  • Изоляция: Все внешние зависимости (базы данных, файловая система, сетевые вызовы, другие сервисы) должны быть заменены моками (mocks) или стабами (stubs). Это гарантирует, что тест проверяет только тестируемый код, а не его зависимости.
  • Скорость: Очень быстрые, так как не требуют запуска внешних систем. Их должно быть много.

Пример (проверка функции Add):

func Add(a, b int) int {
    return a + b
}

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Ожидали 5, получили %d", result)
    }
}

Интеграционные тесты (Integration Tests)

  • Цель: Проверить, что несколько компонентов системы корректно взаимодействуют друг с другом. Они проверяют "швы" и контракты между модулями.
  • Область: Шире, чем у unit-тестов. Например, взаимодействие вашего сервиса с базой данных, с очередью сообщений или с другим API.
  • Изоляция: Частичная. Внешние системы (БД, API) не мокаются, а используются их реальные или тестовые экземпляры (например, тестовая база данных, запущенная в Docker).
  • Скорость: Значительно медленнее, чем unit-тесты, так как требуют настройки окружения и реальных I/O операций.

Пример (проверка создания пользователя в БД):

// db - это реальное подключение к тестовой базе данных
func TestUserCreation(t *testing.T) {
    // 1. Настройка: подключение к тестовой БД и очистка таблиц
    db, cleanup := setupTestDB(t)
    defer cleanup() // 3. Очистка после теста

    repo := NewUserRepository(db)

    // 2. Действие: вызов метода, который работает с реальной БД
    err := repo.CreateUser("test@example.com", "password123")
    if err != nil {
        t.Fatalf("Не удалось создать пользователя: %v", err)
    }

    // Можно добавить проверку, что пользователь действительно появился в БД
}

Сравнительная таблица

Характеристика Модульные тесты (Unit) Интеграционные тесты (Integration)
Цель Корректность отдельного модуля в изоляции Корректность взаимодействия между модулями
Зависимости Заменяются моками/стабами Используются реальные (или тестовые экземпляры)
Скорость Очень высокая Средняя / Низкая
Количество Много (основа пирамиды тестирования) Меньше, чем модульных
Обнаруживаемые ошибки Ошибки в алгоритмах и логике внутри функции Ошибки в контрактах API, конфигурации, схеме БД, сетевом взаимодействии

Заключение: Оба типа тестов критически важны. Unit-тесты обеспечивают надежность отдельных кирпичиков, а интеграционные тесты — что эти кирпичики правильно складываются в стену.

Ответ 18+ 🔞

А, слушай, вот эта вся тема с тестами — она же, блядь, как слоёный пирог, понимаешь? Один слой — это одно, другой — вообще другое. И если их перепутать, то потом сидишь и ебёшь мозг, почему всё падает, а ты нихуя не понимаешь.

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

Вот, смотри, пример, простой как три копейки:

func Add(a, b int) int {
    return a + b
}

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Ожидали 5, получили %d", result)
    }
}

Видишь? Никаких телодвижений. Запустил — получил ответ. Быстро, дёшево, сердито. Таких тестов должно быть, блядь, овердохуища, они фундамент.

А теперь интеграционные тесты — это уже следующий уровень, ёпта. Тут уже не вакуум, а почти реальная жизнь. Цель — проверить, как твои отдельные, протестированные по одиночке, модули начинают друг с другом общаться. Не с заглушками, а по-настоящему. Например, твой код пошёл что-то писать в базу данных. И ты проверяешь — а дошло ли? А правильно ли записалось? А не послал ли он туда какую-нибудь дичь?

Тут уже всё серьёзнее, медленнее и геморройнее. Надо поднимать тестовую базу, чистить её перед тестом, потом после теста убирать за собой. Вот, типа так:

func TestUserCreation(t *testing.T) {
    // 1. Настройка: подключение к тестовой БД и очистка таблиц
    db, cleanup := setupTestDB(t)
    defer cleanup() // 3. Очистка после теста

    repo := NewUserRepository(db)

    // 2. Действие: вызов метода, который работает с реальной БД
    err := repo.CreateUser("test@example.com", "password123")
    if err != nil {
        t.Fatalf("Не удалось создать пользователя: %v", err)
    }

    // Можно добавить проверку, что пользователь действительно появился в БД
}

Видишь разницу? Тут уже не просто логика, тут уже интеграция, блядь. Скорость, конечно, уже не та. Если модульные тесты — это спринт, то интеграционные — это уже кросс с препятствиями.

Короче, чтобы было совсем понятно, вот тебе табличка, в рот меня чих-пых:

Признак Модульные тесты (Юниты) Интеграционные тесты
Зачем? Проверить один кирпичик в изоляции Проверить, как кирпичики склеиваются в стену
С кем общается? Ни с кем, все зависимости — муляжи С реальными или тестовыми базами, API и прочей хуйней
Скорость Быстрее света, их можно тыкать постоянно Медленные, как черепаха в сиропе
Сколько нужно? Много, это основа всего Меньше, но они тоже очень важны
Что ловят? Косяки в твоей собственной логике Косяки в том, как всё связано: схемы БД, контракты, сеть

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