SSD Nodes Learn 🎉 VPS od $5.50/mies.
Przewodniki Matt ConnorAutor: Matt Connor

Własny serwer do mockowania i testowania API na VPS

Dowiedz się, jak samodzielnie hostować WireMock i Hurl na VPS. Zapewnij trwałość raportów testowych i niezależność od zewnętrznych usług dzięki własnej infrastrukturze CI.

Dwa zadania korzystające z jednego repozytorium

Samodzielnie hostowane mockowanie API oraz testowanie to dwa odrębne zadania, a traktowanie ich jako jednego prowadzi do niepotrzebnej straty czasu. Serwer mockujący zastępuje zależność, której nie można wywołać z poziomu CI: dostawcę płatności, API partnera, upstream z nałożonym limitem zapytań lub usługę, której inny zespół jeszcze nie udostępnił. Narzędzie do testowania API wywołuje własne punkty końcowe w ustalonej kolejności i weryfikuje odpowiedzi, przekazując wartości z jednej odpowiedzi do kolejnego żądania.

Te dwa obszary nie pokrywają się. Serwer mockujący nigdy nie raportuje wyniku pozytywnego ani negatywnego. Narzędzie testowe nie ocenia, co zwraca dostawca płatności w przypadku odrzucenia karty. Większość zespołów, które już wynajmują serwer, kończy na uruchomieniu obu rozwiązań, zainicjowanych przez ten sam plik Docker Compose i poddawanych przeglądowi w ramach tego samego pull request.

Dlaczego warto samodzielnie hostować narzędzia do mockowania i testowania API?

Twoje dane testowe (fixtures) odzwierciedlają strukturę danych produkcyjnych. Treść żądania w teście API to rzeczywisty rekord klienta z podmienioną nazwą lub – jeśli nikt tego nie zweryfikował – z nazwą oryginalną. Zarejestrowane makiety (stubs) są jeszcze gorsze: nagrywanie przez proxy zapisuje wszystko, co zwrócił upstream. W rezultacie katalog z makietami zawiera aktywne tokeny i adresy e-mail klientów, dopóki ktoś nie przejrzy każdego pliku z osobna. W przypadku usług hostowanych zewnętrznie, takie dane stają się cudzym incydentem bezpieczeństwa i Twoim obowiązkiem zgłoszenia wycieku.

Drugim powodem jest osiągalność. Usługa powiązana z prywatnym adresem IP jest niedostępna dla hostowanego runnera, co uniemożliwia wykonanie testu. Każde obejście tego problemu generuje koszty. Upublicznienie API w Internecie w celu jego przetestowania niweluje sens utrzymywania go w sieci prywatnej. Tunel lub publiczna kopia stagingowa to kolejne systemy do utrzymania, a kopia stagingowa z czasem przestaje być spójna z produkcją pomiędzy kolejnymi wydaniami. Runner działający w tej samej sieci prywatnej komunikuje się z usługą bezpośrednio i nie wymaga żadnych dodatkowych rozwiązań. Jest to praktyczny argument przemawiający za samodzielnie hostowanym runnerem GitHub Actions.

Który serwer mock typu self-hosted wybrać?

