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ść.
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.
Testy kontraktowe opierają się na trzech filarach, które modelują interakcję między usługami:
Proces testowania kontraktowego jest podzielony na dwa główne etapy, które odbywają się niezależnie w potokach CI/CD obu usług:
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.
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
});
});
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();
});
});
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ę.