Pisanje testov

Pisanje testov za Nette Tester je edinstveno v tem, da je vsak test skripta PHP, ki se lahko zažene samostojno. To ima velik potencial. Ko napišete test, ga lahko preprosto zaženete in preverite, ali deluje pravilno. Če ne, ga lahko preprosto pregledate v IDE in poiščete napako.

Test lahko odprete tudi v brskalniku. Predvsem pa – z njegovim zagonom boste izvedli test. Takoj boste izvedeli, ali je bil uspešno opravljen ali ne.

V uvodnem poglavju smo prikazali zares trivialen preizkus uporabe polja PHP. Zdaj bomo ustvarili svoj razred, ki ga bomo preizkusili, čeprav bo prav tako preprost.

Začnimo s tipično postavitvijo imenika za knjižnico ali projekt. Pomembno je, da teste ločimo od preostale kode, na primer zaradi nameščanja, saj testov ne želimo naložiti v strežnik. Struktura je lahko naslednja:

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

Zdaj bomo ustvarili posamezne datoteke. Začeli bomo s testiranim razredom, ki ga bomo namestili 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 zanj bomo ustvarili test. Ime testne datoteke se mora ujemati z masko *Test.php ali *.phpt, mi bomo izbrali varianto RectangleTest.php:

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

// splošna podolgovata oblika
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());  # we will verify the expected results
Assert::false($rect->isSquare());

Kot lahko vidite, se metode trditev, kot je Assert::same(), uporabljajo za trditev, da se dejanska vrednost ujema s pričakovano vrednostjo.

Zadnji korak je ustvarjanje datoteke bootstrap.php. Ta vsebuje skupno kodo za vse teste. Na primer razredi samodejnega zagona, konfiguracija okolja, ustvarjanje začasnega imenika, pomočniki in podobno. Vsak test naloži zagonsko datoteko in se posveti samo testiranju. Zagonska datoteka je lahko videti takole:

<?php
require __DIR__ . '/vendor/autoload.php';  # load Composer autoloader

Tester\Environment::setup();               # initialization of Nette Tester

// in druge konfiguracije (samo primer, v našem primeru niso potrebne)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');

Ta bootstrap predpostavlja, da bo Composer autoloader lahko naložil tudi razred Rectangle.php. To lahko na primer dosežemo z nastavitvijo razdelka autoload v composer.json, itd.

Zdaj lahko test zaženemo iz ukazne vrstice kot katero koli drugo samostojno skripto PHP. Prvi zagon bo razkril morebitne sintaktične napake, in če niste naredili tiskarske napake, boste videli:

$ php RectangleTest.php

OK

Če v testu spremenimo izjavo v false Assert::same(123, $rect->getArea());, se bo zgodilo naslednje:

$ php RectangleTest.php

Failed: 200.0 should be 123

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

FAILURE

Pri pisanju testov je dobro ujeti vse skrajne situacije. Na primer, če je vhodni podatek nič, negativno število, v drugih primerih pa prazen niz, ničla itd. Pravzaprav vas to prisili v razmišljanje in odločanje, kako naj se koda obnaša v takšnih situacijah. S testi se nato obnašanje popravi.

V našem primeru bi morala negativna vrednost vreči izjemo, kar preverimo z Assert::exception():

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

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

Dobro urejeni testi

Velikost testne datoteke se lahko poveča in hitro postane nepregledna. Zato je praktično, da posamezna testna področja združite v ločene funkcije.

Najprej bomo prikazali preprostejšo, a elegantno različico, ki uporablja globalno funkcijo test(). Tester je ne ustvari samodejno, da bi se izognil trku, če bi imeli v kodi funkcijo z istim imenom. Ustvari jo šele metoda setupFunctions(), ki jo pokličete v datoteki bootstrap.php:

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

S to funkcijo lahko testno datoteko lepo razdelimo na poimenovane enote. Ob izvajanju se bodo oznake prikazale ena za drugo.

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

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

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

Druga različica je objekt. Ustvarili bomo tako imenovani TestCase, ki je razred, v katerem so posamezne enote predstavljene z metodami, katerih imena se začnejo s 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);
	}
}

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

Tokrat smo uporabili pripis @throw za testiranje izjem. Za več informacij glejte poglavje TestCase.

Pomožne funkcije

Nette Tester vključuje več razredov in funkcij, ki vam lahko olajšajo testiranje, na primer pomočnike za testiranje vsebine dokumenta HTML, za testiranje funkcij za delo z datotekami itd.

Njihov opis najdete na strani Pomočniki.

Pripisovanje in preskakovanje testov

Na izvajanje testov lahko vplivajo opombe v komentarju phpDoc na začetku datoteke. Na primer, lahko je videti takole:

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

V opombah je navedeno, da se test lahko izvede le z različico PHP 7.2 ali višjo in če sta prisotni razširitvi PHP pdo in pdo_pgsql. Te opombe nadzoruje testni pogon v ukazni vrstici, ki, če pogoji niso izpolnjeni, preskoči test in ga označi s črko s – preskočen. Vendar pa nimajo nobenega učinka, če test zaženete ročno.

Za opis opomb glejte Test Annotations.

Test lahko preskočite tudi na podlagi lastnega pogoja s Environment::skip(). Ta test bomo na primer preskočili pri operacijskem sistemu Windows:

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

Struktura imenikov

Pri le nekoliko večjih knjižnicah ali projektih priporočamo razdelitev imenika za testiranje 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
	└── ...

Teste boste lahko izvajali iz enega samega imenskega prostora, tj. podimenikov:

tester tests/NamespaceOne

Edge Cases

Test, ki ne kliče nobene metode za potrjevanje, je sumljiv in bo ocenjen kot napačen:

Error: This test forgets to execute an assertion.

Če naj se test brez klicanja trditev res šteje za veljavnega, pokličite na primer Assert::true(true).

Prav tako je lahko zahrbtna uporaba exit() in die(), s katerima se test konča s sporočilom o napaki. Na primer, exit('Error in connection') konča test z izhodno kodo 0, ki sporoča uspeh. Uporabite Assert::fail('Error in connection').