Każde z poniższych rozwiązań działa jako kontener na własnym serwerze. Kluczowe pytanie dotyczy tego, co jest traktowane jako źródło prawdy, ponieważ od tego zależy, czy przebudowa kontenera będzie bezkosztowa, czy zajmie całe popołudnie.

  • WireMock przechowuje każdy stub jako plik JSON w katalogu mappings/, a duże treści odpowiedzi w __files/. Obraz to wiremock/wiremock, jego katalog główny wewnątrz kontenera to /home/wiremock, działa również jako proxy nagrywające. Pliki na dysku oznaczają, że mock żyje w git tak samo jak każdy inny kod.
  • Mockoon CLI przechowuje całe API mock w jednym pliku danych JSON. Zainstaluj je za pomocą npm install -g @mockoon/cli i uruchom przez mockoon-cli start --data ./data-file.json lub uruchom obraz mockoon/cli z zamontowanym plikiem. Aplikacja desktopowa edytuje ten sam plik, więc projektowanie w interfejsie graficznym i commitowanie wyników pozostają w pełni kompatybilne.
  • MockServer działa z obrazu mockserver/mockserver i nasłuchuje na porcie 1080. Oczekiwania (expectations) są przesyłane przez jego własne API REST, co jest wygodne w kodzie testowym, ale ryzykowne w środowisku produkcyjnym: oczekiwanie utworzone przez wywołanie HTTP znika po restarcie kontenera. Użyj pliku inicjalizacyjnego JSON dla stubów, które mają być trwałe.
  • Prism buduje mock na podstawie dokumentu OpenAPI, zamiast korzystać z oddzielnych plików stubów. Zainstaluj go za pomocą npm install -g @stoplight/prism-cli, a następnie uruchom prism mock openapi.yaml. Wewnątrz kontenera dodaj -h 0.0.0.0, ponieważ Prism domyślnie wiąże się z localhost i w przeciwnym razie jest nieosiągalny z zewnątrz kontenera.
  • Microcks to rozwiązanie rozbudowane: interfejs webowy, który importuje dokumenty OpenAPI oraz kolekcje Postman, a następnie serwuje je jako mocki i wykonuje testy kontraktowe. Pełna instalacja wymaga MongoDB i Keycloak, a także Kafka dla funkcji asynchronicznych. Obraz typu all-in-one microcks-uber zawiera wbudowaną bazę MongoDB działającą w pamięci RAM, co w dokumentacji projektu jest wskazane jako rozwiązanie do zastosowań efemerycznych. Dlatego wszystko, co zostało utworzone w tym interfejsie, należy traktować jako tymczasowe, a artefakty źródłowe przechowywać w git.

Który self-hosted runner testów API wybrać?

Zadanie polega na wykonaniu sekwencji: uwierzytelnienie, utworzenie zamówienia, odczytanie go i weryfikacja zmiany stanu. Wymaga to przechwycenia wartości z jednej odpowiedzi i użycia jej w kolejnym żądaniu. Narzędzie, które nie potrafi przenosić stanu między wywołaniami, jest jedynie narzędziem do sprawdzania dostępności (health check), a nie testowania API.

  • Hurl uruchamia pliki tekstowe z żądaniami HTTP z poziomu pojedynczego pliku binarnego. Sekcja [Captures] pobiera wartości z odpowiedzi, sekcja [Asserts] weryfikuje je, a --test przekształca całość w runner testów z podsumowaniem i kodem wyjścia. Wersja 8.0.1 jest aktualna na sierpień 2026.
  • Bruno CLI uruchamia folder z plikami .bru. Instalację przeprowadza się przez npm install -g @usebruno/cli, a uruchomienie przez bru run folder --env Local --reporter-junit results.xml. Format kolekcji oparty na plikach tekstowych w katalogu sprawia, że różnice (diffy) są czytelne podczas przeglądu kodu.
  • Newman uruchamia kolekcje Postman poza środowiskiem Postman: npm install -g newman, a następnie newman run collection.json -r cli,junit --reporter-junit-export results.xml. Problemem jest format. Kolekcja to jeden wyeksportowany plik JSON, więc edycja odbywa się w Postmanie, a plik w repozytorium git jest kopią, która szybko staje się nieaktualna.
  • Schemathesis to inny rodzaj weryfikacji. Odczytuje schemat OpenAPI i generuje przypadki testowe, które próbują wywołać odpowiedzi uznane przez schemat za niemożliwe: uvx schemathesis run https://your.api/openapi.json. Narzędzie wykrywa awarie i naruszenia kontraktu, ale nie posiada wiedzy o regułach biznesowych, dlatego powinno uzupełniać skryptowy zestaw testów, a nie go zastępować.
  • Hoppscotch w wersji self-hosted to opcja z interfejsem webowym, wymagająca instancji Postgres. Przed instalacją należy rozważyć ten kompromis: kolekcje przechowywane są w bazie danych, a nie w repozytorium.

Czego unikać. Step CI wciąż pojawia się w zestawieniach narzędzi, a jego format YAML jest czytelny, jednak ostatni commit w repozytorium pochodzi z sierpnia 2024. Program pośredniczący między CI a API jest niewłaściwym miejscem dla nieutrzymywanego kodu.

Umieszczenie serwera mock za firewallem

Poniższa konfiguracja uruchamia WireMock jako zastępstwo dla dostawcy płatności. Jeśli format pliku compose jest nowy, Docker Compose na VPS omawia polecenia cyklu życia, które zakłada ta sekcja.

