Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Third-party service kommunikáció mockolása WireMockkal

Szűrő megjelenítése

Third-party service kommunikáció mockolása WireMockkal

A WireMock lényegében egy "instant" HTTP szerver, amolyan szoftvertesztelési három-az-egyben kávé, aminek célja, hogy minél egyszerűbben és minél könnyebben tudjunk létrehozni előkészített válaszokat adott HTTP kérésekre. Működési elve tehát lényegében az, hogy megadhatunk kérés patterneket, konkrét útvonalakat és HTTP metódusokat, akár header paramétereket, gyakorlatilag bármit, ami egy adott kérést meg tud határozni, majd ehhez hozzárendelünk egy választ, amit a szerver vissza kell adjon. A WireMock szervere fogadja a kéréseket, majd igyekszik megtalálni a regisztrált request-response párok között azt az egyet, ami pontosan illik arra.

Standalone használat

Konfigurálása és használata két különböző módon lehetséges, előbb a standalone futtatást mutatom be. A standalone mód tipikusan olyan esetekben hasznos, ha például komponensünk fejlesztése közben állandóan szükségünk van egy mock-szerverre, amire az alkalmazásunk kéréseket tud küldeni, és alapvetően még nem azt szeretnénk verifikálni, hogy a válaszok feldolgozása helyes (persze ezt látni fogjuk, csak nem automatizált módon), hanem csupán szükségünk van előre definiált válaszokra. Ilyen módon az alkalmazás logikája könnyen igazíthatóvá válik a visszatérő válaszhoz, anélkül, hogy véletlenszerű eltérésekre, hiányos válaszokra, vagy akár időközben megszűnő erőforrásokra kellene számítanunk - persze, azért jobb, ha ilyen esetekre is készítünk néhány konfigurációt hogy azokra is felkészítsük az alkalmazás logikáját. Tipikusan jó felhasználási cél lehet az, ha a mock-szerver authentikációt mockol és mindig egy érvényes JWT tokent ad vissza, a tényleges authentikációs folyamat nélkül (természetesen szigorúan csak a fejlesztés alatt). Akár az is megoldható, hogy adott authentikációs kéréshez (annak request body-ja alapján) adott választ rendelünk hozzá, így az adott fake-felhasználót authorizáló tokent ad vissza a mock.

Standalone futtatás esetén kétféleképpen konfigurálhatjuk a WireMockot - mindkét módszerről részletes ismertető található a WireMock dokumentációjában. Az egyik az általa kilógatott REST API használata, melyről nagyon fontos megjegyezni, hogy automatikusan nem tartja meg a konfigurációt perzisztensen, így ha újra kell indítanunk, akkor újra létre kell hozni a konfigurációt is - viszont az __admin/mappings/save útvonalra küldött kéréssel megkérhetjük a WireMockot, hogy mentse a memóriában található konfigurációt. A másik a mappingek perzisztens konfigurálása konfigurációs fájlok használatával. Ehhez a WireMock indulásakor létrehoz két mappát: mapping és __files - előbbibe kerülnek azok a JSON fájlok, melyek a request-response mappingeket definiálják, utóbbiba a bonyolultabb, nagyobb response dokumentumok mennek (például komplett, akár több száz soros JSON fájlok). A konfiguráláshoz hozzunk létre egy .json kiterjesztésű fájlt a mapping mappában, és a tartalma legyen a következő:

{
    "request": {
        "method": "GET",
        "url": "/article/1"
    },
    "response": {
        "status": 200,
        "body": "{\"content\": \"This is the content of the article\"}",
        "headers": {
            "Content-Type": "application/json"
        }
    }
}

A fenti konfiguráció létrehoz egy mappinget az /article/1 útvonalra, és amennyiben egy ilyen kérést csíp el a szerver, a visszatérés HTTP 200 lesz, a body-ban található JSON dokumentummal, illetve beállítja a válaszban a Content-Type header-t is az "application/json" mime type-ra. A body tartalmát természetesen ilyenkor escape-elnünk kell, kellőképpen kényelmetlenné téve a feladatot ahhoz, hogy ehelyett a jsonBody (a mapping konfiguráción belül lehet vele JSON objektumot készíteni) vagy a bodyFileName paramétert használjuk (a __files mappában levő fájlok hivatkozhatóak, relatív útvonallal). Részletek a már korábban linkelt dokumentációban találhatóak.

Egy kicsit komolyabb konfigurációs példa lehet a fentebb említett authentication mock. Vegyük az alábbi konfigurációt:

{
    "request": {
        "method": "POST",
        "url": "/claim-token",
        "bodyPatterns": [{
            "matchesJsonPath" : "$[?(@.username == 'user-1')]"
        }]
    },
    "response": {
        "status": 201,
        "body": "{\"token\": \"this-is-a-token-authorizing-user-1\"}",
        "headers": {
            "Content-Type": "application/json"
        }
    }
}

Ebben az esetben a request csak akkor fog matchelni, ha a request body tartalmazza a username fieldet, aminek az értéke user-1. Ebben az esetben viszont "sikeres lesz az authentikáció", és a mapping visszaad egy érvényes tokent user-1 számára. Fontos megjegyezni, hogy a request body matchelésére JSON Path kifejezések használhatóak, mint azt a fenti példa is mutatja.

Természetesen a fentiek mellett még számos további konfigurációs lehetőséget biztosít a WireMock, bár az említettek akár komolyabb mockok elkészítésére is alkalmasak. A dokumentáció szerencsére kifejezetten részletes és minden fontos információt tartalmaz.

Embedded használat

