Как правильно тестировать взаимодействие между микросервисами? Нужно ли писать тесты для чужого сервиса?

Ответ

Прямое тестирование внутренней логики чужого микросервиса — это антипаттерн. Это зона ответственности команды, которая его разрабатывает. Наша задача — тестировать взаимодействие с ним, то есть проверять, что наш сервис корректно отправляет запросы и обрабатывает ответы в соответствии с оговоренным контрактом (API).

Существует несколько подходов:

  1. Интеграционные тесты с моками (заглушками) Это самый частый подход. Мы не используем реальный соседний сервис, а поднимаем его легковесную имитацию (мок) во время теста. В Go для этого отлично подходит пакет net/http/httptest.

    • Что мы проверяем:
      • Наш сервис правильно формирует HTTP-запрос (URL, заголовки, тело).
      • Наш сервис корректно парсит успешный ответ.
      • Наш сервис правильно обрабатывает ошибки (например, 404 или 500).

    Пример (тестируем наш API-клиент):

    package main
    
    import (
        "encoding/json"
        "net/http"
        "net/http/httptest"
        "testing"
    )
    
    // Структура пользователя, которую мы ожидаем от сервиса B
    type User struct {
        ID   int    `json:"id"`
        Name string `json:"name"`
    }
    
    // Клиент для взаимодействия с сервисом B
    type APIClient struct {
        BaseURL string
        Client  *http.Client
    }
    
    // Метод нашего клиента, который мы хотим протестировать
    func (c *APIClient) GetUser(id int) (*User, error) {
        // ... логика похода в сервис B
        // В данном примере она не важна, так как мы тестируем взаимодействие
        // с мок-сервером, который имитирует сервис B.
        return nil, nil // Реальная логика здесь не нужна для примера
    }
    
    func TestGetUserFromServiceB(t *testing.T) {
        // 1. Создаем тестовый сервер, который будет имитировать сервис B
        server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Проверяем, что наш клиент пришел по правильному URL
            if r.URL.Path != "/users/1" {
                t.Errorf("Expected to request '/users/1', got %s", r.URL.Path)
            }
            w.WriteHeader(http.StatusOK)
            // Отдаем заранее подготовленный ответ
            json.NewEncoder(w).Encode(User{ID: 1, Name: "Alice"})
        }))
        defer server.Close()
    
        // 2. Создаем экземпляр нашего клиента, указывая ему адрес мок-сервера
        // В реальном коде здесь будет метод вроде NewAPIClient(server.URL)
        // client := NewAPIClient(server.URL)
    
        // 3. Выполняем запрос и проверяем результат
        // user, err := client.GetUser(1)
    
        // ... тут идут проверки (asserts), что user.Name == "Alice" и err == nil
    }
  2. Контрактные тесты Более продвинутый подход. Производитель (сервис, который предоставляет API) и потребитель (наш сервис) договариваются о "контракте". Контракт описывает структуру запросов и ответов. Тесты автоматически проверяют, что и производитель, и потребитель соответствуют этому контракту. Инструменты: Pact, Pact-Go.

  3. End-to-End (E2E) тесты Эти тесты запускаются на полностью развернутом окружении (например, staging), где все микросервисы работают по-настоящему. Они проверяют сквозные пользовательские сценарии. Это самые дорогие и медленные тесты, но они единственные, кто проверяет систему в целом.

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