Использовал ли ты контрактное тестирование (Contract Testing) для интеграционного тестирования?

Ответ

Да, применял контрактное тестирование, в частности с помощью фреймворка Pact, для решения проблем интеграции в микросервисной архитектуре. Мы столкнулись с ситуацией, когда сервис-потребитель (consumer) и сервис-провайдер (provider) разрабатывались разными командами, и частые breaking changes в API провайдера ломали потребителя.

Как это работало на практике:

  1. Со стороны потребителя (Consumer Tests): В наших автотестах для сервиса-потребителя мы использовали Pact для мокирования провайдера на основе ожидаемого контракта.

    // Пример на JavaScript (Jest + Pact)
    const { Pact } = require('@pact-foundation/pact');
    
    describe('User Service', () => {
      const provider = new Pact({
        consumer: 'FrontendService',
        provider: 'UserApiService',
      });
    
      beforeAll(() => provider.setup());
      afterEach(() => provider.verify());
      afterAll(() => provider.finalize());
    
      it('should receive user data', async () => {
        // Определяем ожидаемое взаимодействие (контракт)
        await provider.addInteraction({
          state: 'a user with id 123 exists',
          uponReceiving: 'a request for user data',
          withRequest: {
            method: 'GET',
            path: '/users/123',
          },
          willRespondWith: {
            status: 200,
            headers: { 'Content-Type': 'application/json' },
            body: {
              id: 123,
              name: 'John Doe', // Ожидаемая структура ответа
            },
          },
        });
    
        // Выполняем тест против мока Pact
        const user = await fetchUser(123);
        expect(user.name).toBe('John Doe');
      });
    });

    После успешного прогона тестов Pact публиковал контракт (JSON-файл) в общий брокер (например, PactFlow).

  2. Со стороны провайдера (Provider Verification): В пайплайне CI сервиса-провайдера запускалась задача верификации. Она забирала опубликованные контракты и прогоняла реальный API провайдера, проверяя, что он удовлетворяет всем ожиданиям потребителей.

    # Команда для верификации провайдера
    pact-verifier --provider-base-url http://localhost:8080 
                  --pact-broker-url https://our-broker.example.com 
                  --provider "UserApiService" 
                  --consumer-version-tags "prod"

Результат: Это позволило обнаруживать несовместимости на этапе Pull Request, а не в продакшене. Провайдер видел, что его изменения ломают конкретного потребителя, и мог либо скорректировать API, либо согласовать изменение с командой потребителя. Контрактное тестирование не заменило end-to-end тесты, но значительно снизило количество интеграционных сбоев.

Ответ 18+ 🔞

Бля, ну вот реально, история знакомая до боли в жопе. Сидишь такой, пишешь свой сервис, а тут бац — и соседняя команда хуяк и поменяла API, даже не предупредив. И твой красивый код накрывается медным тазом, потому что вместо user_name теперь прилетает fullName. Удивление пиздец, а работать-то надо.

Так вот, пришлось нам внедрять эту вашу контрактную хуйню, а именно Pact. Суть в чём, ёпта? Чтобы потребитель и провайдер друг другу в глаза не смотрели, но договорённости нарушать не смели. Как в бандитской разборке, только для программистов.

Как мы это провернули на деле:

  1. Со стороны того, кто пользуется (Consumer): В своих автотестах мы начали мокать провайдера не абы как, а через Pact. То есть прямо в коде прописываем: "слушай, провайдер, я ожидаю, что когда я тебе вот такую хуйню отправлю, ты мне вот такую структуру в ответ вывалишь".

    // Пример на JavaScript (Jest + Pact)
    const { Pact } = require('@pact-foundation/pact');
    
    describe('User Service', () => {
      const provider = new Pact({
        consumer: 'FrontendService',
        provider: 'UserApiService',
      });
    
      beforeAll(() => provider.setup());
      afterEach(() => provider.verify());
      afterAll(() => provider.finalize());
    
      it('should receive user data', async () => {
        // Вот тут мы и диктуем свои условия, блядь
        await provider.addInteraction({
          state: 'a user with id 123 exists',
          uponReceiving: 'a request for user data',
          withRequest: {
            method: 'GET',
            path: '/users/123',
          },
          willRespondWith: {
            status: 200,
            headers: { 'Content-Type': 'application/json' },
            body: {
              id: 123,
              name: 'John Doe', // Чётко говорим, что хотим видеть. Без сюрпризов!
            },
          },
        });
    
        // И тестируем уже против этого мока
        const user = await fetchUser(123);
        expect(user.name).toBe('John Doe');
      });
    });

    И после того как тесты проходят, Pact выплевывает этот контракт (просто JSON-файл) в общее место — в так называемый брокер. Типа публичная оферта, ебать.

  2. Со стороны того, кто предоставляет (Provider): А вот тут самое интересное начинается. В CI/CD пайплайне провайдера впендюрили задачу, которая берёт все эти контракты от потребителей и натягивает их на свой живой, блядовитый API. И смотрит — а удовлетворяет ли он всем этим ожиданиям? Если где-то накосячил и поменял ответ — сразу получает по шапке в пайплайне.

    # Команда, которая всё и ловит за руку
    pact-verifier --provider-base-url http://localhost:8080 
                  --pact-broker-url https://our-broker.example.com 
                  --provider "UserApiService" 
                  --consumer-version-tags "prod"

Что в сухом остатке, чувак? Волнение ебать, но терпения ноль — зато теперь мы ловим поломки не когда всё уже горит в продакшене, а прямо в пулл-реквесте. Провайдер видит: "Опа, а я тут поле name переименовал, а оказывается, FrontendService на него завязан и уже охуел". И можно либо откатиться, либо побежать договариваться. Это не отменяет толстые end-to-end тесты, но количество внезапных пиздецов в пятницу вечером сократилось овердохуища. И жить стало спокойнее, ядрёна вошь.