services:
  mock-payments:
    image: wiremock/wiremock:3.13.2
    command: ["--verbose"]
    volumes:
      - ./mocks/payments:/home/wiremock
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped

Przedrostek 127.0.0.1: przy porcie jest kluczowy. Zwykłe 8080:8080 publikuje mock na każdym interfejsie, w tym na publicznym adresie IP. Pozostaje on osiągalny nawet przy zablokowaniu portu przez ufw, ponieważ Docker zapisuje własne reguły w łańcuchu iptables DOCKER, które są sprawdzane przed regułami INPUT w ufw. Należy powiązać usługę z adresem loopback lub prywatnym adresem interfejsu, aby jądro systemu odrzucało połączenia z zewnątrz.

Testowana usługa wskazuje następnie na mock. Gdy usługa działa w tym samym projekcie compose, bazowy URL mocka to http://mock-payments:8080, ponieważ compose rozwiązuje nazwy usług wewnątrz własnej sieci. Gdy usługa działa na hoście, jest to http://127.0.0.1:8080. Należy to ustawić za pomocą zmiennej środowiskowej, nigdy w kodzie, aby URL testowy nie trafił na produkcję.

Stubs umieszcza się w ./mocks/payments/mappings/, po jednym pliku JSON na każdy.

{
  "request": {
    "method": "POST",
    "urlPath": "/v1/charges",
    "bodyPatterns": [{ "matchesJsonPath": "$.amount" }]
  },
  "response": {
    "status": 201,
    "headers": { "Content-Type": "application/json" },
    "jsonBody": { "id": "ch_test_001", "status": "succeeded", "amount": 4200 }
  }
}

Uruchom usługę, a następnie sprawdź, co faktycznie zostało wczytane.

docker compose up -d --wait mock-payments
curl -fsS http://127.0.0.1:8080/__admin/mappings

--wait blokuje wykonanie do momentu, aż kontener zgłosi stan healthy. Działa to, ponieważ obraz WireMock zawiera HEALTHCHECK dla endpointu /__admin/health. Wywołanie mappings wyświetla listę wszystkich stubów odczytanych przez serwer. Stub, który został utworzony, ale nie pojawia się na liście, nie został wczytany: sprawdź, czy plik znajduje się w mappings/, a nie w katalogu głównym montowania, oraz czy składnia JSON jest poprawna.

Gdy nadejdzie żądanie, do którego nie pasuje żaden stub, WireMock odpowiada 404 z treścią zaczynającą się od Request was not matched, po której następuje różnica (diff) względem najbardziej zbliżonego stuba. Przed wprowadzeniem zmian należy przeanalizować ten diff, ponieważ wskazuje on dokładne pole, które powoduje rozbieżność. Zazwyczaj jest to ścieżka z /v1/charge, podczas gdy stub zawiera /v1/charges.

Zapisywanie testu jako sekwencji z przenoszeniem stanu między wywołaniami

Pliki Hurl są plikami tekstowymi. Zainstaluj pakiet deb z wydań projektu.

VERSION=8.0.1
curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VERSION/hurl_${VERSION}_amd64.deb
sudo apt update && sudo apt install ./hurl_${VERSION}_amd64.deb

Zestaw testów sprawdzający własne API względem moka znajduje się w tests/checkout.hurl.

POST {{base_url}}/orders
Content-Type: application/json
{
  "sku": "ssd-1tb",
  "amount": 4200
}
HTTP 201
[Captures]
order_id: jsonpath "$['id']"

GET {{base_url}}/orders/{{order_id}}
HTTP 200
[Asserts]
jsonpath "$.status" == "paid"
jsonpath "$.charge_id" == "ch_test_001"

Blok [Captures] sprawia, że jest to test API, a nie dwa niezależne żądania. Wartość order_id jest odczytywana z pierwszej odpowiedzi i interpolowana do adresu URL drugiego żądania. Asercja w charge_id stanowi cel całego ćwiczenia: dowodzi ona, że usługa wywołała dostawcę płatności i zapisała otrzymaną odpowiedź, a porównywana wartość jest tą, która została wpisana do stuba WireMock. Jeden plik obejmuje teraz obie części procesu.

hurl --test --variable base_url=http://127.0.0.1:3000 \
  --report-junit reports/junit.xml \
  --report-json reports/json \
  tests/

