Tesztelési rend: Miért fontos szétválasztani az integrációs és unit teszteket?
Sziasztok! Ma egy olyan témába szeretnék mélyebben belevágni, ami sok csapatban okoz fejfájást, mégis alapvetően befolyásolja, hogy mennyire stabil és karbantartható kódot írunk: az integrációs és unit tesztek helyes elkülönítésének a művészetét. Ha már egyszer belebotlottál abba, hogy a teszteseteid órákig futnak, vagy frissítéskor minden törött, akkor tudod, miről beszélek. Neveljük ki magunkból a „teszt-salmit”, ahol minden össze van keverve!
Mi is az a különbség? A lényeg két mondatban
Képzeld el, hogy egy autót szerelsz össze. A unit teszt az, amikor leteszteled a gyertyát, a karburátort vagy a fékbetétet külön-külön egy speciális próbabán. Az integrációs teszt viszont az, amikor beülsz az összeszerelt kocsiiba, beindítod a motort és megpróbálsz elmenni egy kört. Mindkettő kurva fontos, de teljesen más célból.
* Unit teszt: Egyetlen osztályt, metódust vagy funkciót izolálunk és tesztelünk. A külső világot (adatbázis, fájlrendszer, API-k) kicseréljük mock objektumokra vagy stubokra. A kérdés: „Ez a kis alkatrész egyedül jól működik?” * Integrációs teszt: Két vagy több komponens együttműködését vizsgáljuk. Itt már valós, vagy valósnak tekinthető külső függőségekkel dolgozunk (pl. teszt adatbázis, teszt API végpont). A kérdés: „Ezek az alkatrészek együtt tudnak működni?”
Egy klasszikus példa: A UserService bűvészete
Nézzük meg egy tipikus PHP backend szolgáltatáson keresztül. Van egy UserService-ünk, ami felhasználókat regisztrál. Ehhez szüksége van egy UserRepository-ra (ami az adatbázissal beszél) és egy EmailSender-re (ami e-mailt küld).
<?php
class UserService {
private $userRepository;
private $emailSender;
public function __construct(UserRepository $userRepository, EmailSender $emailSender) {
$this->userRepository = $userRepository;
$this->emailSender = $emailSender;
}
public function registerUser(string $email, string $password): bool {
// 1. Létezik-e már a felhasználó?
if ($this->userRepository->findByEmail($email)) {
return false;
}
// 2. Mentés az adatbázisba
$user = new User($email, password_hash($password, PASSWORD_DEFAULT));
$saved = $this->userRepository->save($user);
// 3. Üdvözlő email küldése
if ($saved) {
$this->emailSender->sendWelcomeEmail($email);
}
return $saved;
}
}Hogyan írnánk rá UNIT tesztet?
A unit tesztben NEM akarunk valós adatbázis-írást és email-küldést. Csak azt akarjuk letesztelni, hogy a logikánk helyes-e. Tehát kicseréljük a függőségeket mockokra.
<?php
use PHPUnit\Framework\TestCase;
class UserServiceUnitTest extends TestCase {
public function testRegisterUserFailsIfUserExists(): void {
// 1. Mockoljuk a függőségeket
$mockRepository = $this->createMock(UserRepository::class);
$mockEmailSender = $this->createMock(EmailSender::class);
// 2. Beállítjuk a mock viselkedését: a findByEmail térjen vissza egy "létező" userrel
$mockRepository->method('findByEmail')->willReturn(new User('existing@test.com', 'hash'));
// 3. Azt VÁRJUK, hogy a save és sendWelcomeEmail METÓDUSOKAT SZEMÉTAN SE HÍVJUK MEG.
// Erre explicit expectation-öket is írhatnánk.
$service = new UserService($mockRepository, $mockEmailSender);
// 4. Teszteljük
$result = $service->registerUser('existing@test.com', 'password123');
$this->assertFalse($result); // Az elvárt eredmény: false, mert már létezik
}
public function testRegisterUserSavesAndSendsEmailOnSuccess(): void {
$mockRepository = $this->createMock(UserRepository::class);
$mockEmailSender = $this->createMock(EmailSender::class);
// A findByEmail null-t ad vissza (nincs ilyen user)
$mockRepository->method('findByEmail')->willReturn(null);
// A save metódus true-t adjon vissza (sikeres mentés)
$mockRepository->method('save')->willReturn(true);
// ELVÁRJUK, hogy a sendWelcomeEmail metódust pontosan egyszer, a megadott email címmel meghívják
$mockEmailSender->expects($this->once())
->method('sendWelcomeEmail')
->with('new@test.com');
$service = new UserService($mockRepository, $mockEmailSender);
$result = $service->registerUser('new@test.com', 'password123');
$this->assertTrue($result);
}
}Látod? A teszt gyors (nincs I/O), izolált (csak a UserService logikáját teszteli), és determinisztikus (a mockok viselkedését mi irányítjuk).
Hogyan írnánk rá INTEGRÁCIÓS tesztet?
Itt már a valódi együttműködésre vagyunk kíváncsiak. Például, hogy a UserRepository tényleg elmenti-e a felhasználót egy teszt adatbázisba (pl. SQLite memóriában).
<?php
use PHPUnit\Framework\TestCase;
class UserServiceIntegrationTest extends TestCase {
private $pdo;
private $repository;
private $service;
protected function setUp(): void {
// 1. Létrehozunk egy VALÓDI, de memóriában élő adatbázis kapcsolatot
$this->pdo = new PDO('sqlite::memory:');
$this->pdo->exec('CREATE TABLE users (email TEXT UNIQUE, password_hash TEXT)');
// 2. A VALÓDI repository-t használjuk, ami ezzel a PDO-val dolgozik
$this->repository = new UserRepository($this->pdo);
// 3. Az EmailSender-t viszont továbbra is mockolhatjuk, vagy használhatunk egy "fake" változatot,
// ami nem küld valódi emailt, csak naplózza. Most mockoljuk.
$mockEmailSender = $this->createMock(EmailSender::class);
$mockEmailSender->method('sendWelcomeEmail')->willReturn(true);
$this->service = new UserService($this->repository, $mockEmailSender);
}
public function testUserIsPersistedInRealDatabase(): void {
$testEmail = 'integrated@test.com';
// A service meghívása, ami a valós repository-n keresztül a valós adatbázisba ír
$result = $this->service->registerUser($testEmail, 'password123');
$this->assertTrue($result);
// ELLENŐRZÉS: Közvetlenül lekérdezzük az adatbázist, hogy tényleg benne van-e
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$testEmail]);
$persistedUser = $stmt->fetch();
$this->assertNotEmpty($persistedUser);
$this->assertEquals($testEmail, $persistedUser['email']);
// Itt akár a jelszó-hash ellenőrzését is végezhetnénk
}
}Ez a teszt lassabb, de értékesebbet mond: garantálja, hogy a komponenseink (Service + Repository + Adatbázis) tényleg össze tudnak dolgozni.
A frontend oldal: Egy kis jQuery és Bootstrap eszme
A frontenden is ugyanez a logika él. Egy gombkattintás kezelője unit tesztelhető anélkül, hogy a DOM-ot manipulálnánk. Az integrációs teszt viszont leteszteli, hogy a gomb tényleg megjelenik-e (Bootstrap osztályokkal) és a kattintás után a megfelelő jQuery callback fut-e, ami módosítja a felületet. Gyors példa egy SCSS stílussal:
// _buttons.scss - Egy komponens stílusa
.submit-btn {
@extend .btn, .btn-primary; // Bootstrap alapok
font-weight: bold;
border-radius: $custom-radius; // SCSS változó
&.loading {
opacity: 0.6;
// A tényleges "betöltés" státusz integrációs teszttel ellenőrizhető
}
}// submitHandler.js - Egy izolált funkció, ami könnyen unit tesztelhető
function validateAndSubmit(formData, apiClient) {
if (!formData.email) {
return { success: false, error: 'Email required' };
}
// apiClient itt egy mockolható függőség
return apiClient.post('/register', formData);
}
// main.js - Az "integráció", ami összeköti a DOM-eseményt a logikával
$(document).ready(function() {
$('#registerForm').on('submit', function(e) {
e.preventDefault();
let formData = $(this).serializeArray();
// A validáló/logikai funkció hívása (ezt unit tesztelnénk)
let result = validateAndSubmit(formData, realApiClient);
// A DOM működésre fókuszáló rész (ezt integrációs vagy E2E teszt ellenőrizné)
if (!result.success) {
$('#errorAlert').removeClass('d-none').text(result.error); // Bootstrap d-none osztály
} else {
$('.submit-btn').addClass('loading');
}
});
});Gyakori buktatók, amiket kerüljünk
1. „A unit tesztem csatlakozik az adatbázishoz”: Ha így van, az nem unit teszt. Lassú lesz, törékeny lesz (függ a DB állapotától), és nem az alkatrész logikáját teszteli.
2. Túl sok mockolás egy integrációs tesztben: Ha az integrációs tesztedben mindent mockolsz, az gyakorlatilag egy bonyolult unit tesztté fajul, ami nem teszteli az integrációt.
3. Ugyanabban a fájlban, suite-ben kevered a kettő: Válaszd szét őket! tests/Unit/ és tests/Integration/ mappákba szervezd. Futtathatod külön a gyors unit tesztjeidet (phpunit --testsuite Unit), míg az integrációsokat csak akkor, amikor átfogóbban akarsz tesztelni.
4. Az „integrációs teszt” átcsap teljes end-to-end (E2E) tesztté: Ne tedd! Az integrációs teszteknek is határozott határaik legyenek (pl. backend réteg). A teljes rendszert (frontend + backend + adatbázis + külső szolgáltatások) az E2E tesztek fedik le, azok még lassabbak és törékenyebbek.
Összegzés: A szétválasztás ereje
A két tesztfajta elkülönítése nem pedáns szabálykövetés, hanem hatékonysági és megbízhatósági kérdés. A gyors, izolált unit tesztek a fejlesztői önbizalom alapkövei – percek alatt futtathatod őkat akár commit előtt is. A lassabb, de átfogóbb integrációs tesztek a komponensek közötti szerződéseket védik – futtathatod őkat a CI pipeline-ban minden merge után.
Legközelebb, amikor tesztet írsz, kérdezd meg magadtól: *”Most éppen melyik réteget akarom biztosítani? Az egyes alkatrészek hibátlan működését, vagy azok együttműködését?”* A válasz adja meg, hogy milyen eszközt vegyél elő a tárházból. Így fogsz egy erős, gyorsan futó és megbízható tesztfallal rendelkezni, ami nem akadály, hanem a szárnyad.
Kitartás, és jó tesztelést!