Tesztek írása

A Nette Testerhez való tesztek írása abban egyedi, hogy minden teszt egy PHP szkript, amelyet önállóan lehet futtatni. Ez nagy potenciált rejt magában. Már amikor a tesztet írod, egyszerűen futtathatod, és megállapíthatod, hogy helyesen működik-e. Ha nem, könnyen lépésenként végigmehetsz rajta az IDE-ben, és keresheted a hibát.

A tesztet akár meg is nyithatod a böngészőben. De mindenekelőtt – azzal, hogy futtatod, végrehajtod a tesztet. Azonnal megtudod, hogy átment-e vagy meghiúsult.

A bevezető fejezetben mutattuk egy igazán triviális tesztet a tömbökkel való munkára. Most már létrehozunk egy saját osztályt, amelyet tesztelni fogunk, bár ez is egyszerű lesz.

Kezdjük egy tipikus könyvtárstruktúrával egy könyvtárhoz vagy projekthez. Fontos elkülöníteni a teszteket a kód többi részétől, például a deployment miatt, mert a teszteket nem akarjuk feltölteni az éles szerverre. A struktúra például ilyen lehet:

├── src/           # a kód, amelyet tesztelni fogunk
│   ├── Rectangle.php
│   └── ...
├── tests/         # tesztek
│   ├── bootstrap.php
│   ├── RectangleTest.php
│   └── ...
├── vendor/
└── composer.json

És most létrehozzuk az egyes fájlokat. Kezdjük a tesztelt osztállyal, amelyet az src/Rectangle.php fájlba helyezünk:

<?php
class Rectangle
{
	private float $width;
	private float $height;

	public function __construct(float $width, float $height)
	{
		if ($width < 0 || $height < 0) {
			throw new InvalidArgumentException('A méret nem lehet negatív.');
		}
		$this->width = $width;
		$this->height = $height;
	}

	public function getArea(): float
	{
		return $this->width * $this->height;
	}

	public function isSquare(): bool
	{
		return $this->width === $this->height;
	}
}

És létrehozunk hozzá egy tesztet. A tesztfájl nevének meg kell felelnie a *Test.php vagy *.phpt maszknak, válasszuk például a RectangleTest.php változatot:

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

// általános téglalap
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());  # ellenőrizzük a várt eredményeket
Assert::false($rect->isSquare());

Ahogy látod, az ún. assert metódusok, mint az Assert::same(), arra szolgálnak, hogy megerősítsék, hogy a tényleges érték megfelel a várt értéknek.

Már csak az utolsó lépés van hátra, ez a bootstrap.php fájl. Ez tartalmazza az összes teszthez közös kódot, például az osztályok autoloadingját, a környezet konfigurálását, ideiglenes könyvtár létrehozását, segédfüggvényeket és hasonlókat. Minden teszt betölti a bootstrapot, és tovább csak a teszteléssel foglalkozik. A bootstrap például így nézhet ki:

<?php
require __DIR__ . '/vendor/autoload.php';   # betölti a Composer autoloadert

Tester\Environment::setup();                # Nette Tester inicializálása

// és további konfiguráció (ez csak egy példa, esetünkben nincs rá szükség)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');

A megadott bootstrap feltételezi, hogy a Composer autoloader képes lesz betölteni a Rectangle.php osztályt is. Ezt például az autoload szakasz beállításával lehet elérni a composer.json-ban stb.

A tesztet most futtathatjuk a parancssorból, mint bármely más önálló PHP szkriptet. Az első futtatás felfedi az esetleges szintaktikai hibákat, és ha sehol nincs elírás, kiíródik:

$ php RectangleTest.php

OK

Ha a tesztben az állítást hamisra változtatnánk: Assert::same(123, $rect->getArea());, ez történne:

$ php RectangleTest.php

Failed: 200.0 should be 123

in RectangleTest.php(5) Assert::same(123, $rect->getArea());

FAILURE

Tesztek írásakor jó lefedni az összes szélsőséges helyzetet. Például, ha a bemenet nulla, negatív szám, más esetekben például üres string, null stb. Valójában ez arra kényszerít, hogy elgondolkodj, és eldöntsd, hogyan kell a kódnak viselkednie ilyen helyzetekben. A tesztek ezután rögzítik a viselkedést.

Esetünkben a negatív értéknek kivételt kell dobnia, amit az Assert::exception() segítségével ellenőrzünk:

// a szélesség nem lehet negatív
Assert::exception(
	fn() => new Rectangle(-1, 20),
	InvalidArgumentException::class,
	'A méret nem lehet negatív.',
);

És hasonló tesztet adunk hozzá a magassághoz. Végül teszteljük, hogy az isSquare() true-t ad-e vissza, ha mindkét méret azonos. Próbálja meg gyakorlásként megírni ezeket a teszteket.

Áttekinthetőbb tesztek

A tesztfájl mérete növekedhet, és gyorsan áttekinthetetlenné válhat. Ezért praktikus az egyes tesztelt területeket különálló függvényekbe csoportosítani.

