Ответ
Да, я писал E2E-тесты. Важно понимать, что E2E-тест проверяет всю систему в сборе как "черный ящик", в отличие от юнит- или интеграционных тестов.
Мой подход к E2E-тестированию выглядит так:
-
Подготовка окружения: Тест должен запускать реальные экземпляры зависимостей (база данных, кэш, другие сервисы). Лучший инструмент для этого в Go — библиотека
testcontainers-go. Она позволяет программно поднимать нужные Docker-контейнеры на время выполнения тестов и автоматически их останавливать. -
Запуск приложения: В рамках теста мы компилируем и запускаем наше приложение как отдельный процесс или в горутине. Оно должно слушать реальный порт (например,
localhost:8080). -
Выполнение тестовых сценариев: Тесты отправляют настоящие HTTP-запросы к запущенному приложению с помощью стандартного
http.Client. -
Проверка результатов: Мы проверяем не только код ответа и тело, но и состояние системы (например, делаем прямой запрос в тестовую БД, чтобы убедиться, что запись действительно создалась).
Ключевые инструменты:
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-тесты, говоришь? Ну это, блядь, отдельная песня, ёпта! Это когда ты не просто кусочек кода трясёшь, а берёшь всю свою конструкцию, эту, блядь, систему в сборе, и проверяешь её как чёрный ящик. Не то что юниты, там, по одному винтику. Тут — вся махина, от и до, чтобы прям пиздец как похоже на продакшен.
Я обычно действую вот по такой, блядь, схеме, чтоб её:
-
Окружение поднимаем. Это ж не мокать всё, нахуй. Надо, чтобы реальные штуки работали: база, кэш, соседние сервисы — всё как у больших. Для этого в Go есть просто овердохуищная библиотека —
testcontainers-go. Она тебе, сука, прямо из кода дёргает Docker, поднимает нужные контейнеры на время теста, а потом их прибивает. Красота, а не жизнь! -
Приложение запускаем. Берём наш бинарь, этот, компилируем и запускаем как отдельный процесс, чтобы он на реальном порту висел (ну, типа
localhost:8080). Пусть думает, что он в бою. -
Сценарии гоняем. А вот тут уже, блядь, начинается самое интересное. Тесты начинают слать настоящие HTTP-запросы в это запущенное приложение, через обычный
http.Client. Прямо как пользователь или фронтенд. -
Результаты смотрим. И мало, блядь, получить ответ «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, даёт тебе, блядь, почти стопроцентную уверенность, что все твои железяки друг с другом сцепились как надо и работают. А не просто в воздухе болтаются.