Contract vs E2E — wybór właściwej granicy

Testy E2E są wolne i kruche. Contract testy są szybkie, ale ograniczone. Naucz się, kiedy walidować granice integracyjne z kontraktami, kiedy wciąż potrzebujesz E2E i jak łączyć oba.

Ten wpis jest częścią serii Strategia automatyzacji. Jeśli przegapiłeś poprzedni wpis, przeczytaj najpierw Część 6 — Niestabilne testy.


Masz przepływ checkout. Wywołuje trzy serwisy backendowe: zapasy, płatność i wysyłkę. Aby przetestować to end-to-end, potrzebujesz:

  • Działającego frontendu
  • Trzech serwisów backendowych
  • Danych testowych we wszystkich trzech bazach danych
  • Mockowanych credentials bramki płatności
  • 5–10 minut runtime’u testu

Gdy jakikolwiek serwis zmienia API, test E2E się łamie. Debugowanie failury wymaga przekopywania logów przez cztery serwisy, aby znaleźć który złamał kontrakt.

Jest lepszy sposób: contract testy.

To ostatni wpis w serii Strategia automatyzacji. Omówimy, kiedy contract testy oszczędzają czas i pieniądze, kiedy testy E2E są wciąż konieczne i jak łączyć oba dla maksymalnej pewności z minimalnym kosztem utrzymania.


Czym są contract testy?

Contract test waliduje, że dwa serwisy mogą komunikować się poprawnie bez uruchamiania obu w tym samym czasie.

Kontrakt

„Kontrakt” to specyfikacja API: format requestu, format response, status codes, headers. Obie strony zgadzają się go respektować.

Przykładowy kontrakt (OpenAPI):

paths:
  /inventory/check:
    post:
      summary: Sprawdź dostępność produktu
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                productId:
                  type: string
                quantity:
                  type: integer
      responses:
        '200':
          description: Dostępność sprawdzona
          content:
            application/json:
              schema:
                type: object
                properties:
                  available:
                    type: boolean
                  stock:
                    type: integer

Consumer-Driven Contracts (Pact)

Najpopularniejsze podejście contract testingu to consumer-driven contracts. Konsument (np. frontend) definiuje, czego potrzebuje od providera (np. backend API). Provider waliduje, że może spełnić kontrakt.

Narzędzia: Pact, Spring Cloud Contract, Postman Contract Testing

Provider Contract Testing

Alternatywnie provider publikuje kontrakt (spec OpenAPI), a konsumenci walidują, że używają go poprawnie.

Narzędzia: Prism, Dredd, Postman


Contract testy vs E2E: kiedy każdy łapie błędy

Porównajmy, co każdy typ testu waliduje.

Co łapią contract testy

Niezgodność kształtu API — Konsument oczekuje userId, provider zwraca user_id
Błędy typów — Konsument oczekuje string, provider zwraca integer
Brakujące pola — Konsument wymaga email, provider go nie zwraca
Niezgodność status code — Konsument oczekuje 200, provider zwraca 201
Breaking changes — Provider usuwa pole, od którego konsument zależy

Czego nie łapią contract testy

Błędy logiki biznesowej — Obliczenie płatności jest błędne (zwraca 200 z nieprawidłowym totalem)
Failury integracji end-to-end — Serwisy integrują się poprawnie, ale w złej kolejności
Błędy UI — Przycisk wyłączony gdy powinien być włączony
Problemy prawdziwych danych — Działa z danymi testowymi, failuje z produkcyjnymi edge case’ami
Problemy wydajnościowe — API odpowiada poprawnie, ale trwa 30 sekund

Co łapią testy E2E

Pełne user journey działa — Użytkownik może ukończyć checkout od początku do końca
Logika biznesowa przez serwisy — Rabat zastosowany poprawnie, podatek obliczony, email wysłany
Problemy UI/UX — Przycisk wyłączony gdy nie powinien, zepsuta walidacja formularza
Prawdziwe failury integracji — Serwisy komunikują się, ale w złej kolejności lub z side effects
Transakcje cross-service — Płatność pobrana, ale zamówienie nie utworzone (failure rollback)

