Psaní testů

Psaní testů pro Nette Tester je unikátní v tom, že každý test je PHP skript, který lze samostatně spustit. To ukrývá velký potenciál. Už když test píšete, můžete jej jednoduše spouštět a zjišťovat, jestli funguje správně. Pokud ne, lze jej snadno krokovat v IDE a hledat chybu.

Test můžete dokonce otevřít v prohlížeči. Ale především – tím, že jej spustíte, tak test vykonáte. Okamžitě zjistíte, jestli prošel, nebo selhal.

V úvodní kapitole jsme si ukázali opravdu triviální test práce s polem. Teď už si vytvoříme vlastní třídu, kterou budeme testovat, byť bude také jednoduchá.

Začneme od typické adresářové struktury pro knihovnu nebo projekt. Důležité je oddělit testy od zbytku kódu, například kvůli deploymentu, protože testy na ostrý server nahrávat nechceme. Struktura může být třeba taková:

├── src/           # kód, který budeme testovat
│   ├── Rectangle.php
│   └── ...
├── tests/         # testy
│   ├── bootstrap.php
│   ├── RectangleTest.php
│   └── ...
├── vendor/
└── composer.json

A nyní vytvoříme jednotlivé soubory. Začneme od testované třídy, kterou umístíme do souboru 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;
	}
}

A vytvoříme pro ni test. Název souboru s testem by měl odpovídat masce *Test.php nebo *.phpt, zvolíme třeba variantu RectangleTest.php:

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

// obecný oblong
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());  # ověříme očekávané výsledky
Assert::false($rect->isSquare());

Jak vidíte, tzv. aserční metody jako Assert::same() se používají k potvrzení, že skutečná hodnota odpovídá očekávané hodnotě.

Zbývá poslední krok a tím je soubor bootstrap.php. Ten obsahuje kód společný pro všechny testy, například autoloading tříd, konfiguraci prostředí, vytvoření dočasného adresáře, pomocné funkce a podobně. Všechny testy bootstrap načítají a dále se věnují pouze testování. Bootstrap může vypadat následovně:

<?php
require __DIR__ . '/vendor/autoload.php';   # načte Composer autoloader

Tester\Environment::setup();                # inicializace Nette Tester

// a další konfigurace (jde jen o příklad, v našem případě nejsou potřeba)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');

Uvedený bootstrap předpokládá, že autoloader Composeru bude schopný načíst i třídu Rectangle.php. Toho lze docílit například nastavením sekce autoload v composer.json apod.

Test můžeme nyní spustit z příkazové řádky jako jakýkoliv jiný samostatný PHP skript. První spuštění nám odhalí případné syntaktické chyby a pokud nikde není překlep, vypíše se:

$ php RectangleTest.php

OK

Pokud bychom změnili v testu tvrzení na nepravdivé Assert::same(123, $rect->getArea()); stane se tohle:

$ php RectangleTest.php

Failed: 200.0 should be 123

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

FAILURE

Při psaní testů je dobré podchytit všechny krajní situace. Například když bude vstupem nula, záporné číslo, v jiných případech třeba prázdný řetězec, null atd. Vlastně vás to nutí zamyslet se a rozhodnout, jak se v takových situacích má kód chovat. Testy potom chování zafixují.

V našem případě má záporná hodnota vyhodit výjimku, což ověříme pomocí Assert::exception():

// šířka nesmí být záporná
Assert::exception(
	fn() => new Rectangle(-1, 20),
	InvalidArgumentException::class,
	'The dimension must not be negative.',
);

A obdobný test přidáme pro výšku. Nakonec otestujeme, že isSquare() vrátí true, pokud jsou oba rozměry stejné. Zkuste si jako cvičení takové testy napsat.

Přehlednější testy

Velikost souboru s testem může narůstat a rychle se stane nepřehledným. Proto je praktické jednotlivé testované oblasti seskupit do samostatných funkcí.

Nejprve si ukážeme jednodušší, avšak elegantní variantu, a to pomocí globální funkce test(). Tester ji nevytváří automaticky, aby nedošlo ke kolizi, kdybyste měli v kódu funkci se stejným jménem. Vytvoří ji až metoda setupFunctions(), kterou zavolejte v souboru bootstrap.php:

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

Pomocí této funkce můžeme testovací soubor hezky rozčlenit na pojmenované celky. Při spuštění se budou postupně vypisovat popisky.

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

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

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

test('rozměry nesmí být záporné', function () {
	Assert::exception(
		fn() => new Rectangle(-1, 20),
        InvalidArgumentException::class,
	);

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

Pokud potřebujete před nebo po každém testu spustit kód, předejte ho funkci setUp() resp. tearDown():

setUp(function () {
	// inicializační kód, který se spustí před každým test()
});

Druhá varianta je objektová. Vytvoříme si tzv. TestCase, což je třída, kde jednotlivé celky představují metody, jejichž názvy začínají na test–.

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

// Spuštění testovacích metod
(new RectangleTest)->run();

Pro testování výjimek jsme tentokrát použili anotaci @throw. Více se dozvíte v kapitole TestCase.

Pomocné funkce

Nette Tester obsahuje několik tříd a funkcí, které vám mohou usnadnit například testování obsahu HTML dokumentu, testování funkcí pracujících se soubory a tak dále.

Jejich popis najdete na stránce Pomocné třídy.

Anotace a přeskakování testů

Spouštění testů může být ovlivněno anotacemi v podobě phpDoc komentáře na začátku souboru. Může vypadat například takto:

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

Uvedené anotace říkají, že test má být spuštěn pouze s PHP verze 7.2 nebo vyšší a pokud jsou přítomna PHP rozšíření pdo a pdo_pgsql. Těmito anotacemi se řídí spouštěč testů z příkazové řádky, který v případě, že podmínky nejsou splněné, test přeskočí a ve výpisu označí písmenem s – skipped. Avšak při ručním spuštění testu nemají žádný vliv.

Popis anotací najdete na stránce Anotace testů.

Test lze nechat přeskočit taky na základě splnění vlastní podmínky pomocí Environment::skip(). Například tato přeskočíme testy na Windows:

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

Adresářová struktura

Doporučujeme u jen trošku větších knihoven nebo projektů rozdělit adresář s testy ještě do podadresářů podle jmenného prostoru testované třídy:

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

Budete tak moci spouštět testy z jediného jmenného prostoru čili podadresáře:

tester tests/NamespaceOne

Speciální situace

Test, který nezavolá ani jednu aserční metodu, je podezřelý a vyhodnotí se jako chybný:

Error: This test forgets to execute an assertion.

Pokud opravdu má být test bez volání asercí považován platný, zavolejte třeba Assert::true(true).

Také může být zrádné používat exit() a die() k ukončení testu s chybovou zprávou. Například exit('Error in connection') ukončí test s návratovým kódem 0, což signalizuje úspěch. Použijte Assert::fail('Error in connection').