Unit és integrációs tesztek: Hol a határ?
Sziasztok! Ma egy olyan témát szeretnék megragadni, ami elsőre triviálisnak tűnhet, de a gyakorlatban rengeteg fejfájást okoz: hogyan válasszuk szét tisztán az unit és az integrációs teszteket? Hányszor fordult elő, hogy egy teszt, amit unit tesztnek szántunk, lassú lett, instabil, és valójában egy külső rendszert is megkérdez? Vagy éppenséggel egy bonyolult integrációs tesztet írtunk meg olyan módon, mintha az egy egyszerű unit teszt lenne? Nézzük meg, hol húzódik a vékony, de kritikus vonal.
Mi is az a „pure” unit teszt?
Az unit teszt célja egy elkülönített, apró egység (egy függvény, egy metódus, egy osztály) tesztelése, teljesen elszigetelten a külső világtól. Itt a kulcsszó a elszigeteltség. A tesztelt egység minden függőségét (dependency) le kell cserélni mockolt vagy stubolt változatokra.
Egy tipikus PHP példa
Képzeljük el egy egyszerű OrderCalculator osztályt, ami kiszámolja egy rendelés végső árát, beleértve az áfát. A lényeg: nem kérdez le adatbázist, nem hív külső API-t.
taxRate = $taxRate;
}
public function calculateTotal(array $items): float {
$subtotal = 0;
foreach ($items as $item) {
$subtotal += $item['price'] * $item['quantity'];
}
return $subtotal * (1 + $this->taxRate);
}
}
// A hozzá tartozó PEST vagy PHPUnit teszt lehetne:
// (Itt most demonstrálom a logikát)
class OrderCalculatorTest {
public function testCalculateTotal() {
// 1. ARRANGE - Előkészítés
$calculator = new OrderCalculator(0.27); // 27% ÁFA
$mockItems = [
['price' => 1000, 'quantity' => 2],
['price' => 500, 'quantity' => 1]
];
// 2. ACT - Művelet végrehajtása
$result = $calculator->calculateTotal($mockItems);
// 3. ASSERT - Eredmény ellenőrzése
// Elvárt: (1000*2 + 500*1) * 1.27 = 2500 * 1.27 = 3175
assert($result === 3175.0);
}
}Itt nincs adatbázis, nincs fájlrendszer, nincs hálózati hívás. A konstruktorban átadott adókulcs is egy egyszerű primitív érték. Ez egy tiszta unit teszt.
Az integrációs teszt: Amikor a dolgok együttműködését nézzük
Az integrációs teszt célja, hogy két vagy több egység (modul, szolgáltatás, komponens) együttműködését ellenőrizze. Ide tartozik pl. az, hogy az osztályunk valódi adatbázissal kommunikál, vagy egy külső API választ dolgoz fel. A határ itt kezd elmosódni.
Gyakori buktató: A „másfajta” unit teszt
Itt jön a gyakorlati buktató. Írtunk egy szép metódust, ami adatbázisból kér le usereket. Aztán írunk rá egy „unit tesztet”, ami elindít egy teljes Docker konténert MySQL-lel, feltölti adatokkal, és lefuttatja a tesztet. Ez nem unit teszt! Ez egy (valószínűleg lassú és kényes) integrációs teszt lett.
dbConnection = $dbConnection;
}
public function findActiveUsers(): array {
$stmt = $this->dbConnection->prepare("SELECT * FROM users WHERE active = 1");
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
// Unit teszt NEM így néz ki:
class UserRepositoryIncorrectTest {
public function testFindActiveUsers() {
// !! Figyelem: Ez integrációs teszt, mert valódi adatbázist használ !!
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$repository = new UserRepository($pdo);
$users = $repository->findActiveUsers();
// ... assert-ek
}
}
// Unit teszt HELYESEN, mockolt függőséggel:
class UserRepositoryCorrectTest {
public function testFindActiveUsers() {
// 1. Mockoljuk a PDO (és a Statement) objektumot
$mockStatement = $this->createMock(PDOStatement::class);
$mockStatement->method('execute')->willReturn(true);
$mockStatement->method('fetchAll')->willReturn([['id' => 1, 'name' => 'Teszt User']]);
$mockPdo = $this->createMock(PDO::class);
$mockPdo->method('prepare')->willReturn($mockStatement);
// 2. Teszteljük a repository logikát
$repository = new UserRepository($mockPdo);
$result = $repository->findActiveUsers();
// 3. Assert: a metódus visszaadta a mockolt adatot?
assert(count($result) === 1);
assert($result[0]['name'] === 'Teszt User');
}
}A különbség nyilvánvaló: a második esetben nem az adatbázis működését teszteljük, hanem azt, hogy a repository osztályunk helyesen hívja meg a PDO metódusait és dolgozza fel a visszaért adatokat. Az adatbázis elérés maga egy külön integrációs teszt témaköre.
Frontend világban: jQuery és Bootstrap komponensek
A probléma nem csak backend jelenség. Képzelj el egy frontend komponenst, ami jQuery-vel egy Bootstrap modal tartalmát frissíti, adatok betöltése után.
// Egy egyszerű modul, ami felhasználói adatot tölt be és jelenít meg
var UserProfileWidget = (function() {
function loadUserProfile(userId, $targetElement) {
$.ajax({
url: '/api/user/' + userId,
method: 'GET',
success: function(data) {
var profileHtml = '<div class="card"><div class="card-body">' +
'<h5 class="card-title">' + data.name + '</h5>' +
'<p class="card-text">' + data.email + '</p>' +
'</div></div>';
$targetElement.html(profileHtml).addClass('border-success');
},
error: function() {
$targetElement.html('<div class="alert alert-danger">Hiba!</div>');
}
});
}
return {
loadProfile: loadUserProfile
};
})();
// --- TESZTEK ---
// Unit teszt (pl. Jasmine vagy Jest környezetben) a HTML generálásra:
// Mockoljuk a $.ajax hívást, hogy ne fusson valódi hálózati kérés
describe('UserProfileWidget unit teszt', function() {
it('megjeleníti a felhasználói adatokat sikeres válasz esetén', function() {
// Mock jQuery ajax
spyOn($, 'ajax').and.callFake(function(options) {
options.success({ name: 'John Doe', email: 'john@example.com' });
});
var $testDiv = $('<div>');
UserProfileWidget.loadProfile(123, $testDiv);
expect($testDiv.html()).toContain('John Doe');
expect($testDiv.html()).toContain('john@example.com');
expect($testDiv.hasClass('border-success')).toBe(true);
});
});
// Integrációs teszt (pl. Cypress vagy Selenium) lehetne:
// "1. látogassunk el a /profile/123 oldalra"
// "2. kattintsunk a 'Betöltés' gombon"
// "3. ellenőrizzük, hogy megjelenik-e a .card elem a böngészőben"
// EZ már valódi hálózati kérést indít és a valódi DOM-mal dolgozik.A CSS/SCSS stílusok (pl. a .border-success osztály, ami Bootstrap-ből származik) megjelenését általában nem unit teszteljük kód szinten. Az a vizuális regressziós tesztek (pl. Percy, Applitools) vagy manuális ellenőrzés területe. Unit szinten csak annyit tudunk ellenőrizni, hogy a megfelelő CSS osztályt kapta-e az elem.
Miért fontos a tiszta szétválasztás?
1. Sebesség: Unit teszteknek milliszekundumok alatt le kell futniuk. Egy százas unit teszt szüted másodperc alatt. Ha integrációs teszteket csempészünk közéjük, a futási idő az egekbe szökik. 2. Stabilitás: Egy unit tesztnek determinisztikusnak kell lennie. Ha függ egy adatbázis állapotától, egy külső API elérhetőségétől, akkor random bukhat („flaky test”). Ez alattomos és sok időt emészt fel. 3. Karbantarthatóság: A mockolt függőségekkel könnyebb izolálni a hibát. Ha egy „unit teszt” elbukik, tudjuk, hogy a valódi logikában van a hiba, nem egy külső rendszer konfigurációs problémája. 4. Futási környezet: Unit teszteket bárhol le lehet futtatni, akár internetkapcsolat nélkül is. Integrációs tesztekhez gyakran kell adatbázis, message broker, stb.
Gyakori érvátlások és megoldások
* „De a mock összetett!” – Igen, lehet. De ha egy osztály annyira bonyolult, hogy mockolása is nehéz, az egy tervezési jelzés lehet („High Coupling”). Érdemes átgondolni a felelősség elválasztást (Single Responsibility Principle).
* „De ki kell próbálni, hogy működik az adatbázissal!” – Teljesen igaz! Ezért kell írni külön, jól elnevezett integrációs teszteket, amik pl. egy tests/integration/ mappában vannak, és csak akkor futnak le, amikor explicit megkérjük a CI pipeline-unkat rá (pl. php artisan test --group=integration).
* „A frontendben minden DOM-mal integrálódik, az mi?” – Itt a komponens-tesztek (pl. React Testing Library) ügyesen állnak a kettő között: lehetőleg a valódi DOM API-kat használják, de a hálózati hívásokat mockolják. A lényeg, hogy a üzleti logikád unit tesztelni tudd mockokkal.
Összegzés
Ne hagyjuk, hogy a tesztek kavargókádjává váljanak. A kulcs a szándékosságban és a tisztán fogalmazott célban van: * Unit teszt: „Ez a függvény a megadott bemenetre a várt kimenetet adja-e, ha minden külső tényezőt kontrollálunk?” * Integrációs teszt: „Ez a két rendszerrész (pl. a PHP backend és a MySQL) együtt, valós körülmények között helyesen kommunikál-e?”
Ha ezt a különbséget szem előtt tartjuk, tesztkészletünk gyorsabb, stabilabb és sokkal hasznosabb lesz a hosszú távú fejlesztés során. Legközelebb, amikor tesztet írsz, kérdezd meg magadtól: *”Most épp melyiket csinálom?”* A válasz sokat segít a struktúrában.
Hasznos volt? Írd meg kommentben, neked hogy vált be a két teszttípus elkülönítése a projekteiden!
A weboldalon megjelenő szöveges és vizuális tartalmak előállításához mesterséges intelligenciát (AI) használunk.