Poprawne wykonanie testu wypisuje jedną linię na plik oraz podsumowanie.

tests/checkout.hurl: Success (2 request(s) in 61 ms)
Executed files:    1
Executed requests: 2 (30.1/s)
Succeeded files:   1 (100.0%)
Failed files:      0 (0.0%)
Duration:          64 ms

W przypadku błędu wypisywany jest komunikat error: Assert failure wraz z nazwą pliku i numerem linii, a następnie otrzymana wartość w zestawieniu z oczekiwaną. Polecenie hurl kończy działanie z kodem wyjścia innym niż zero, co zatrzymuje proces CI. Jeśli status odczytuje pending w miejscu, gdzie oczekiwano paid, oznacza to, że usługa nie przetworzyła odpowiedzi moka. Kolejnym krokiem diagnostycznym jest sprawdzenie dziennika żądań WireMock pod adresem /__admin/requests, który wykaże, czy wywołanie w ogóle dotarło do moka.

Uruchamianie pakietu testów z własnego CI runnera

Jeśli runner jest zarejestrowany na tej samej maszynie, przepływ pracy jest krótki. Runner działa jako zwykły proces na hoście, więc docker oraz hurl muszą być zainstalowane na tym hoście. Żadne środowisko nie jest dziedziczone z hostowanego obrazu.

name: api-tests
on: [push]
jobs:
  hurl:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - name: Start the mock
        run: docker compose up -d --wait mock-payments
      - name: Run the suite
        run: hurl --test --variable base_url=http://127.0.0.1:3000 --report-junit reports/junit.xml tests/
      - name: Archive the reports
        if: always()
        run: install -d /srv/api-tests/reports/$GITHUB_SHA && cp -r reports/. /srv/api-tests/reports/$GITHUB_SHA/
      - name: Stop the mock
        if: always()
        run: docker compose down

if: always() w kroku archiwizacji ma kluczowe znaczenie. Bez tego parametru nieudany przebieg testów pomija kopiowanie, co powoduje utratę raportu, który jest niezbędny do analizy. Kopia musi również trafić poza obszar roboczy, ponieważ runner czyści ten obszar przed kolejnym zadaniem, co skutkuje usunięciem raportów.

Przechowywanie wyników, a nie tylko ostatniego uruchomienia

Plik JUnit XML dla każdego commita odpowiada na jedno pytanie: czy test przeszedł. Nie wyjaśnia on jednak, kiedy dany endpoint zaczął działać wolniej, ponieważ nikt nie analizuje tych plików po ich zamknięciu. Aby śledzić trendy, należy dopisywać jeden wiersz dla każdego uruchomienia do niewielkiej bazy danych na tym samym serwerze. Wystarczy pojedyncza tabela przechowująca commit SHA, nazwę pliku, liczbę sukcesów, liczbę błędów oraz czas trwania. SQLite w środowisku produkcyjnym na VPS jest odpowiednim rozwiązaniem: jeden plik, brak procesu serwera, a cała historia jest uwzględniona w wykonywanych już kopiach zapasowych. Należy parsować wyjście --report-json narzędzia Hurl zamiast formatu JUnit XML, ponieważ jest to format lepiej przystosowany do odczytu maszynowego.

Co musi przetrwać przebudowę kontenera

Definicje mocków i zestawy testów to kod źródłowy. Powinny znajdować się w repozytorium obok usługi, którą opisują, i być zmieniane w tym samym pull request, który modyfikuje endpoint. Stub edytowany w interfejsie webowym lub oczekiwanie przesłane do MockServer przez jego API REST w czasie wykonywania, istnieje tylko w pamięci kontenera lub bazie danych danego narzędzia. Uruchomienie docker compose down powoduje ich utratę, a nikt nie zauważy problemu, dopóki test nie zacznie przechodzić z niewłaściwych powodów. Jeśli repozytoria działają również na własnym sprzęcie, własny serwer git utrzymuje fixture'y i usługę wewnątrz jednej granicy zaufania.

Następnie zasady praktyczne. Należy przypinać tagi obrazów, ponieważ latest może zmienić sposób, w jaki mock dopasowuje żądania bez żadnych zmian w repozytorium, a taką awarię bardzo trudno powiązać z przyczyną. Katalogi ze stubami należy montować w trybie tylko do odczytu, jeśli narzędzie nie musi do nich zapisywać. Nigdy nie należy umieszczać stubów mocka w nazwanym wolumenie Docker, ponieważ wolumen staje się wtedy źródłem prawdy, a kopia w git po cichu staje się nieaktualna.

