Logo Testów Kontraktowych

Testy kontraktowe: nowoczesny przewodnik po testowaniu w mikroserwisach

Testy kontraktowe (Contract Testing) to zaawansowana technika testowania stosowana w architekturach rozproszonych (np. mikroserwisy), której celem jest weryfikacja, czy dwie oddzielne, zintegrowane ze sobą usługi są w stanie poprawnie się komunikować. W przeciwieństwie do tradycyjnych testów integracyjnych, które wymagają jednoczesnego uruchomienia wielu komponentów, testy kontraktowe pozwalają na weryfikację integracji w sposób całkowicie niezależny i asynchroniczny, co drastycznie przyspiesza proces deweloperski i zwiększa jego niezawodność.

Problem: Ból Tradycyjnych Testów Integracyjnych

W architekturze mikroserwisów, gdzie dziesiątki, a nawet setki usług komunikują się ze sobą, uruchomienie pełnego, zintegrowanego środowiska do testów End-to-End jest niezwykle trudne, kosztowne i czasochłonne. Zmiana w jednej usłudze może nieświadomie "złamać" integrację z inną, a błąd ten zostanie wykryty dopiero na późnym etapie, często już na produkcji. Tradycyjne podejście generuje szereg problemów w systemach informatycznych: wolne pętle informacji zwrotnej, niestabilne i trudne w utrzymaniu środowiska testowe oraz kaskadowe awarie, gdy jedna usługa jest niedostępna. Testy kontraktowe powstały, aby rozwiązać te palące problemy.

Kluczowe Koncepcje: Konsument, Dostawca i Kontrakt

Testy kontraktowe opierają się na trzech filarach, które modelują interakcję między usługami:

  • Konsument (Consumer): Usługa, która inicjuje komunikację i jest zależna od danych dostarczanych przez inną usługę. Konsument "konsumuje" API dostawcy.
  • Dostawca (Provider): Usługa, która odpowiada na żądania konsumenta (np. serwer API). Dostawca "dostarcza" dane lub wykonuje akcje na żądanie konsumenta.
  • Kontrakt (Contract): Dokument (zazwyczaj w formacie JSON) definiujący oczekiwania konsumenta wobec dostawcy. Jest on generowany po stronie konsumenta i określa, jakich dokładnie danych, w jakiej strukturze i pod jakimi warunkami oczekuje. Kontrakt jest "spisaną umową" dotyczącą działania API.

Jak Działają Testy Kontraktowe w Praktyce?

Proces testowania kontraktowego jest podzielony na dwa główne etapy, które odbywają się niezależnie w potokach CI/CD obu usług:

  1. Po stronie konsumenta: Deweloperzy piszą testy jednostkowe, które definiują, jak ich aplikacja będzie komunikować się z API dostawcy. W trakcie tych testów, narzędzie do testów kontraktowych (np. Pact) uruchamia mockowy serwer, który udaje prawdziwego dostawcę i zwraca dane zgodne z oczekiwaniami. Jeśli testy przejdą pomyślnie, generowany jest plik kontraktu.
  2. Publikacja kontraktu: Wygenerowany kontrakt jest wysyłany do centralnego repozytorium zwanego "Brokerem" (np. Pact Broker), który zarządza wersjami kontraktów i wynikami weryfikacji.
  3. Po stronie dostawcy: W potoku CI/CD dostawcy, jego usługa pobiera z Brokera wszystkie kontrakty opublikowane przez swoich konsumentów.
  4. Weryfikacja kontraktu: Narzędzie (Pact) automatycznie "odgrywa" żądania zapisane w kontrakcie przeciwko uruchomionej aplikacji dostawcy i sprawdza, czy odpowiedzi generowane przez prawdziwe API są zgodne z oczekiwaniami zdefiniowanymi w kontrakcie. Jeśli tak, weryfikacja kończy się sukcesem.

Kluczowe Narzędzie: Pact

Pact to de facto standard i najpopularniejszy framework open-source do implementacji testów kontraktowych. Dostarcza on biblioteki dla wielu języków programowania (Java, JavaScript, .NET, Ruby, Go i wiele innych) oraz narzędzia wspierające cały proces, w tym Pact Broker. Jego siłą jest promowanie podejścia "Consumer-Driven Contracts", gdzie to potrzeby konsumenta dyktują kształt API, co jest nieocenione przy tworzeniu dedykowanego systemu.

