11.2 Unit teszt alapok

← 11. TDD – Automatizált tesztek

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:

  1. Mi az elvárt viselkedés normál esetben? → Arrange + Act + Assert
  2. Mi történik ha negatív az input? → külön teszt
  3. Mi történik ha 0? → külön teszt
  4. 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:

  1. Az Assert helyes feltételt ellenőröz?
  2. A tesztben lévő várt érték valóban az elvárt eredmény?
  3. 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:

  1. Normal case teszt
  2. Edge case teszt
  3. 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.

Scroll to Top