Jeszcze jedna zasada, która często sprawia kłopoty. Jeśli stuby tworzone są przez rejestrowanie rzeczywistego ruchu przez proxy, należy przeczytać każdy wygenerowany plik przed jego zatwierdzeniem (commit). Nagranie zawiera dokładnie to, co zwrócił upstream, w tym tokeny bearer oraz adresy e-mail klientów. Zatwierdzenie takiego pliku umieszcza te dane w repozytorium na stałe, ponieważ git przechowuje usuniętą zawartość w historii.

FAQ

Jaka jest różnica między serwerem mock API a narzędziem do uruchamiania testów API?

Serwer mock odpowiada na żądania. Zastępuje zależność, której nie można wywołać z poziomu CI i nigdy nie raportuje wyniku pozytywnego ani negatywnego. Narzędzie do uruchamiania testów API wysyła żądania do własnej usługi, weryfikuje odpowiedzi, przekazuje wartości między kolejnymi wywołaniami i kończy działanie z kodem błędu, gdy asercja nie zostanie spełniona. Rozwiązują one różne problemy, a typowa konfiguracja obejmuje oba te elementy jednocześnie: narzędzie testowe wywołuje usługę, podczas gdy usługa wywołuje mock.

Czy mogę testować wewnętrzne API z hostowanego narzędzia CI?

Nie bez jego wystawienia. Hostowane narzędzie znajduje się poza siecią, więc nie może połączyć się z usługą powiązaną z prywatnym adresem. Dostępne opcje to publikacja API, uruchomienie tunelu lub utrzymywanie publicznej kopii stagingowej, a każda z nich dodaje system, który może ulec awarii lub wyciekowi. Narzędzie uruchomione w tej samej prywatnej sieci wywołuje usługę bezpośrednio, co jest głównym praktycznym powodem, dla którego zespoły decydują się na self-hosting tych zadań.

Gdzie powinny znajdować się makiety (mock stubs) i zestawy testów API?

W systemie git, obok usługi, którą opisują. Narzędzia przechowujące definicje jako pliki, takie jak katalog mappings/ w WireMock, plik danych Mockoon, pliki Hurl oraz folder .bru w Bruno, umożliwiają przegląd kodu i przebudowę kontenera bez dodatkowych kosztów. Narzędzia przechowujące definicje w bazie danych lub interfejsie webowym wymagają planu kopii zapasowej i kroku eksportu, a o eksporcie zapomina się zazwyczaj do momentu utraty kontenera.

Dlaczego mój mock zwraca 404, mimo że stub wygląda poprawnie?

WireMock serwuje stub tylko przy dokładnym dopasowaniu. Niedopasowane żądanie otrzymuje 404 z treścią zaczynającą się od Request was not matched, po której następuje różnica (diff) względem najbliższego stuba, wskazująca pole, które nie pasuje. Częstymi przyczynami są ukośnik na końcu ścieżki, nagłówek Content-Type wymagany przez stub, którego klient nie wysłał, użycie urlPath tam, gdzie stub wymaga urlPathPattern dla segmentu zmiennego, oraz dopasowanie treści (body matcher), które nie pasuje do ładunku. W pierwszej kolejności sprawdź /__admin/requests, aby potwierdzić, czy żądanie w ogóle dotarło do mocka.

Czy nadal potrzebuję mocków, jeśli mam środowisko stagingowe?

Tak, z dwóch powodów. Kopia stagingowa zewnętrznej usługi, nad którą nie masz kontroli, również może ulec awarii lub nałożyć limity zapytań, przez co zestaw testów zakończy się niepowodzeniem z przyczyn niezależnych od kodu. Środowisko to nie jest również w stanie wygenerować odpowiedzi, które są najbardziej potrzebne do testów, takich jak odrzucona karta czy przekroczenie czasu bramki (gateway timeout). Mock zwraca takie odpowiedzi na żądanie z prędkością sieci lokalnej, co skraca czas trwania zestawu testów z minut w środowisku sandbox do sekund. Środowisko stagingowe należy zachować do ostatecznej weryfikacji przed wydaniem wersji, a w CI stosować mocki.