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 testu | Pokrycie | Częstotliwość | Cel |
|---|---|---|---|
| Contract testy | Wszystkie granice serwisów | Każdy commit | Szybki feedback na kompatybilność API |
| Testy integracyjne | API + baza danych | Każdy commit | Walidacja logiki backendu bez UI |
| Testy E2E (krytyczne ścieżki) | Pełne user journeys | Każde wdrożenie | Pewność przepływów użytkownika |
| Testy E2E (rozszerzone) | Edge case’y, drugorzędne przepływy | Nocnie | Kompleksowe 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
/orderstworzy 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:
- Definiuje, czego oczekuje konsument (format request/response)
- Generuje plik kontraktu (JSON)
- 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:
- Czyta kontrakt wygenerowany przez konsumenta
- Uruchamia prawdziwe requesty przeciw API providera
- 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ć.
| Scenariusz | Rekomendowany typ testu |
|---|---|
| Walidacja formatu request/response API | Contract test |
| Walidacja, że API istnieje i odpowiada | Contract test |
| Walidacja logiki biznesowej w backendzie | Test integracyjny |
| Walidacja, że użytkownik może ukończyć zadanie | Test E2E |
| Walidacja integracji 3+ serwisów | Test E2E |
| Walidacja zachowania UI | Test E2E |
| Walidacja edge case’ów w formularzach | Test jednostkowy + 1 test E2E |
| Walidacja użycia zewnętrznego API | Contract 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:
- Contract testy dla wszystkich granic serwisów (szybki feedback)
- Testy integracyjne dla logiki backendu (średnia szybkość, wysoka pewność)
- Testy E2E (krytyczne ścieżki) dla kluczowych user journeys (wolne, ale niezbędne)
- 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:
- Kiedy automatyzować (i kiedy nie)
- Piramida testów w prawdziwych zespołach
- Co automatyzować najpierw (framework ROI)
- Strategie danych testowych
- Projektowanie utrzymywalnych suite’ów
- Niestabilne testy: źródła i naprawy
- 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.