Czego testy E2E nie łapią efektywnie

Błędy kontraktu API (contract testy są szybsze)
Edge case’y w logice backendu (testy unit/integration są lepsze)
Wydajność pod obciążeniem (potrzeba dedykowanych testów wydajnościowych)


Kiedy używać contract testów

Contract testy są idealne gdy:

1. Masz mikroserwisy

W architekturze mikroserwisowej serwisy ewoluują niezależnie. Contract testy zapobiegają breaking changes bez wymagania skoordynowanych wdrożeń.

Przykład: Serwis płatności zmienia format response. Contract testy łapią złamanie przed wdrożeniem. Brak potrzeby uruchamiania pełnego suite’a E2E przez 12 serwisów.

2. Zespoły posiadają różne serwisy

Gdy frontend i backend są posiadane przez różne zespoły, contract testy definiują interfejs jasno. Oba zespoły mogą rozwijać niezależnie.

Przykład: Zespół frontendowy definiuje, czego potrzebują w Pact. Zespół backendowy waliduje, że to dostarczają. Brak spotkań, brak nieporozumień.

3. Zewnętrzne API

Gdy integrujesz z API trzeciej strony (Stripe, Twilio, AWS), contract testy walidują, że używasz API poprawnie bez dotykania prawdziwych endpointów.

Przykład: Stripe zmienia format response. Contract test łapie to natychmiast. Brak failur produkcyjnych.

4. Potrzebujesz szybkiego feedbacku

Contract testy działają w sekundach. Testy E2E działają w minutach. Dla pipeline’ów CI/CD szybkość ma znaczenie.

Przykład: Pull request wyzwala contract testy w 30 sekund. Merge zablokowany jeśli kontrakt się łamie. Suite E2E działa nocnie.


Kiedy wciąż potrzebujesz testów E2E

Contract testy nie są zamiennikiem E2E. Wciąż potrzebujesz E2E gdy:

1. Walidacja krytycznych user journeys

Contract testy potwierdzają komunikację API. Nie potwierdzają, że użytkownicy mogą ukończyć zadania.

Przykład: Przepływ checkout. Contract testy walidują inventory API, payment API i shipping API. Test E2E waliduje, że użytkownik może ukończyć checkout i otrzymuje email potwierdzenia.

2. Testowanie logiki biznesowej przez serwisy

Niektóre błędy pojawiają się tylko gdy serwisy wchodzą w interakcję sekwencyjnie z prawdziwymi danymi.

Przykład: Utworzenie zamówienia się udaje, płatność pobierana pomyślnie, ale zapasy nie są dekrementowane z powodu race condition. Tylko test E2E to łapie.

3. Walidacja UI/UX

Contract testy nie dotykają UI. Jeśli twój biznes zależy od poprawności UI (e-commerce, dashboardy SaaS), testy E2E są konieczne.

Przykład: Przycisk „Złóż zamówienie” wyłączony pomimo prawidłowego formularza. Contract testy przechodzą, ale użytkownicy nie mogą zamówić.

4. Integracja z systemami zewnętrznymi, których nie kontrolujesz

Jeśli API trzeciej strony zmienia zachowanie bez zmiany kontraktu, contract testy tego nie złapią.

Przykład: Bramka płatności zwraca 200, ale zmienia success response z { "status": "success" } na { "result": "success" }. Contract test przechodzi (oba są prawidłowym JSON), ale integracja się łamie.


Łączenie contract testów i E2E

Najlepsza strategia: używaj obu, na różnych warstwach.

Strategia kombinowana

Typ testuPokrycieCzęstotliwośćCel
Contract testyWszystkie granice serwisówKażdy commitSzybki feedback na kompatybilność API
Testy integracyjneAPI + baza danychKażdy commitWalidacja logiki backendu bez UI
Testy E2E (krytyczne ścieżki)Pełne user journeysKażde wdrożeniePewność przepływów użytkownika
Testy E2E (rozszerzone)Edge case’y, drugorzędne przepływyNocnieKompleksowe pokrycie regresji

