Standardem w tworzeniu systemów stają się mikroserwisy, w których działanie aplikacji jest często zależne od dostępności innych aplikacji. Nastręcza to problemów choćby z odtworzeniem dość specyficznych błędów w procesach, w których uczestniczą inne serwisy. Trudnością jest zasymulowanie zachowania systemu, gdy serwis za długo nie odpowiada lub połączenie zostało przerwane w trakcie. Podobnie jest z potrzebą sprawdzenia lokalnie działania całego procesu.
Mock-server – do czego służy
Tutaj z pomocą może nam przyjść mock-server. Jest to projekt, który umożliwia zasymulowanie działanie innego serwisu oraz łatwe konfigurowanie oczekiwanych odpowiedzi statycznych i dynamicznych, zależnych od danych, jakie przyjdą w żądaniu.
Mock-server wspiera:
- protokoły http,
- https,
- połączenie websocket.
Przy połączeniu SSL domyślnie jest używany ich certyfikat. Oczywiście, można wgrać też własny, co zostanie wyjaśnione w dalszej części artykułu. Aplikacja udostępnia także skromny interfejs użytkownika, pod którym da się podejrzeć aktualną konfigurację i logi, a także po nich filtrować.
Ponadto, mock-server udostępnia API umożlwiające zarządzanie i konfigurację. Opisałem tu tylko część funkcjonalności – więcej znajdziecie w oficjalnej dokumentacji.
Podstawowe informacje dot. uruchomienia i integracji
Mock-server można uruchomić lub zintegrować na wiele sposobów:
- Przy użyciu obrazu Dockera.
- Z użyciem menadżera paczek helm w środowisku Kubernetesa.
- Jako część procesu budowania w Maven i Grunt.
- Bezpośrednio w aplikacji Java
- w testach Junit4 wykorzystując adnotację @Rule,
- w Junit5 z wykorzystaniem adnotacji @ExtendWith,
- w Spring Test Execution Listener z adnotacją @MockServerTest.
- Uruchomienie przez Node.js (npm).
- Bezpośrednio z linii komend za pomocą
- Homebrew,
- uruchomienie pliku jar,
- Mavena,
- Dockera.
W artykule przedstawię jak:
- uruchomić mock-server,
- skonfigurować oczekiwane odpowiedzi,
- użyć własnego certyfikatu,
- wymusić mTls.
Uruchamiamy mock-server
Uruchomienie z linii komend z użyciem java jest moim zdaniem najszybszym i najprostszym sposobem. Wymagane jest tylko posiadanie zainstalowanej javy w wersji 8+ i pobranie wersji mock-servera z wbudowanym serwerem.
Dostępne wersje mock-servera z wbudowanym serwerem netty można znaleźć tutaj. Tylko wersje z suffixem shaded (starsze wersje mają suffix jar-with-dependecies) zawierają wszystkie niezbędne zależności potrzebne do uruchomienia.
Na potrzeby artykułu pobrałem plik mockserver-netty-5.12.0-shaded.jar. Po pobraniu pliku jar, w zależności od zainstalowanej wersji javy, należy wywołać jedną z poniższych komend w folderze w którym znajduje się nasza aplikacji.
- Komenda dla javy 8
java -jar mockserver-netty-5.12.0-shaded.jar -serverPort 1080,1081 -logLevel INFO
- Komenda dla javy 9+
java --add-exports=java.base/sun.security.x509=ALL-UNNAMED --add-exports=java.base/sun.security.util=ALL-UNNAMED -jar mockserver-netty-5.12.0-shaded.jar -serverPort 1080,1081 -logLevel INFO
Kod mock-servera w wersji 5.12.0 nie jest w pełni dostosowany do nowej javy 9+, dlatego uruchomienie go wymagało podania dwóch dodatkowych argumentów:
--add-exports=java.base/sun.security.x509=ALL-UNNAMED
--add-exports=java.base/sun.security.util=ALL-UNNAMED
Ponadto, zostały podane parametry stricte związane z samym mock-serverem
pARAMETR | OPIS |
---|---|
-serverPort <lista portów po przecinki> | Można podać jeden lub więcej portów na jakich ma nasłuchiwać mock-server |
-logLevel <poziom> | Poziom logów, jakie będą pisane do konsoli. Można ustawić: INFO, WARN, DEBUG, TRACE |
-Dmockserver.logLevel=OFF | Wyłączenie pisania logów z klas mock-servera |
-Droot.logLevel=OFF | Całkowite wyłączenie logowania |
Weryfikacja działania aplikacji
Najprostszym sposobem sprawdzenia czy nasza aplikacja działa, jest wejście na stronę, gdzie udostępniono dashboard mock-servera, w którym możemy podejrzeć logi, filtrować po nich, a także zobaczyć aktualną konfigurację odpowiedzi mock-servera.
Tworzenie statycznej oczekiwanej odpowiedzi
Domyślnie aplikacja zwraca 404 na każdy request. Odpowiedzi można skonfigurować przez api mock-servera. Opisy punktów krańcowych (endpointów) są dostępne do sprawdzenia. Można także wczytać odpowiedzi z pliku z użyciem statycznych odpowiedzi, wykorzystując OpenApi w wersji trzeciej lub Apache Velocity.
W artykule zaproponowałem inicjalizację odpowiedzi z pliku, wykorzystując składnie mock-servera do statycznych odpowiedzi. Należy stworzyć plik z rozszerzeniem json, w którym znajduje się lista ze statycznymi odpowiedziami. W moim przypadku jest to plik init.json.
[{
"httpRequest": {
"method":"GET",
"path": "/some/path"
},
"httpResponse": {
"headers": {
"content-type": ["applications/json"]
},
"body": {
"field":"value",
"field":"value2"
}
}]
}}]
Następnie, musimy uruchomić mock-server, dodając dodatkowe parametry:
-Dmockserver.initializationJsonPath=<ścieżka do pliku>
-Dmockserver.watchInitializationJson=true
Dmockserver.initializationJsonPath wskazuje na ścieżkę z plikiem konfiguracyjnym. Natomiast Dmockserver.watchInitializationJson=true co 5 sekund sprawdza, czy w pliku nie nastąpiły zmiany, dzięki czemu nie ma potrzeby restartowania aplikacji, gdy modyfikujemy oczekiwane odpowiedzi.
Poniżej znajdziecie pełne komendy. Należy podmienić <ścieżka do pliku konfiguracyjnego> na pełną lub relatywną ścieżkę do wcześniej utworzonego pliku.
- Komenda dla javy 8
java -Dmockserver.initializationJsonPath=init.json -Dmockserver.watchInitializationJson=true -jar mockserver-netty-5.12.0-shaded.jar -serverPort 1080,1081 -logLevel INFO
- Komenda dla javy 9+
java --add-exports=java.base/sun.security.x509=ALL-UNNAMED --add-exports=java.base/sun.security.util=ALL-UNNAMED -Dmockserver.initializationJsonPath=init.json -Dmockserver.watchInitializationJson=true -jar mockserver-netty-5.12.0-shaded.jar -serverPort 1080,1081 -logLevel INFO
Następnie wystarczy wykonać żądanie GET pod adres http://localhost:1080/some/path, a w odpowiedzi otrzymamy jsona :
{
„field”:”value”,
„field”:”value2”
}
Sekcje httpRequest i httpResponse
W przykładzie konfiguracji odpowiedzi widać dwie sekcje:
- httpRequest,
- httpResponse.
Sekcja httpRequest zawiera konfigurację, dla jakiego żądania ma być zwrócona odpowiedź z sekcji httpResponse. Można filtrować po ścieżce, metodzie, parametrach i wielu innych.
Aby dowiedzieć się więcej, polecam zajrzeć do dokumentacji w sekcji „Creating Expectations”.
W sekcji httpResponse jest definicja dokładnej odpowiedzi. Oprócz jej ciała można zdefiniować:
- status odpowiedzi,
- nagłówki,
- ciasteczka,
- a także symulować opóźnienia występujące w sieci.
Opis wszystkich możliwości znajdziecie na stronie mock-server.
Ważne! Należy pamiętać, że lista odpowiedzi jest analizowana od góry do dołu i brana jest pierwsza pasująca odpowiedź według kryteriów zdefiniowanych w sekcji httpRequest.
Konfiguracja w osobnym pliku
Przy większej ilości parametrów wygodniejsze będzie przeniesienie konfiguracji do osobnego pliku properties. W moim przypadku będzie to plik mock-server.properties. Poniżej znajdziecie jego treść.
Nie ma tam jedynie parametru -serverPort 1080,1081, który w dalszym ciągu musi być podany jako parametr uruchomieniowy.
mockserver.logLevel=INFO
mockserver.disableLogging=false
mockserver.disableSystemOut=false
mockserver.initializationJsonPath=init.json
mockserver.watchInitializationJson=true
Doszedł nam jeden nowy parametr:
- -Dmockserver.propertyFile=<ścieżka do pliku konfiguracyjnego>
W folderze, z którego uruchamiamy aplikację, powinny znajdować się 3 pliki:
- mockserver-netty-5.12.0-shaded.jar
- properties
- json
Komenda do uruchomienia aplikacji wygląda następująco:
- Komenda dla javy 8
java -Dmockserver.propertyFile=mockserver.properties -jar mockserver-netty-5.12.0-shaded.jar -serverPort 1080,1081
- Komenda dla javy 9+
java --add-exports=java.base/sun.security.x509=ALL-UNNAMED --add-exports=java.base/sun.security.util=ALL-UNNAMED -Dmockserver.propertyFile=mockserver.properties -jar mockserver-netty-5.12.0-shaded.jar -serverPort 1080,1081
Jeśli nie zmodyfikowaliśmy init.json, to po uruchmonieniu aplikacji mock-server i wykonaniu żądania http://localhost:1080/some/path o typie GET, w odpowiedzi otrzymamy jsona o treści:
{
„field”:”value”,
„Field”:”value2”
}
Tworzenie dynamicznej odpowiedzi
Odpowiedzi dynamiczne można stworzyć na dwa sposoby. Jednym z nich jest wykorzystanie javascriptTemaplate, a drugim dodanie do ścieżki klasy pliku jar zawierającego klasę implementującą interfejs org.mockserver.mock.action.ExpectationResponseCallback.
W artykule przedstawię tylko drugą metodę. Związane jest to z usunięciem z javy 15+ silnika nashrom, w związku z czym pierwszy sposób nie zadziała już na javie wyższej niż 15.
Do stworzenia klasy wystarczy nam zwykły edytor tekstowy. Można też użyć IDE – w tym wypadku polecam dodać wcześniej użytą wersję aplikacji mock-server jako bibliotekę, aby w IDE działało automatyczne podpowiadanie składni. Aplikacja zostanie skompilowana i zbudowana przy użyciu javac, które jest dostępne wraz zainstalowaną javą.
W moim przypadku wykorzystem Inteljii Idea Community Edition. Tworzę przy jego użyciu pusty projekt, w nim folder libs i kolejno umieszczam w nim plik jar z mock-servera. U mnie jest to mockserver-netty-5.12.0-shaded.jar. Dodaję ten plik jako zewnętrzną bibliotekę projektu. Dostępny jest także poradnik jak to zrobić w Idea Community Edition.
W projekcie tworzymy klasę mockserver.response.SampleResponse implementującą interfejs org.mockserver.mock.action.ExpectationResponseCallback i metodę public HttpResponse handle(HttpRequest httpRequest). Klasa HttpResponse implementuje wzorzec budowniczy, dzięki czemu możemy szybko stworzyć oczekiwaną odpowiedź.
W przykładzie w odpowiedzi zwrócimy to samo, co zostało nam przekazane w żądaniu.
package mockserver.response;
import org.mockserver.mock.action.ExpectationResponseCallback;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
public class SampleResponse implements ExpectationResponseCallback {
@Override
public HttpResponse handle(HttpRequest httpRequest) throws Exception {
return HttpResponse.<em>response</em>()
.withBody(httpRequest.getBodyAsString())
.withStatusCode(200);
}
}
Stworzyłem także drugą klasę, która w odpowiedzi zwraca to samo tylko ze statusem 500.
package mockserver.response;
import org.mockserver.mock.action.ExpectationResponseCallback;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
public class SampleResponse2 implements ExpectationResponseCallback {
@Override
public HttpResponse handle(HttpRequest httpRequest) throws Exception {
return HttpResponse.<em>response</em>()
.withBody(httpRequest.getBodyAsString())
.withStatusCode(500);
}
}
Sama struktura projektu powinna wyglądać następująco:
Następnym krokiem jest skompilowanie naszych dwóch klas i wygenerowanie paczki jar. Kod można skompilować wywołując komendę:
javac -cp .\lib\mockserver-netty-5.12.0-shaded.jar .\mockserver\response\*.java -d out
W rezultacie zostaną skompilowane wszystkie klasy w pakiecie mockserver.response, a wynik kompilacji znajdzie się w folderze out. Powinno wyglądać to następująco:
Następnie wywołujemy polecenie, które spakuj to w plik jar. Należy to zrobić z poziomu projektu. Zależnie jakiej linii komend używacie, trzeba będzie wykonać jedno z poleceń:
- Windows powershell
cd out; jar -cf ../msLib.jar .; cd ..
- Windows CMD
cd out & jar -cf ../msLib.jar . & cd ..
- Linux
cd out && jar -cf ../msLib.jar . && cd ..
W rezultacie pojawi nam się plik msLib.jar. W lokalizacji, z której uruchamiamy mock-server, tworzy folder libs. Umieszczamy w nim nasz plik msLib.jar. Następnie, we wcześniej utworzonym pliku init.json, dodajemy nową konfigurację, gdzie oczekiwana odpowiedź będzie zwrócona przez nasze klasy. Plik init.json powinien wyglądać następująco:
[{
"httpRequest": {
"method":"GET",
"path":"/some/path"
},
"httpResponse": {
"headers": {
"content-type": ["applications/json"]
},
"body":{
"field":"value",
"field":"value2"
}
}
},
{
"httpRequest" : {
"path" : "/myResponse1"
},
"httpResponseClassCallback" : {
"callbackClass" : "mockserver.response.SampleResponse"
}
},
{
"httpRequest" : {
"path" : "/myResponse2"
},
"httpResponseClassCallback" : {
"callbackClass" : "mockserver.response.SampleResponse2"
}
}]
Jak widać, w przypadku, gdy odpowiedź ma być dynamicznie generowana przez klasę, sekcja httpResponse zmieniła się w sekcję httpResponseClassCallback z jednym polem callbackClass, gdzie podana jest klasa wraz z pakietem.
Folder z mock-server powinien mieć następującą zawartość:
Plik mockserver.properties powinien zawierać następującą treść:
mockserver.logLevel=INFO
mockserver.disableLogging=false
mockserver.disableSystemOut=false
mockserver.initializationJsonPath=init.json
mockserver.watchInitializationJson=true
Ostatnim etapem jest uruchomienie aplikacji tak, aby wczytała do swojej ścieżki uruchomieniowej pliki z lokalizacji libs/*. Aby to osiągnąć, trzeba uruchomić mock-server, używając komendy -cp zamiast -jar. Komenda wygląda następująco.
- Java 8
java -Dmockserver.propertyFile=mockserver.properties -cp "mockserver-netty-5.12.0-shaded.jar;libs/*" org.mockserver.cli.Main -serverPort 1080,1081
- Java 9+
java --add-exports=java.base/sun.security.x509=ALL-UNNAMED --add-exports=java.base/sun.security.util=ALL-UNNAMED -Dmockserver.propertyFile=mockserver.properties -cp "mockserver-netty-5.12.0-shaded.jar;libs/*" org.mockserver.cli.Main -serverPort 1080,1081
Po uruchomieniu i wykonaniu zapytania POST pod adres otrzymamy w odpowiedzi kod 200 lub 500, a w ciele żądania to, co wysłaliśmy.
Mock-server – własny certyfikat i włączenie mTLS
Komunikacja szyfrowana jest w dzisiejszych czasach standardem i mock-server także ją wspiera. Domyślnie mock-server przedstawia się certyfikatem. Lepszym rozwiązaniem jest jednak podanie własnego certyfikatu. W artykule przedstawię, jak wygenerować własne CA, certyfikat i jak go podpisać naszym CA. Pokażę także, jak przygotować już istniejący certyfikat tak, żeby obsłużył go mock-server.
Generowanie własnego certyfikatu
Na potrzeby lokalnych testów stworzymy własne CA, a następnie certyfikat, który zostanie nim podpisany. Wszystko z użyciem openssl (na systemie Windows polecam wykorzystać do tego zadania WSL).
W pierwszym kroku w wybranej lokalizacji stworzymy certyfikat CA z użyciem poniższych komend.
openssl genrsa -des3 -out rootCA.key 4096
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt
Następnie tworzymy folder i generujemy certyfikaty dla mock-servera.
mkdir mockserver
openssl genrsa -out mockserver/mockserver.key 2048
openssl req -new -sha256 -key mockserver/mockserver.key -subj "/C=PL/ST=CA/O=MyOrgMock, Inc./CN=localhost" -out mockserver/mockserver.csr
openssl x509 -req -in mockserver/mockserver.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out mockserver/mockserver.crt -days 500 -sha256
Na nieszczęście mock-server nie jest w stanie obsłużyć naszego certyfikatu i klucza w domyślnym formacie. Musimy więc skonwertować certyfikat do formatu PEM, a klucz prywatny do PKCS#8. Można to zrobić poniższymi komendami:
openssl pkcs8 -topk8 -inform PEM -in mockserver/mockserver.key -out mockserver/mockserver.pksc8.pem -nocrypt
openssl x509 -in mockserver/mockserver.crt -out mockserver/mockserver.crt.pem -outform PEM
Ostatnim etapem jest skonwertowanie naszego certyfikatu rootCa.crt, którym podpisaliśmy certyfikat dla mock-servera, do formatu PEM. Dzięki temu mock-server będzie akceptował wszystkie certyfikaty, które są nim podpisane.
Można to zrobić poniższą komendą:
openssl x509 -in rootCA.crt -out rootCA.pem -outform PEM
Konwersja istniejącego certyfikatu
Może się zdarzyć, że posiadamy własny CA lub dostajemy gotowe certyfikaty do aplikacji, która będzie działać na hoście. W tym wypadku wystarczy przekonwertować certyfikat do formatu PEM i dodatkowo wygenerowany klucz skonwertować do PKCS#8. Poniżej przykład konwersji certyfikatu w formacie p12.
openssl pkcs12 -in path.p12 -out newfile.crt.pem -clcerts -nokeys
openssl pkcs12 -in path.p12 -out newfile.key.pem -nocerts -nodes
openssl pkcs8 -topk8 -inform PEM -in newfile.key.pem -out newfile.key.pkcs8.pem -nocrypt
Trzeba także dodać CA, którym zostały podpisane nasze certyfikaty. W tym wypadku wystarczy go tylko skonwertować do formatu PEM. Poniżej przykład konwersji z formatu .cer do .pem
openssl x509 -inform der -in certificate.cer -out certificate.pem
Konfiguracje mtls
Gdy już posiadamy certyfikaty w poprawnych formatach, umieszczamy je w folderze config, który należy utworzyć w tej samej lokalizacji, co naszą aplikację mock-servera. Będą to następujące pliki:
rootCA.pem
mockserver/mockserver.crt.pem
mockserver/mockserver.pksc8.pem
Do pliku mockserver.properties należy dodać dodatkowe parametry, z zachowaniem tych, które zostały dołączone we wcześniejszej części artykułu.
#Wymuszenie mutualTLS
mockserver.tlsMutualAuthenticationRequired=false
#Certyfikat root
mockserver.certificateAuthorityCertificate=config/rootCA.pem
#Certyfikat dla mock-server’a
mockserver.privateKeyPath=config/private_key_PKCS_8.pem
mockserver.x509CertificatePath=config/localhost.pem
Parametr mockserver.tlsMutualAuthenticationRequired wymusza mutual TLS. Po jego ustawieniu na true nie będziemy mieli możliwości łączyć się z mock-serverem po porcie http, ani po porcie https z poziomu przeglądarki.
Folder z naszą aplikacją powinien wyglądać następująco:
Aplikację uruchamiamy tak jak we wcześniejszej części artykułu z użyciem komendy:
- Dla javy 8
java -Dmockserver.propertyFile=mockserver.properties -jar mockserver-netty-5.12.0-shaded.jar -serverPort 1080
- Dla javy 9+
java --add-exports=java.base/sun.security.x509=ALL-UNNAMED --add-exports=java.base/sun.security.util=ALL-UNNAMED -Dmockserver.propertyFile=mockserver.properties -jar mockserver-netty-5.12.0-shaded.jar -serverPort 1080
Istnieje możliwość sprawdzenia, czy mock-server przedstawia się teraz naszym certyfikatem.
Kolejną rzeczą jest sprawdzenie czy mutualTLS działa poprawnie. Po zmianie parametru mockserver.tlsMutualAuthenticationRequired na true i restarcie aplikacji nie będziemy mieli możliwości wejścia na wcześniejszy adres, ponieważ nasza przeglądarka nie przedstawia się żadnym certyfikatem, a każde żądanie z niepoprawnym certyfikatem będzie odrzucane.
Do przetestowania działania mTls wykorzystam postmana. Na potrzeby testów wygenerowałem nowy certyfikat analogicznie jak dla mock-servera i podpisałem go stworzonym lokalnie CA. Można także wykorzystać certyfikat wygenerowany dla mock-servera.
Aby skonfigurować aplikację postman, należy wejść w: File -> settings -> Certificates, a następnie włączyć parametr CACertificates i podać tam nasz plik rootCA, zaś w sekcji Client certificates dodać nowy certyfikat. Należy wskazać plik CRT i klucz prywatny, a host ustawić na adres, na którym działa mock-server.
W moim przypadku wygląda to następująco:
Po wykonaniu żądania na nieskonfigurowany endpoint dostaniemy domyślną odpowiedź 404.
Podsumowanie
Mock-server daje duże możliwości stworzenia całego systemu umożliwiając lokalne sprawdzenie działania procesów, jak i możliwość stworzenie testów integracyjnych z uwzględnieniem brzegowych przypadków. W prosty sposób można także wymusić mTls, dzięki czemu jeszcze dokładniej możemy odwzorować system produkcyjny lokalnie.
***
Jeśli chcesz rozpocząć swoją przygodę z Dockerem lub interesują Cię informacje dotyczące wprowadzenia do Kubernetes, zachęcamy do przeczytania innych artykułów naszych ekspertów.
Super dawka wiedzy, dziekuje!
ps. przystojny Pan jest 🙂