Przykład Kodu - Test Kontraktowy w Pact JS (Konsument)

Poniżej znajduje się prosty przykład testu po stronie konsumenta, który generuje kontrakt.


// consumer.test.js - Test po stronie Konsumenta

const { Pact } = require('@pact-foundation/pact');
const { like, eachLike } = require('@pact-foundation/pact').Matchers;
const apiClient = require('./apiClient'); // Klient API konsumenta

const provider = new Pact({
  consumer: 'MyConsumer',
  provider: 'MyProvider',
  port: 1234,
});

describe('API Pact test', () => {
  before(() => provider.setup());
  afterEach(() => provider.verify());
  after(() => provider.finalize());

  it('should return a list of products', async () => {
    // Definicja interakcji i oczekiwanej odpowiedzi
    await provider.addInteraction({
      state: 'I have a list of products',
      uponReceiving: 'a request for all products',
      withRequest: {
        method: 'GET',
        path: '/products',
      },
      willRespondWith: {
        status: 200,
        headers: { 'Content-Type': 'application/json; charset=utf-8' },
        body: eachLike({
          id: like(1),
          name: like('Product 1'),
          price: like(100.0),
        }),
      },
    });

    // Uruchomienie prawdziwego kodu klienta API
    const products = await apiClient.getProducts();
    // Tutaj mogą znajdować się asercje dotyczące logiki biznesowej
  });
});

Przykład Kodu - Weryfikacja Kontraktu (Dostawca)

Ten kod pokazuje, jak dostawca (provider) weryfikuje, czy jego API spełnia oczekiwania zdefiniowane w kontrakcie opublikowanym przez konsumenta.


// provider.verify.js - Skrypt weryfikujący po stronie Dostawcy

const { Verifier } = require('@pact-foundation/pact');
const server = require('./providerApi'); // Uruchomienie serwera API dostawcy

// Uruchomienie serwera przed weryfikacją
beforeAll(() => {
    server.listen(8081, () => {
        console.log('Provider API listening on http://localhost:8081');
    });
});

describe('Pact Verification', () => {
  it('validates the expectations of MyConsumer', () => {
    const opts = {
      provider: 'MyProvider',
      providerBaseUrl: 'http://localhost:8081', // Adres działającej aplikacji dostawcy
      pactBrokerUrl: 'https://your-pact-broker.com', // Adres Pact Brokera
      publishVerificationResult: true,
      providerVersion: '1.0.0', // Wersja dostawcy

      // Definicja stanów (states) potrzebnych do testów
      stateHandlers: {
        'I have a list of products': () => {
          // Tutaj przygotowujemy stan aplikacji, np. dodajemy dane do bazy
          return Promise.resolve('Products ready for test');
        }
      }
    };

    return new Verifier(opts).verifyProvider();
  });
});

Główne Zalety

  • Szybka i precyzyjna informacja zwrotna: Błędy integracyjne są wykrywane na bardzo wczesnym etapie, w potokach CI/CD poszczególnych usług, a nie podczas kosztownych testów E2E. Dokładnie wiadomo, która zmiana w API dostawcy naruszyła kontrakt z konsumentem.
  • Niezależne wdrożenia: Zespoły mogą wdrażać swoje usługi niezależnie i autonomicznie, z dużą pewnością, że nie zepsują integracji z innymi komponentami systemu.
  • Eliminacja złożonych środowisk testowych: Redukuje potrzebę utrzymywania skomplikowanych, w pełni zintegrowanych środowisk testowych, co znacząco obniża koszty i złożoność infrastruktury dla każdego systemu na zamówienie.
  • Żywa dokumentacja API: Kontrakty opublikowane w Brokerze służą jako zawsze aktualna, techniczna dokumentacja API, pokazująca dokładnie, jak poszczególni konsumenci wykorzystują dane punkty końcowe.

Podsumowanie

Podsumowując, testy kontraktowe to potężna i nowoczesna technika, która jest kluczowa dla zapewnienia stabilności, niezawodności i szybkości rozwoju w złożonych architekturach mikroserwisowych. Poprzez wczesne wykrywanie błędów integracyjnych, umożliwiają one zespołom autonomiczną, równoległą pracę, jednocześnie dając pewność, że poszczególne komponenty systemu będą ze sobą poprawnie współpracować po wdrożeniu na produkcję.

Testy Kontraktowe logo
AKTUALNE SZKOLENIA