Praktyczny przykład: checkout e-commerce

Contract testy

  • Frontend ↔ Inventory API: Sprawdź dostępność produktu
  • Frontend ↔ Payment API: Przetwórz płatność
  • Frontend ↔ Order API: Utwórz zamówienie
  • Order API ↔ Email Service: Wyślij potwierdzenie

Runtime: 30 sekund
Częstotliwość: Każdy commit

Testy integracyjne

  • POST /orders tworzy zamówienie w bazie danych
  • Integracja bramki płatności (tryb testowy)
  • Serwis emailowy wysyła potwierdzenie (mock SMTP)

Runtime: 2 minuty
Częstotliwość: Każdy commit

Testy E2E (krytyczne ścieżki)

  • Użytkownik-gość kończy checkout z płatnością kartą
  • Zalogowany użytkownik kończy checkout z zapisaną kartą

Runtime: 5 minut
Częstotliwość: Każde wdrożenie (10–20 razy/dzień)

Testy E2E (rozszerzone)

  • Checkout z kodem rabatowym
  • Checkout z produktem brakującym w magazynie
  • Checkout z odrzuconą płatnością
  • Checkout z wygasłą kartą

Runtime: 15 minut
Częstotliwość: Nocnie


Implementacja contract testów: przykład Pact

Oto jak zaimplementować consumer-driven contract testing z Pact.

Krok 1: Konsument definiuje kontrakt (Frontend)

// checkout.pact.test.ts
import { pact } from '@pact-foundation/pact';

describe('Kontrakt Checkout', () => {
  const provider = pact({
    consumer: 'frontend',
    provider: 'inventory-api',
  });

  it('sprawdza dostępność produktu', async () => {
    await provider.addInteraction({
      state: 'produkt istnieje',
      uponReceiving: 'request do sprawdzenia dostępności',
      withRequest: {
        method: 'POST',
        path: '/inventory/check',
        body: { productId: 'prod-123', quantity: 2 },
      },
      willRespondWith: {
        status: 200,
        body: { available: true, stock: 10 },
      },
    });

    // Wykonaj request przeciw mockowanemu providerowi
    const response = await fetch(`${provider.mockService.baseUrl}/inventory/check`, {
      method: 'POST',
      body: JSON.stringify({ productId: 'prod-123', quantity: 2 }),
    });

    const data = await response.json();
    expect(data.available).toBe(true);
  });
});

Ten test:

  1. Definiuje, czego oczekuje konsument (format request/response)
  2. Generuje plik kontraktu (JSON)
  3. Działa przeciw mockowanemu providerowi

Krok 2: Provider waliduje kontrakt (Backend)

// inventory.pact.test.ts
import { Verifier } from '@pact-foundation/pact';

describe('Kontrakt Inventory API', () => {
  it('waliduje kontrakt konsumenta', async () => {
    await new Verifier({
      provider: 'inventory-api',
      providerBaseUrl: 'http://localhost:3000', // Prawdziwe API
      pactUrls: ['./pacts/frontend-inventory-api.json'],
    }).verifyProvider();
  });
});

Ten test:

  1. Czyta kontrakt wygenerowany przez konsumenta
  2. Uruchamia prawdziwe requesty przeciw API providera
  3. Waliduje, że provider spełnia kontrakt

Jeśli provider zmienia API bez aktualizacji kontraktu, ten test failuje.


OpenAPI jako kontrakt: alternatywne podejście

Zamiast Pact możesz użyć OpenAPI (Swagger) jako kontraktu.

Krok 1: Zdefiniuj spec OpenAPI

# inventory-api.yaml
openapi: 3.0.0
paths:
  /inventory/check:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                productId:
                  type: string
                quantity:
                  type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  available:
                    type: boolean
                  stock:
                    type: integer

Krok 2: Waliduj providera przeciw specowi

Użyj Prism do walidacji API:

prism mock inventory-api.yaml &
npm test # Uruchom testy integracyjne przeciw mockowi Prism

Prism zapewnia, że twoje API przestrzega specu.

Krok 3: Waliduj konsumenta przeciw specowi

Użyj Postman lub Dredd do walidacji, że konsumenci używają API poprawnie:

dredd inventory-api.yaml http://localhost:3000

Dredd wysyła requesty zdefiniowane w specue i waliduje responses.


Powszechne pułapki

Pułapka #1: Nadmierne poleganie na contract testach

Contract testy walidują kształt API, nie zachowanie. Nie zastępuj wszystkich testów E2E contract testami.

Przykład: Payment API zwraca 200 z { "success": true }, ale faktycznie nie pobiera opłaty z karty. Contract test przechodzi. Test E2E łapie błąd.

Pułapka #2: Brak weryfikacji providera

Jeśli tylko konsumenci piszą contract testy, providerzy mogą łamać kontrakty nie wiedząc o tym.

Naprawa: Uruchom weryfikację providera w pipeline CI providera.

Pułapka #3: Nieaktualne kontrakty

Kontrakty muszą ewoluować z kodem. Jeśli kontrakt jest zaktualizowany, ale nie implementacja (lub odwrotnie), testy dostarczają fałszywą pewność.

Naprawa: Auto-generuj kontrakty z kodu (np. OpenAPI z adnotacji kodu) lub wersjonuj kontrakty explicite.


Framework decyzyjny

Użyj tego frameworka do decydowania, który typ testu napisać.

ScenariuszRekomendowany typ testu
Walidacja formatu request/response APIContract test
Walidacja, że API istnieje i odpowiadaContract test
Walidacja logiki biznesowej w backendzieTest integracyjny
Walidacja, że użytkownik może ukończyć zadanieTest E2E
Walidacja integracji 3+ serwisówTest E2E
Walidacja zachowania UITest E2E
Walidacja edge case’ów w formularzachTest jednostkowy + 1 test E2E
Walidacja użycia zewnętrznego APIContract test (jeśli wspierany) lub test integracyjny z mockiem

Podsumowanie

Contract testy i testy E2E rozwiązują różne problemy.

Contract testy:

  • Walidują kompatybilność API
  • Szybkie (sekundy)
  • Uruchamiane per commit
  • Łapią breaking changes wcześnie

Testy E2E:

  • Walidują user journeys
  • Wolne (minuty)
  • Uruchamiane per deploy (lub rzadziej)
  • Łapią błędy integracji i UI

Najlepsza strategia łączy oba:

  1. Contract testy dla wszystkich granic serwisów (szybki feedback)
  2. Testy integracyjne dla logiki backendu (średnia szybkość, wysoka pewność)
  3. Testy E2E (krytyczne ścieżki) dla kluczowych user journeys (wolne, ale niezbędne)
  4. Testy E2E (rozszerzone) dla edge case’ów (nocne lub rzadsze)

Z tą strategią dostajesz szybki feedback, wysoką pewność i możliwy do zarządzania koszt utrzymania.

To kończy serię Strategia automatyzacji. Masz teraz pełny framework do budowania i utrzymywania efektywnej automatyzacji testów:

  1. Kiedy automatyzować (i kiedy nie)
  2. Piramida testów w prawdziwych zespołach
  3. Co automatyzować najpierw (framework ROI)
  4. Strategie danych testowych
  5. Projektowanie utrzymywalnych suite’ów
  6. Niestabilne testy: źródła i naprawy
  7. Contract testy vs E2E (ten wpis)

Wkrótce: Seria Integracja CI/CD startująca 5 kwietnia 2026.


Zadanie na ten tydzień: Zidentyfikuj jeden test E2E w swoim suite walidujący integrację API między dwoma serwisami. Przekonwertuj go na contract test (Pact lub OpenAPI). Zatrzymaj jeden test E2E dla happy path. Zmierz poprawę szybkości i zdecyduj, czy contract testing jest wartościowy dla twojej architektury.