De mi történik akkor, ha a WireMock-ot például integrációs vagy acceptance tesztekben szeretnénk használni? Nos, a WireMock elérhető erre alkalmas formában is. Ez esetben Maven vagy Gradle függésként adhatjuk hozzá projektünkhöz (természetesen test scope-ra szűkítve), a szervert pedig a választott unit testing frameworknek megfelelő, rövid konfigurációval tudjuk elindítani, mely az alábbiak szerint történik:

JUnit 4.x framework alatt:

// Mindkét rule szükséges ahhoz, hogy a WireMock ne álljon le a tesztesetek között
// Ha viszont pont ezt szeretnénk elérni, akkor a ClassRule elhagyható
// Az alábbi konfigurációval egyébként a szerver a 9999-es porton kezd hallgatózni
@ClassRule
public static WireMockClassRule wireMockRule = new WireMockClassRule(options().port(9999));

@Rule
public WireMockClassRule wireMockInstanceRule = wireMockRule;

JUnit 5 vagy TestNG alatt:

// A tesztkészlet szintű inicializálóban (BeforeClass) helyezzük el az alábbi kódrészletet ...
WireMockServer wireMockServer = new WireMockServer(options().port(8089));
wireMockServer.start();

// ... illetve a tesztkészlet szintű takarítás (AfterClass) részeként az alábbit
wireMockServer.stop();

Utóbbi esetben persze azt is elérhetjük, hogy az egész teszt végrehajtás alatt ugyanaz a WireMock instance fusson, ehhez a teszt kontextus felállításáért felelős konfigurációban kell elhelyeznünk a fenti kódrészleteket. Az elvi működése innentől kezdve ugyanaz, mint standalone futtatás esetén, ám ilyenkor a tesztesetben lehetőségünk van a tesztelt alkalmazást elhagyó HTTP kérések verifikálására is - így ellenőrzive, hogy a megfelelő paraméterekkel történik-e a hívás, a megfelelő request body-t használja-e, és így tovább.

A kérések és a válaszok definiálása embedded használat esetén sokkal inkább on-the-fly jellegű, és aki már használt Mockito-t vagy nagyjából bármilyen mocking frameworköt, annak fölöttébb ismerősnek is fog hatni. Az alábbi egy egyszerű kérés-válasz definíció:

givenThat(get("/entries")
    .willReturn(ResponseDefinitionBuilder.okForJson(entryListDataModel)));

A fenti példa beállít a WireMock-ban egy mapping-et, ami az /entries útvonalra vár egy GET kérést, a válasz pedig egy JSON dokumentum lesz, melynek a tartalma az átadott, ez esetben entryListDataModel nevű objektum. A ResponseDefinitionBuilder.okForJson metódus egy shortcut, a válasz státusza így 200 lesz és szimplán JSON-ként formázza a response body-t. Ha kicsit több szabadságra van szükségünk a válasz összeállítását illetően, a ResponseDefinitionBuilder erre is lehetőséget biztosít:

ResponseDefinitionBuilder.responseDefinition()
    .withBody("{\"message\": \"this is a response\"}")
    .withHeader("Content-Type", "application/json")
    .withStatus(201);

A fenti esetben mi magunk állítjuk be a válasz státuszát, a visszatérő header-öket, és magát a body-t. Természetesen érdemes ilyenkor is inkább előre definiált, szerializált objektumokat használni body gyanánt, erre a célra például kiváló eszköz a Jackson ObjectMapper.

Ha a kérésnél használt request body-ra is szeretnénk megszorítást tenni, akkor már egy kicsit több dolgunk lesz, alább látható egy példa:

StringValuePattern requestBody = equalToJson("{\"username\": \"user-1\", \"password\":  \"1234\"}");
givenThat(post("/claim-token")
    .withRequestBody(requestBody)
    .willReturn(jsonResponse(claimTokenResponse, 201)));

Ezekben az esetekben (és majd a verifikálásnál is hasonlóan) lesz szükségünk a StringValuePattern objektumokra, mely például a fent látható equalToJson metódussal definiálható (megjegyzés: itt is van lehetőség JSON Path matchelésre a matchingJsonPath metódussal). Ebben az esetben a mapping csak akkor fog működni, ha a kérés pontosan a fenti paraméterekkel történik.

Ha kellőképpen szigorú matchelést állítunk be, verifikálásra már igazából nem is feltétlenül lesz szükség, hiszen ha a request nem a megfelelő paraméterezéssel hagyja el az alkalmazást, a WireMock kivételt dob (vagy beállítástól függően 404-es státusszal tér vissza). A lehetőség viszont megvan az utólagos, kézi verifikálásra, mégpedig az alábbi módon:

verify(postRequestedFor(urlEqualTo("/claim-token"))
    .withRequestBody(equalToJson("{\"username\": \"user-1\", \"password\":  \"1234\"}"))
    .withHeader("Authorization", equalTo("Bearer token")));

A fenti példa verifikálja, hogy POST kérés indult a /claim-token útvonalra, az adott request body-val és header-rel. Az equalToJson és az equalTo metódusok a korábbihoz hasonlóan itt is StringValuePattern objektumokat állítanak elő.

Nos, álljon most itt ennyi a WireMock-ról. Természetesen fontos megjegyezni, hogy a WireMock által biztosított funkcionalitás csak töredékét érintette cikkem, a terjedelmes dokumentáció sokkal több információt tartalmaz, így ha a WireMock használata mellett döntenétek, érdemes lemenni a dokumentáció mélységeibe, hogy az adott probléma megoldására milyen lehetőségeket biztosít.

Kommentek

Komment írásához jelentkezz be
Bejelentkezés

Még senki nem szólt hozzá ehhez a bejegyzéshez.