Pisanje testov

Pisanje testov za Nette Tester je edinstveno v tem, da je vsak test PHP skript, ki ga je mogoče samostojno zagnati. To skriva velik potencial. Že ko test pišete, ga lahko preprosto zaženete in ugotovite, ali deluje pravilno. Če ne, ga lahko enostavno korakate v IDE in iščete napako.

Test lahko celo odprete v brskalniku. Ampak predvsem – s tem, ko ga zaženete, test izvedete. Takoj ugotovite, ali je uspel ali ne.

V uvodnem poglavju smo si pokazali res trivialen test dela s poljem. Zdaj pa si bomo ustvarili lasten razred, ki ga bomo testirali, čeprav bo tudi preprost.

Začnimo od tipične imenikske strukture za knjižnico ali projekt. Pomembno je ločiti teste od preostanka kode, na primer zaradi uvajanja, ker testov na produkcijski strežnik ne želimo nalagati. Struktura je lahko na primer takšna:

├── src/           # koda, ki jo bomo testirali
│   ├── Rectangle.php
│   └── ...
├── tests/         # testi
│   ├── bootstrap.php
│   ├── RectangleTest.php
│   └── ...
├── vendor/
└── composer.json

In zdaj ustvarimo posamezne datoteke. Začnimo od testiranega razreda, ki ga postavimo v datoteko 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;
	}
}

In ustvarimo zanj test. Ime datoteke s testom bi moralo ustrezati maski *Test.php ali *.phpt, izberimo na primer varianto RectangleTest.php:

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

// splošni pravokotnik
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());  # preverimo pričakovane rezultate
Assert::false($rect->isSquare());

Kot vidite, se t.i. asercijske metode kot Assert::same() uporabljajo za potrditev, da dejanska vrednost ustreza pričakovani vrednosti.

Ostaja še zadnji korak in to je datoteka bootstrap.php. Ta vsebuje kodo, skupno za vse teste, na primer samodejno nalaganje razredov, konfiguracijo okolja, ustvarjanje začasnega imenika, pomožne funkcije in podobno. Vsi testi bootstrap naložijo in se nato posvetijo samo testiranju. Bootstrap lahko izgleda na naslednji način:

<?php
require __DIR__ . '/vendor/autoload.php';   # naloži Composer autoloader

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

// in druga konfiguracija (gre samo za primer, v našem primeru niso potrebne)
date_default_timezone_set('Europe/Ljubljana');
define('TmpDir', '/tmp/app-tests');

Navedeni bootstrap predpostavlja, da bo Composer autoloader sposoben naložiti tudi razred Rectangle.php. To je mogoče doseči na primer z nastavitvijo sekcije autoload v composer.json ipd.

Test lahko zdaj zaženemo iz ukazne vrstice kot katerikoli drug samostojen PHP skript. Prvi zagon nam bo razkril morebitne sintaktične napake in če nikjer ni tipkarske napake, se bo izpisalo:

$ php RectangleTest.php

OK

Če bi v testu spremenili trditev na neresnično Assert::same(123, $rect->getArea());, se zgodi tole:

$ php RectangleTest.php

Failed: 200.0 should be 123

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

FAILURE

Pri pisanju testov je dobro zajeti vse skrajne situacije. Na primer, ko bo vhod nič, negativno število, v drugih primerih morda prazen niz, null itd. Pravzaprav vas to sili, da se zamislite in odločite, kako naj se koda v takšnih situacijah obnaša. Testi nato obnašanje fiksirajo.

V našem primeru mora negativna vrednost sprožiti izjemo, kar preverimo s pomočjo Assert::exception():

// širina ne sme biti negativna
Assert::exception(
	fn() => new Rectangle(-1, 20),
	InvalidArgumentException::class,
	'The dimension must not be negative.',
);

In podoben test dodamo za višino. Na koncu testiramo, da isSquare() vrne true, če sta obe dimenziji enaki. Poskusite si kot vajo napisati takšne teste.

Preglednejši testi

Velikost datoteke s testom lahko narašča in hitro postane nepregledna. Zato je praktično posamezna testirana področja združiti v ločene funkcije.

Najprej si bomo pokazali enostavnejšo, vendar elegantno varianto, in sicer s pomočjo globalne funkcije test(). Tester je ne ustvarja samodejno, da ne bi prišlo do kolizije, če bi imeli v kodi funkcijo z istim imenom. Ustvari jo šele metoda setupFunctions(), ki jo pokličite v datoteki bootstrap.php:

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

S pomočjo te funkcije lahko testno datoteko lepo razčlenimo na poimenovane celote. Ob zagonu se bodo postopoma izpisovali opisi.

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

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

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

test('dimenzije ne smejo biti negativne', function () {
	Assert::exception(
		fn() => new Rectangle(-1, 20),
        InvalidArgumentException::class,
	);

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

Če potrebujete pred ali po vsakem testu zagnati kodo, jo predajte funkciji setUp() oz. tearDown():

setUp(function () {
	// inicializacijska koda, ki se zažene pred vsakim test()
});

Druga varianta je objektna. Ustvarimo si t.i. TestCase, kar je razred, kjer posamezne celote predstavljajo metode, katerih imena se začnejo 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);
	}
}

// Zagon testnih metod
(new RectangleTest)->run();

Za testiranje izjem smo tokrat uporabili anotacijo @throw. Več se boste naučili v poglavju TestCase.

Pomožne funkcije

Nette Tester vsebuje več razredov in funkcij, ki vam lahko olajšajo na primer testiranje vsebine HTML dokumenta, testiranje funkcij, ki delajo z datotekami in tako naprej.

Njihov opis najdete na strani Pomožni razredi.

Anotacije in preskakovanje testov

Zagon testov je lahko vplivan z anotacijami v obliki phpDoc komentarja na začetku datoteke. Lahko izgleda na primer takole:

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

Navedene anotacije pravijo, da naj se test zažene samo z PHP različico 7.2 ali višjo in če so prisotne PHP razširitve pdo in pdo_pgsql. S temi anotacijami se ravna zaganjalnik testov iz ukazne vrstice, ki v primeru, da pogoji niso izpolnjeni, test preskoči in v izpisu označi s črko s – skipped. Vendar pri ročnem zagonu testa nimajo nobenega vpliva.

Opis anotacij najdete na strani Anotacije testov.

Test lahko pustimo preskočiti tudi na podlagi izpolnitve lastnega pogoja s pomočjo Environment::skip(). Na primer, ta preskoči teste na Windows:

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

Imeniška struktura

Priporočamo, da pri le malo večjih knjižnicah ali projektih imenik s testi razdelite še v podimenike glede na imenski prostor testiranega razreda:

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

Tako boste lahko zaganjali teste iz enega samega imenskega prostora oz. podimenika:

tester tests/NamespaceOne

Posebne situacije

Test, ki ne pokliče niti ene asercijske metode, je sumljiv in se oceni kot napačen:

Error: This test forgets to execute an assertion.

Če res mora biti test brez klica asercij veljaven, pokličite na primer Assert::true(true).

Prav tako je lahko zavajajoče uporabljati exit() in die() za končanje testa s sporočilom o napaki. Na primer exit('Error in connection') konča test z izhodno kodo 0, kar signalizira uspeh. Uporabite Assert::fail('Error in connection').