Как вы подходите к написанию End-to-End (E2E) тестов для Go-приложений? Какие инструменты и подходы используете?

Ответ

Да, я писал E2E-тесты. Важно понимать, что E2E-тест проверяет всю систему в сборе как "черный ящик", в отличие от юнит- или интеграционных тестов.

Мой подход к E2E-тестированию выглядит так:

  1. Подготовка окружения: Тест должен запускать реальные экземпляры зависимостей (база данных, кэш, другие сервисы). Лучший инструмент для этого в Go — библиотека testcontainers-go. Она позволяет программно поднимать нужные Docker-контейнеры на время выполнения тестов и автоматически их останавливать.

  2. Запуск приложения: В рамках теста мы компилируем и запускаем наше приложение как отдельный процесс или в горутине. Оно должно слушать реальный порт (например, localhost:8080).

  3. Выполнение тестовых сценариев: Тесты отправляют настоящие HTTP-запросы к запущенному приложению с помощью стандартного http.Client.

  4. Проверка результатов: Мы проверяем не только код ответа и тело, но и состояние системы (например, делаем прямой запрос в тестовую БД, чтобы убедиться, что запись действительно создалась).

Ключевые инструменты:

  • testing: Стандартный пакет для организации тестов.
  • testcontainers-go: Для управления жизненным циклом Docker-контейнеров с зависимостями (Postgres, Redis и т.д.).
  • net/http: Стандартный HTTP-клиент для отправки запросов к нашему сервису.
  • testify/assert и testify/require: Для удобных и читаемых проверок (ассертов).

Пример концептуальной структуры теста:

// Этот код показывает структуру, а не полную реализацию

func TestUserFlow_E2E(t *testing.T) {
    // 1. Запускаем контейнер с PostgreSQL с помощью testcontainers-go
    ctx := context.Background()
    dbContainer, err := postgres.RunContainer(ctx, /* ...opts */)
    require.NoError(t, err)
    defer dbContainer.Terminate(ctx)

    // Получаем connection string к тестовой БД
    connStr, err := dbContainer.ConnectionString(ctx, "sslmode=disable")
    require.NoError(t, err)

    // 2. Запускаем наше приложение в горутине, передав ему connStr
    app := NewApp(connStr)
    go app.Run(":8081") // Используем другой порт, чтобы не конфликтовать
    time.Sleep(1 * time.Second) // Даем время на запуск

    // 3. Выполняем тестовый сценарий
    // Создаем нового пользователя
    resp, err := http.Post("http://localhost:8081/users", "application/json", bytes.NewBuffer(newUserJSON))
    require.NoError(t, err)
    assert.Equal(t, http.StatusCreated, resp.StatusCode)

    // 4. Проверяем, что пользователь действительно появился в БД
    // ... делаем прямой SQL-запрос в тестовую БД ...
}

Этот подход, в отличие от моков или httptest, дает максимальную уверенность в том, что все компоненты системы корректно работают вместе.

Ответ 18+ 🔞

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

Я обычно действую вот по такой, блядь, схеме, чтоб её:

  1. Окружение поднимаем. Это ж не мокать всё, нахуй. Надо, чтобы реальные штуки работали: база, кэш, соседние сервисы — всё как у больших. Для этого в Go есть просто овердохуищная библиотека — testcontainers-go. Она тебе, сука, прямо из кода дёргает Docker, поднимает нужные контейнеры на время теста, а потом их прибивает. Красота, а не жизнь!

  2. Приложение запускаем. Берём наш бинарь, этот, компилируем и запускаем как отдельный процесс, чтобы он на реальном порту висел (ну, типа localhost:8080). Пусть думает, что он в бою.

  3. Сценарии гоняем. А вот тут уже, блядь, начинается самое интересное. Тесты начинают слать настоящие HTTP-запросы в это запущенное приложение, через обычный http.Client. Прямо как пользователь или фронтенд.

  4. Результаты смотрим. И мало, блядь, получить ответ «200 ОК». Надо залезть в тестовую базу и проверить, а появилась ли там запись на самом деле? А в кэше что? Вот это и есть уверенность, а не «ой, мы замокали, и вроде работает».

Чем, сука, воюем:

  • testing — ну, куда ж без него, основа основ.
  • testcontainers-go — главная звезда, чтобы Postgres, Redis и прочую хуйню поднимать.
  • net/http — чтобы стучаться к своему же сервису.
  • testify/assert и testify/require — чтобы проверки писать не вот это вот if err != nil, а красиво и понятно.

Вот, смотри, как это примерно в коде выглядит, концептуально:

// Это схема, блядь, а не готовый код, не тупи!

func TestUserFlow_E2E(t *testing.T) {
    // 1. Дёргаем testcontainers, пусть нам Postgres в контейнере поднимет
    ctx := context.Background()
    dbContainer, err := postgres.RunContainer(ctx, /* ...опции */)
    require.NoError(t, err) // Если ошибка — сразу пиздец тесту, дальше не идём
    defer dbContainer.Terminate(ctx) // А после теста контейнер — нахуй!

    // Тянем строку подключения к этой самой тестовой базе
    connStr, err := dbContainer.ConnectionString(ctx, "sslmode=disable")
    require.NoError(t, err)

    // 2. Наше приложение запускаем, передав ему эту строку
    app := NewApp(connStr)
    go app.Run(":8081") // Кидаем в горутину, пусть себе слушает
    time.Sleep(1 * time.Second) // Ждём, пока, блядь, глаза откроет

    // 3. Пошли сценарии!
    // Пытаемся создать пользователя
    resp, err := http.Post("http://localhost:8081/users", "application/json", bytes.NewBuffer(newUserJSON))
    require.NoError(t, err)
    assert.Equal(t, http.StatusCreated, resp.StatusCode) // Должен сказать "создано"

    // 4. А теперь, хитрая жопа, лезем прямо в базу проверять!
    // ... делаем прямой SQL-запрос в ту самую тестовую БД из контейнера ...
}

Вот этот подход, в отличие от всех этих моков и httptest, даёт тебе, блядь, почти стопроцентную уверенность, что все твои железяки друг с другом сцепились как надо и работают. А не просто в воздухе болтаются.