Először egy egyszerűbb, de elegánsabb változatot mutatunk be, a globális test() függvény segítségével. A Tester nem hozza létre automatikusan, hogy ne legyen ütközés, ha a kódban azonos nevű függvény lenne. Csak a setupFunctions() metódus hozza létre, amelyet a bootstrap.php fájlban hívjon meg:

Tester\Environment::setup();
Tester\Environment::setupFunctions();

Ezzel a függvénnyel szépen feloszthatjuk a tesztfájlt elnevezett egységekre. Futtatáskor a leírások sorban kiíródnak.

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

test('általános téglalap', function () {
	$rect = new Rectangle(10, 20);
	Assert::same(200.0, $rect->getArea());
	Assert::false($rect->isSquare());
});

test('általános négyzet', function () {
	$rect = new Rectangle(5, 5);
	Assert::same(25.0, $rect->getArea());
	Assert::true($rect->isSquare());
});

test('a méretek nem lehetnek negatívak', function () {
	Assert::exception(
		fn() => new Rectangle(-1, 20),
        InvalidArgumentException::class,
	);

	Assert::exception(
		fn() => new Rectangle(10, -1),
        InvalidArgumentException::class,
	);
});

Ha minden teszt előtt vagy után kódot kell futtatnia, adja át azt a setUp() ill. tearDown() függvénynek:

setUp(function () {
	// inicializációs kód, amely minden test() előtt lefut
});

A második változat objektumorientált. Létrehozunk egy ún. TestCase-t, ami egy osztály, ahol az egyes egységeket metódusok képviselik, amelyek nevei test– kezdetűek.

class RectangleTest extends Tester\TestCase
{
	public function testGeneralOblong()
	{
		$rect = new Rectangle(10, 20);
		Assert::same(200.0, $rect->getArea());
		Assert::false($rect->isSquare());
	}

	public function testGeneralSquare()
	{
		$rect = new Rectangle(5, 5);
		Assert::same(25.0, $rect->getArea());
		Assert::true($rect->isSquare());
	}

	/** @throws InvalidArgumentException */
	public function testWidthMustNotBeNegative()
	{
		$rect = new Rectangle(-1, 20);
	}

	/** @throws InvalidArgumentException */
	public function testHeightMustNotBeNegative()
	{
		$rect = new Rectangle(10, -1);
	}
}

// Tesztmetódusok futtatása
(new RectangleTest)->run();

A kivételek tesztelésére ezúttal a @throws annotációt használtuk. Többet a TestCase fejezetben tudhat meg.

Segédfüggvények

A Nette Tester több osztályt és függvényt tartalmaz, amelyek megkönnyíthetik például a HTML dokumentum tartalmának tesztelését, a fájlokkal dolgozó függvények tesztelését és így tovább.

Leírásukat a Segédosztályok oldalon találja.

Annotációk és tesztek kihagyása

A tesztek futtatását befolyásolhatják a fájl elején lévő phpDoc kommentár formájában megadott annotációk. Például így nézhetnek ki:

/**
 * @phpExtension pdo, pdo_pgsql
 * @phpVersion >= 7.2
 */

A megadott annotációk azt mondják, hogy a tesztet csak PHP 7.2 vagy újabb verzióval kell futtatni, és ha a pdo és pdo_pgsql PHP kiterjesztések jelen vannak. Ezeket az annotációkat a parancssori tesztfuttató veszi figyelembe, amely abban az esetben, ha a feltételek nem teljesülnek, kihagyja a tesztet, és a kimenetben s – skipped betűvel jelöli.

Azonban a teszt manuális futtatásakor nincs hatásuk.

A tesztet saját feltétel teljesülése alapján is ki lehet hagyni az Environment::skip() segítségével. Például ez kihagyja a teszteket Windows rendszeren:

if (defined('PHP_WINDOWS_VERSION_BUILD')) {
	Tester\Environment::skip('Requires UNIX.');
}

Könyvtárstruktúra

Javasoljuk, hogy már kicsit nagyobb könyvtáraknál vagy projekteknél ossza fel a teszteket tartalmazó könyvtárat még alkönyvtárakra a tesztelt osztály névtere szerint:

└── tests/
	├── NamespaceOne/
	│   ├── MyClass.getUsers.phpt
	│   ├── MyClass.setUsers.phpt
	│   └── ...
	│
	├── NamespaceTwo/
	│   ├── MyClass.creating.phpt
	│   ├── MyClass.dropping.phpt
	│   └── ...
	│
	├── bootstrap.php
	└── ...

Így futtathatja a teszteket egyetlen névtérből, azaz alkönyvtárból:

tester tests/NamespaceOne

Speciális helyzetek

Az a teszt, amely egyetlen assert metódust sem hív meg, gyanús, és hibásnak minősül:

Error: This test forgets to execute an assertion.

Ha valóban azt szeretné, hogy az assert hívások nélküli teszt érvényesnek minősüljön, hívja meg például az Assert::true(true)-t.

Szintén félrevezető lehet az exit() és die() használata a teszt hibaüzenettel történő leállítására. Például az exit('Hiba a kapcsolatban') a tesztet 0 visszatérési értékkel fejezi be, ami sikert jelez. Használja az Assert::fail('Hiba a kapcsolatban')-t.