11.2 Unit tesztek alapjai – Arrange-Act-Assert
Áttekintő
Az előző alfejezetben megértettük, hogy miért érdemes teszteket írni. Most nézzük meg, hogy hogyan néz ki egy jól megírt unit teszt – és miért van olyan sok rosszul megírt.
A unit teszt a tesztelés legkisebb egysége: egyetlen függvényt, metódust vagy osztályt tesztel, a többi résztől izoláltan. Az Arrange-Act-Assert (AAA) pattern egy egyszerű, de erőteljes sablon, amit szinte minden professzionális tesztkönyvtár hallgatólagosan elvár – függetlenül attól, hogy Python, JavaScript vagy PHP kódot írsz.
Ha megtanulsz jó unit teszteket írni, azonnal látszik a code review-n. Ez az a tudás, ami első munkahelyen komolyan megkülönböztet a többiektől.
Részletes leírás
Mi a unit teszt?
A unit teszt egy automatikusan futtatható kódrészlet, ami:
- Egyetlen viselkedést ellenőriz
- Emberi beavatkozás nélkül fut
- Egyértelmű eredményt ad (sikeres / sikertelen)
Fontos különbség: A unit teszt nem a teljes alkalmazást teszteli – csak egyetlen, jól körülhatárolt egységet. Ezért hívják unit (egység) tesztnek.
A jó unit teszt három jellemzője
1. Gyors (Fast)
Egy unit teszt milliszekundumok alatt fut. Ha másodpercekig tart, valami baj van – valószínűleg adatbázishoz vagy hálózathoz nyúl, ami egy unit teszt esetén tilos.
2. Izolált (Isolated)
A teszt nem függ más tesztektől, futtatási sorrendtől, adatbázis állapottól, vagy külső service-től. Ha az egyiket kiveszed és önállóan futtatod, ugyanazt az eredményt kell adnia.
3. Determinisztikus (Deterministic)
Ugyanaz a bemenet → mindig ugyanaz a kimenet. Ha a teszt egyszer sikeres, egyszer sikertelen ugyanarra a kódra, az nem a kód problémája – a teszt a probléma.
Az Arrange-Act-Assert (AAA) pattern
Az AAA pattern három részre osztja a tesztet:
Arrange – Előkészítés: mi kell a teszthez?
Act – Végrehajtás: hívd meg a tesztelt dolgot
Assert – Ellenőrzés: az eredmény az volt, amit vártál?
Ez nem csak szépítés – a három rész szétválasztása kényszert ad: ha az Arrange rész hatalmas, az jelzés, hogy a kód valószínűleg túl sok mindentől függ.
Példák – AAA a gyakorlatban
Python (pytest)
Tesztelünk egy egyszerű árengedmény-számító függvényt:
# src/pricing.py
def apply_discount(price: float, discount_percent: float) -> float:
if discount_percent < 0 or discount_percent > 100:
raise ValueError("Discount must be between 0 and 100")
return price * (1 - discount_percent / 100)
# tests/test_pricing.py
from src.pricing import apply_discount
import pytest
def test_apply_discount_reduces_price_by_given_percent():
# Arrange
original_price = 100.0
discount = 20.0
# Act
result = apply_discount(original_price, discount)
# Assert
assert result == 80.0
def test_apply_discount_raises_error_for_negative_discount():
# Arrange
price = 100.0
invalid_discount = -5.0
# Act & Assert
with pytest.raises(ValueError):
apply_discount(price, invalid_discount)
def test_apply_zero_discount_returns_original_price():
# Arrange
price = 50.0
# Act
result = apply_discount(price, 0)
# Assert
assert result == 50.0
JavaScript (Jest)
// src/pricing.js
function applyDiscount(price, discountPercent) {
if (discountPercent < 0 || discountPercent > 100) {
throw new Error("Discount must be between 0 and 100");
}
return price * (1 - discountPercent / 100);
}
module.exports = { applyDiscount };
// tests/pricing.test.js
const { applyDiscount } = require('../src/pricing');
describe('applyDiscount', () => {
test('reduces price by given percentage', () => {
// Arrange
const price = 100;
const discount = 20;
// Act
const result = applyDiscount(price, discount);
// Assert
expect(result).toBe(80);
});
test('throws error for discount above 100', () => {
// Arrange
const price = 100;
const invalidDiscount = 110;
// Act & Assert
expect(() => applyDiscount(price, invalidDiscount)).toThrow();
});
});
PHP (PHPUnit)
<?php
// src/Pricing.php
class Pricing {
public function applyDiscount(float $price, float $discountPercent): float {
if ($discountPercent < 0 || $discountPercent > 100) {
throw new \InvalidArgumentException("Discount must be between 0 and 100");
}
return $price * (1 - $discountPercent / 100);
}
}
<?php
// tests/PricingTest.php
use PHPUnit\Framework\TestCase;
class PricingTest extends TestCase {
public function test_apply_discount_reduces_price(): void {
// Arrange
$pricing = new Pricing();
$price = 100.0;
$discount = 20.0;
// Act
$result = $pricing->applyDiscount($price, $discount);
// Assert
$this->assertEquals(80.0, $result);
}
public function test_apply_discount_throws_for_invalid_percent(): void {
// Arrange
$pricing = new Pricing();
// Act & Assert
$this->expectException(\InvalidArgumentException::class);
$pricing->applyDiscount(100.0, -10.0);
}
}
Teszt elnevezési konvenciók
A jó teszt neve dokumentál. Ha a teszt megbukik, a nevéből értsd meg, mi a probléma – anélkül, hogy megnyitnád a kódot.
Rossz nevek:
test_discount()
test1()
testApplyDiscount_error()
Jó nevek:
test_apply_discount_reduces_price_by_given_percent()
test_apply_discount_raises_error_for_negative_discount()
apply_discount_with_zero_returns_original_price()
Bevált sablon: [mit csinál a kód]_[milyen feltétel mellett]_[mi az elvárt eredmény]
Például: apply_discount_when_discount_is_100_percent_returns_zero
Tesztelési szemcsézettség (Granularity)
Mennyi viselkedést tesztelj egy tesztben? Egyet.
Ha egy tesztben több assert is van különböző dolgokra, az többnyire azt jelzi, hogy valójában több tesztet kellene írnod.
❌ Nem ideális – egy teszt, két különböző viselkedés:
def test_discount_function():
assert apply_discount(100, 20) == 80 # helyes eset
assert apply_discount(100, 0) == 100 # edge case
# Ha az egyik megbukik, nem tudod melyik viselkedés a hibás
✅ Jobb – külön teszt, külön fókusz:
def test_apply_discount_with_20_percent():
assert apply_discount(100, 20) == 80
def test_apply_discount_with_zero_percent_returns_original():
assert apply_discount(100, 0) == 100
Kivétel: Ha természetes csoportban tartoznak össze (pl. egy objektum attribútumait ellenőrzöd), néhány assert egy tesztben elfogadható.
Mikor érdemes unit tesztet írni?
Mindenképpen érdemes, ha:
- Komplex logikát írsz (elágazások, számítások, transzformációk)
- Edge case-eket kezelsz (null, üres lista, negatív szám)
- Regressiont akarsz megelőzni ("ezt már egyszer elrontottuk")
Valószínűleg nem unit tesztre van szükséged, ha:
- A kód csak adatbázist hív és visszaad (integrációs teszt kell)
- A kód csak UI elemeket renderel (e2e teszt kell)
- Triviális getter/setter, aminek nincs logikája
Életszerű példák
"A senior kért tőlem tesztet – és én nem tudtam, miből álljanak"
Képzeld el: beadsz egy PR-t, amiben egy kedvezmény-kalkulátor logika van. A senior reviewer azt írja: „tesztek?". Te nem tudod pontosan, mit vegyen fel a teszt.
Amit sokan csinálnak: Tesztelik azt az egy esetet, amit ők maguk is lefuttattak kézzel. A teszt átmegy, de edge case-eket nem fed le.
Amit AAA-val csinálsz:
- Mi az elvárt viselkedés normál esetben? → Arrange + Act + Assert
- Mi történik ha negatív az input? → külön teszt
- Mi történik ha 0? → külön teszt
- Mi történik ha 100%? → külön teszt
Ezzel a gondolkodásmóddal a kódod valóban tesztelt lesz, nem csak „van mellette teszt".
"A CI megbukott, de lokálisan minden oké"
Ez majdnem mindig azt jelenti, hogy a teszt nem igazán izolált. Pl. valamelyik teszt adatbázis-állapotot hagy maga után, amit egy másik teszt felvesz.
Ha az AAA pattern szerint dolgozol és az Arrange részben minden szükséges adatot létrehozol (nem feltételezed, hogy „már benne van"), ez a probléma nem fordulhat elő.
"A tesztem megbukott, de a kód jó"
Ha a teszt megbukik, de te biztos vagy benne, hogy a kód helyes, kérdezd meg magadtól:
- Az Assert helyes feltételt ellenőröz?
- A tesztben lévő várt érték valóban az elvárt eredmény?
- Létezik-e valami sorrend-függőség?
Ilyenkor a test-first (TDD) gondolkodásmód segít: ha előbb írod a tesztet, és az megbukik a helyes kódon – az azt jelenti, hogy a teszt a hibás, nem a kód.
Tesztfeladatok
1. feladat – AAA azonosítása (fogalmi)
Nézd meg az alábbi tesztet. Melyik sor az Arrange, melyik az Act, és melyik az Assert?
def test_email_is_valid():
email = "user@example.com"
result = is_valid_email(email)
assert result == True
Kérdés: Azonosítsd a három részt. Van-e valami, ami hiányozhat ebből a tesztből, ha üzleti logika szempontjából nézzük?
2. feladat – Teszt neve alapján a viselkedés
Az alábbi tesztnevek közül melyik a legjobb és miért?
a) test_user()
b) test_create_user_sets_name()
c) test_that_when_we_create_a_user_the_name_field_is_correctly_set_to_the_given_value()
d) test_create_user_with_valid_name_stores_name()
3. feladat – Hibás teszt javítása
Az alábbi tesztben mi a probléma?
test('user functions work', () => {
const user = createUser('Anna', 25);
expect(user.name).toBe('Anna');
expect(user.age).toBe(25);
expect(user.id).toBeDefined();
expect(isAdult(user)).toBe(true);
expect(formatUserName(user)).toBe('Anna (25)');
});
Írd át úgy, hogy minden teszt egyetlen dolgot ellenőrizzen! (Nem kell tényleges kódot írni, csak vázold fel a teszteket.)
4. feladat – Edge case-ek azonosítása
Van egy függvény, ami egy szöveget fogad, és visszaadja nagybetűsen az első szót:
input: "hello world" → output: "HELLO"
input: " hello" → output: "HELLO" (vezető szóközzel)
Milyen edge case-eket tesztelnél? Sorolj fel legalább 4-et, és minden esethez írd le: mi az Arrange, mi az Act, és mi az Assert.
5. feladat – Determinizmus
Az alábbi tesztek közül melyik lesz valószínűleg nem-determinisztikus és miért?
# A teszt
def test_get_current_year():
result = get_current_year()
assert result == 2024
# B teszt
def test_random_id_is_positive():
result = generate_random_id()
assert result > 0
# C teszt
def test_double_value():
result = double(5)
assert result == 10
6. feladat – Saját kódod tesztelése
Vegyél egy egyszerű függvényt a saját projektedből (vagy írj egyet most), ami:
- Legalább egy paramétert kap
- Visszaad valamit
- Van legalább egy edge case-je (pl. 0, üres string, negatív szám)
Írd meg hozzá az AAA struktúrájú teszteket:
- Normal case teszt
- Edge case teszt
- Hibás input teszt (ha van)
Bármilyen language-t használhatsz.
7. feladat – Teszt értékelése
Az alábbi pytest teszt helyes-e? Mi a probléma vele, ha van?
import random
def test_password_generator():
# Arrange
length = 10
# Act
password1 = generate_password(length)
password2 = generate_password(length)
# Assert
assert len(password1) == 10
assert password1 != password2 # minden hívás más jelszót ad
8. feladat – Fogalmi kérdések (kvíz)
Jelöld meg a helyes választ:
8a. Melyik NEM jellemzője a jó unit tesztnek?
- a) Gyors
- b) Izolált
- c) Adatbázishoz csatlakozik
- d) Determinisztikus
8b. Az AAA pattern „Assert" részében mit csinálsz?
- a) Létrehozod a szükséges objektumokat
- b) Meghívod a tesztelt függvényt
- c) Ellenőrzöd, hogy az eredmény megfelel-e az elvárásnak
- d) Letisztítod a tesztkörnyezetet
8c. Mi jelzi, hogy egy teszt valószínűleg nem jól van megírva?
- a) Egy darab assert van benne
- b) A neve leírja, mit tesztel
- c) Az Arrange rész 50 soros és sok objektumot hoz létre
- d) Minden értéket lokálisan definiál
9. feladat – Valós szituáció
Te írtál egy regisztrációs folyamatot. A backend oldalon van egy validate_registration függvény, ami:
- Elfogadja az email és jelszó mezőket
- Visszaadja, hogy érvényes-e a regisztráció (True/False)
- Érvénytelen, ha: email formátum helytelen, jelszó rövidebb mint 8 karakter, jelszóban nincs szám
Sorold fel, milyen teszteket írna egy tapasztalt fejlesztő ehhez a függvényhez! Legyen legalább 6 különböző eset – minden esethez add meg a teszt nevét (AAA sablon szerint).
10. feladat – Gondolkodási feladat
Képzeld el, hogy a csapatodon valaki azt mondja: „Minek külön teszt fájl, egyszerűbb ha a fájl végén van egy if __name__ == '__main__' blokk, ahol kézzel kiíratjuk az eredményt."
Milyen érveket hoznál fel az automatizált unit tesztek mellett? Sorolj fel legalább 3 konkrét, valós előnyt.