PHPUnit mock külső szolgáltatások tesztelése

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.