Írásbeli tesztek

A tesztek írása a Nette Tester számára egyedülálló abban a tekintetben, hogy minden teszt egy PHP szkript, amely önállóan futtatható… Ebben nagy lehetőségek rejlenek. Ahogy megírja a tesztet, egyszerűen lefuttathatja, hogy lássa, megfelelően működik-e. Ha nem, akkor az IDE-ben könnyen végigmehet rajta, és megkeresheti a hibát.

Akár meg is nyithatja a tesztet egy böngészőben. De mindenekelőtt – a futtatással elvégzi a tesztet. Azonnal megtudod, hogy átment-e vagy nem ment át.

A bevezető fejezetben mutattunk egy igazán triviális tesztet a PHP tömb használatával. Most létrehozzuk a saját osztályunkat, amelyet tesztelni fogunk, bár ez is egyszerű lesz.

Kezdjük egy tipikus könyvtár vagy projekt könyvtár elrendezésével. Fontos, hogy a teszteket elkülönítsük a kód többi részétől, például a telepítés miatt, mert nem akarjuk a teszteket feltölteni a szerverre. A struktúra lehet a következő:

├── src/           # code that we will test
│   ├── Rectangle.php
│   └── ...
├── tests/         # tests
│   ├── bootstrap.php
│   ├── RectangleTest.php
│   └── ...
├── vendor/
└── composer.json

Most pedig létrehozzuk az egyes fájlokat. A tesztelt osztállyal kezdünk, amelyet a következő fájlban helyezünk el src/Rectangle.php

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

	public function __construct(float $width, float $height)
	{
		if ($width < 0 || $height < 0) {
			throw new InvalidArgumentException('The dimension must not be negative.');
		}
		$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 egyeznie a *Test.php vagy a *.phpt maszkkal, mi a RectangleTest.php változatot választjuk:

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

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

Mint látható, az olyan állítási módszerek, mint a Assert::same(), arra szolgálnak, hogy egy tényleges érték egyezzen egy elvárt értékkel.

Az utolsó lépés a bootstrap.php fájl létrehozása. Ez tartalmazza az összes teszt közös kódját. Például az osztályok automatikus betöltése, a környezet konfigurálása, ideiglenes könyvtár létrehozása, segédprogramok és hasonlók. Minden teszt betölti a bootstrapet és csak a tesztelésre figyel. A bootstrap így nézhet ki:

<?php
require __DIR__ . '/vendor/autoload.php'; # Composer autoloader betöltése

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

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

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

Most már futtathatjuk a tesztet a parancssorból, mint bármely más önálló PHP-szkriptet. Az első futtatás során kiderül, hogy vannak-e szintaktikai hibák, és ha nem elírás történt, akkor látni fogjuk:

$ php RectangleTest.php

OK

Ha a tesztben megváltoztatjuk az utasítást false Assert::same(123, $rect->getArea());, ez fog történni:

$ php RectangleTest.php

Failed: 200.0 should be 123

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

FAILURE

A tesztek írásakor jó, ha minden szélsőséges helyzetet elkapunk. Például, ha a bemenet nulla, negatív szám, más esetekben üres karakterlánc, null stb. Valójában arra kényszerít, hogy gondolkodjunk és eldöntsük, hogyan viselkedjen a kód ilyen helyzetekben. A tesztek aztán rögzítik a viselkedést.

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

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

És hozzáadunk egy hasonló tesztet a magasságra. Végül teszteljük, hogy a isSquare() a true értéket adja vissza, ha mindkét dimenzió megegyezik. Próbáljunk meg gyakorlatként ilyen teszteket írni.

Jól elrendezett tesztek

A tesztfájl mérete megnőhet, és gyorsan zsúfolttá válhat. Ezért praktikus az egyes tesztelt területeket külön függvényekbe csoportosítani.

Először egy egyszerűbb, de elegáns változatot mutatunk be, a test() globális függvényt használva. A tesztelő nem hozza létre automatikusan, hogy elkerüljük az ütközést, ha a kódunkban volt egy azonos nevű függvény. Csak a setupFunctions() metódus hozza létre, amelyet a bootstrap.php fájlban hívunk meg:

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

Ezzel a függvénnyel szépen fel tudjuk osztani a tesztfájlt névre szóló egységekre. Végrehajtáskor a címkék egymás után jelennek meg.

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

test('general oblong', function () {
	$rect = new Rectangle(10, 20);
	Assert::same(200.0, $rect->getArea());
	Assert::false($rect->isSquare());
});

test('general square', function () {
	$rect = new Rectangle(5, 5);
	Assert::same(25.0, $rect->getArea());
	Assert::true($rect->isSquare());
});

test('dimensions must not be negative', function () {
	Assert::exception(
		fn() => new Rectangle(-1, 20),
        InvalidArgumentException::class,
	);

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

Ha az egyes tesztek előtt vagy után kell futtatni a kódot, adja át a setUp() vagy a tearDown() címre:

setUp(function () {
	// inicializáló kód, amely minden egyes teszt() előtt lefut.
});

A második változat az objektum. Létrehozzuk az úgynevezett TestCase-t, amely egy olyan osztály, amelyben az egyes egységeket olyan metódusok reprezentálják, amelyek neve test- kezdetű.

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);
	}
}

// Tesztmódszerek futtatása
(new RectangleTest)->run();

Ezúttal a @throw megjegyzést használtuk a kivételek tesztelésére. További információért lásd a TestCase fejezetet.

Segédfunkciók

A Nette Tester számos olyan osztályt és függvényt tartalmaz, amelyek megkönnyíthetik a tesztelést, például a HTML-dokumentum tartalmának teszteléséhez, a fájlokkal való munka funkcióinak teszteléséhez és így tovább.

Ezek leírását a Súgók oldalon találja.

Megjegyzések és tesztek kihagyása

A tesztek végrehajtását befolyásolhatják a fájl elején található phpDoc megjegyzésben szereplő megjegyzések. Ez például így nézhet ki:

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

A megjegyzések szerint a teszt csak a 7.2-es vagy magasabb PHP-verzióval és a pdo és pdo_pgsql PHP-kiterjesztések megléte esetén futtatható. Ezeket az annotációkat a parancssori tesztfutó vezérli, amely, ha a feltételek nem teljesülnek, kihagyja a tesztet, és s betűvel jelöli – kihagyva. A teszt kézi futtatásakor azonban nincs hatásuk.

A megjegyzések leírását lásd a Teszt megjegyzések című fejezetben.

A teszt saját feltétel alapján is kihagyható a Environment::skip() segítségével. Például Windows esetén kihagyjuk ezt a tesztet:

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

Könyvtárszerkezet

Csak kicsit nagyobb könyvtárak vagy projektek esetén javasoljuk, hogy a tesztkönyvtárat a tesztelt osztály névterének megfelelően osszuk alkönyvtárakra:

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

A teszteket egyetlen névtérből, azaz alkönyvtárból tudja majd futtatni:

tester tests/NamespaceOne

Edge Cases

Egy olyan teszt, amely nem hív meg egyetlen állítás metódust sem, gyanús, és hibásan kerül kiértékelésre:

Error: This test forgets to execute an assertion.

Ha az állítások meghívása nélküli tesztet valóban érvényesnek akarjuk tekinteni, hívjuk meg például a Assert::true(true).

Árulkodó lehet a exit() és a die() használata is, ha a tesztet hibaüzenettel zárjuk le. A exit('Error in connection') például 0 kilépési kóddal fejezi be a tesztet, ami sikert jelez. Használja a Assert::fail('Error in connection') címet.