A külső szolgáltatások mockolása PHP unit tesztekben: Ne fuss neki a falnak!
Üdv ismét a kódfronton! Ma egy olyan témát szeretnék megragadni, ami sok fejlesztőt hidegrázással tölt el, pedig lenyűgözően hatékony eszköz lehet: külső szolgáltatások mockolása PHPUnit tesztekben. Gondolok itt fizetési átjárókra, e-mail szolgáltatókra, API-kra, vagy akár a saját adatbázisodra. Ezek nélkül a tesztek vagy lassúak, vagy instabilak, vagy egyszerűen nem futtathatók izoláltan. A mocking segítségével viszont teljes kontrollt szerezhetsz ezek felett, és olyan teszteket írhatsz, amelyek valóban a *te* logikádat vizsgálják.
Miért is baj az, ha a teszt külsőktől függ?
Képzeld el a klasszikus helyzetet: írsz egy PaymentProcessor osztályt, ami a Stripe API-t hívja. Ha a teszted valóban küldene kéréseket, akkor:
– Lassú lenne (hálózati késés)
– Instabil lenne (mi van, ha Stripe épp nem elérhető?)
– Pénzbe kerülne (valódi tranzakciók)
– Nem izolált (a Stripe hibája miatt bukná a teszted, holott a te kódod rendben van)
A mocking lényege, hogy elképzelted ezeket a külső függőségeket. Létrehozol egy „ál”-Stripe klienst, ami előre meghatározott válaszokat ad, mintha valódi lenne. Így a teszt csak a te logikádat győri meg.
Bevezetés a PHPUnit mocking keretrendszerébe
A PHPUnit beépített mocking könyvtárral rendelkezik, amit a getMockBuilder() vagy a modernebb createMock() metódusokon keresztül érsz el. A kulcs fogalom a „test double” (teszt dublőr), aminek több fajtája van: mock, stub, spy. Most a mockkal foglalkozunk, ami nemcsak válaszokat ad, hanem elvárásokat is fogalmaz meg (pl. „egy adott metódust pontosan egyszer kell meghívni”).
<?php
use PHPUnit\Framework\TestCase;
class PaymentProcessorTest extends TestCase
{
public function testSuccessfulPayment()
{
// Elkészítjük a mock külső szolgáltatást
$stripeMock = $this->createMock(StripeClient::class);
// Megmondjuk, hogy a 'charge' metódust EGYSZER kell meghívni,
// és milyen paraméterekkel, mit adjon vissza
$stripeMock->expects($this->once())
->method('charge')
->with(1000, 'EUR', 'token_xyz')
->willReturn(['id' => 'ch_123', 'status' => 'succeeded']);
// Injektáljuk a mock-ot a tesztelt osztályba
$processor = new PaymentProcessor($stripeMock);
$result = $processor->process(1000, 'token_xyz');
$this->assertTrue($result->isSuccessful());
}
}Látod? A StripeClient egyetlen valódi példánya sem jött létre. Az egész egy színház: mi írjuk a szereplők szövegét. A process metódusunk azt hiszi, hogy valódi Stripe-val kommunikál, de valójában a mi scriptünket játssza.
Gyakori buktatók, amikbe én is belefutottam
1. Túl bonyolult mockok: Ha egy mockod 20 sort foglal el, valószínűleg a tesztett kód túl összetetten függ a külső szolgáltatástól. Érdemes átgondolni az architektúrát.
2. A valóságot túlságosan eltávolító mock: A mock visszaadhat egy tökéletes választ, de a valódi API 500-as hibát dob. Teszteld a hibakezelést is!
// Teszteljük a hibaesetet is!
$stripeMock->method('charge')
->willThrowException(new ApiConnectionError('Network down'));
$this->expectException(PaymentFailedException::class);
$processor->process(1000, 'token_xyz');3. Statikus metódusok mockolása: Régi PHP kódokban gyakori, de nehezebb vele bánni. Érdemes dependency injection felé térni, vagy speciális eszközöket (pl. Mockery) használni.
És mi van a frontend oldallal?
A mocking nem csak backend koncepció! Képzeld el, hogy jQueryvel készítesz egy űrlapot, ami AJAX kérést küld a backendnek. A tesztek során nyilván nem akarod, hogy valódi kérés menjen ki.
// Egy egyszerű mock a jQuery AJAX helyett
$.ajax = function(request) {
// Előre definiált válasz a teszt kontextusában
if (request.url === '/api/pay') {
return $.Deferred().resolve({ success: true }).promise();
}
};
// Majd a teszt meghívja a fizetés indító függvényt
initiatePayment(1000);
// És ellenőrzi, hogy a UI megfelelően reagál-e a sikeres válaszra
assert($('.alert-success').is(':visible'));Természetesen komolyabb teszt keretrendszerek (mint a Jest) sokkal kifinomultabb mocking lehetőségeket kínálnak, de a koncepció ugyanaz: szigeteld el a tesztelt kódot a külső világ változásaitól és kiszámíthatatlanságától.
Összegzés: A kontroll érzete
A mocking lényege nem az, hogy minél több kódot írj, hanem hogy kontrollt szerezz a tesztkörnyezeted felett. Amikor egy mockot készítesz, gyakorlatilag megfogalmazod: „én pontosan tudom, hogyan kell viselkednie ennek a külső komponensnek ebben a forgatókönyvben”. Ez az, ami átveszi a hatalmat a flaky tesztek (amik néha lefutnak, néha nem) felett.
Kezdetben furcsának tűnhet, hogy „kitalálod” a külső világ viselkedését. De pont ez a lényeg: a unit teszt ne a Stripe megbízhatóságát mérje, hanem hogy a te kódod helyesen dolgozza-e fel a Stripe válaszát – akár az sikeres, akár hibás. Így ha holnap a Stripe API változik, a teszted még mindig eldönti, hogy a te alkalmazásod logikája helyes-e.
Szóval legközelebb, amikor egy külső API-t integrálsz, ne gondolj a tesztelhetőségre utólag! Injektáld függőségként, és máris készen állsz a mockok használatára. Egy stabil, gyorsan futó tesztcsomag sokkal több érték, mint azt elsőre gondolnád – főleg, amikor éjjel 11-kor deploy-olsz, és biztosra szeretnél menni, hogy nem törtél el semmit.
A mocking nem csalás: az a tudatos döntés, hogy a külső világot a te szabályaid szerint teszteled.