Pisanie testów
Pisanie testów dla Nette Tester jest unikalne w tym, że każdy test jest skryptem PHP, który można uruchomić samodzielnie. To kryje w sobie duży potencjał. Już gdy piszesz test, możesz go po prostu uruchamiać i sprawdzać, czy działa poprawnie. Jeśli nie, można go łatwo krokować w IDE i szukać błędu.
Test możesz nawet otworzyć w przeglądarce. Ale przede wszystkim – tym, że go uruchomisz, wykonasz test. Natychmiast dowiesz się, czy przeszedł, czy zawiódł.
W rozdziale wprowadzającym pokazaliśmy naprawdę trywialny test pracy z tablicą. Teraz już stworzymy własną klasę, którą będziemy testować, choć będzie również prosta.
Zaczniemy od typowej struktury katalogów dla biblioteki lub projektu. Ważne jest oddzielenie testów od reszty kodu, na przykład ze względu na deployment, ponieważ testów na serwer produkcyjny nie chcemy wgrywać. Struktura może być na przykład taka:
├── src/ # kod, który będziemy testować
│ ├── Rectangle.php
│ └── ...
├── tests/ # testy
│ ├── bootstrap.php
│ ├── RectangleTest.php
│ └── ...
├── vendor/
└── composer.json
A teraz stworzymy poszczególne pliki. Zaczniemy od testowanej klasy, którą umieścimy w
pliku 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;
}
}
I stworzymy dla niej test. Nazwa pliku z testem powinna odpowiadać masce *Test.php
lub *.phpt
,
wybierzemy na przykład wariant RectangleTest.php
:
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
// ogólny prostokąt
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea()); # zweryfikujemy oczekiwane wyniki
Assert::false($rect->isSquare());
Jak widzisz, tzw. metody asercji jak Assert::same()
służą
do potwierdzenia, że rzeczywista wartość odpowiada oczekiwanej wartości.
Pozostaje ostatni krok, a tym jest plik bootstrap.php
. Zawiera on kod wspólny dla wszystkich testów, na
przykład autoloading klas, konfigurację środowiska, tworzenie tymczasowego katalogu, funkcje pomocnicze i podobne. Wszystkie
testy wczytują bootstrap i dalej zajmują się tylko testowaniem. Bootstrap może wyglądać następująco:
<?php
require __DIR__ . '/vendor/autoload.php'; # wczytuje autoloader Composera
Tester\Environment::setup(); # inicjalizacja Nette Tester
// i dalsza konfiguracja (to tylko przykład, w naszym przypadku nie są potrzebne)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');
Podany bootstrap zakłada, że autoloader Composera będzie w stanie wczytać również klasę
Rectangle.php
. Można to osiągnąć na przykład ustawieniem sekcji autoload w
composer.json
itp.
Test możemy teraz uruchomić z wiersza poleceń jak każdy inny samodzielny skrypt PHP. Pierwsze uruchomienie ujawni ewentualne błędy składniowe, a jeśli nigdzie nie ma literówki, wypisze się:
$ php RectangleTest.php
OK
Jeśli zmienilibyśmy w teście twierdzenie na nieprawdziwe Assert::same(123, $rect->getArea());
stanie
się to:
$ php RectangleTest.php Failed: 200.0 should be 123 in RectangleTest.php(5) Assert::same(123, $rect->getArea()); FAILURE
Przy pisaniu testów dobrze jest uchwycić wszystkie skrajne sytuacje. Na przykład gdy wejściem będzie zero, liczba ujemna, w innych przypadkach na przykład pusty ciąg znaków, null itp. Właściwie zmusza to do zastanowienia się i zdecydowania, jak w takich sytuacjach ma zachowywać się kod. Testy następnie utrwalają zachowanie.
W naszym przypadku wartość ujemna ma rzucić wyjątek, co zweryfikujemy za pomocą Assert::exception():
// szerokość nie może być ujemna
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
'The dimension must not be negative.',
);
I podobny test dodamy dla wysokości. Na koniec przetestujemy, że isSquare()
zwróci true
, jeśli
oba wymiary są takie same. Spróbuj jako ćwiczenie napisać takie testy.
Bardziej przejrzyste testy
Rozmiar pliku z testem może rosnąć i szybko stać się nieprzejrzystym. Dlatego praktyczne jest grupowanie poszczególnych testowanych obszarów w oddzielne funkcje.
Najpierw pokażemy prostszą, ale elegancką wariantę, a mianowicie za pomocą globalnej funkcji test()
. Tester
nie tworzy jej automatycznie, aby nie doszło do kolizji, gdybyś miał w kodzie funkcję o tej samej nazwie. Utworzy ją dopiero
metoda setupFunctions()
, którą wywołaj w pliku bootstrap.php
:
Tester\Environment::setup();
Tester\Environment::setupFunctions();
Za pomocą tej funkcji możemy plik testowy ładnie podzielić na nazwane całości. Przy uruchomieniu będą kolejno wypisywane opisy.
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
test('ogólny prostokąt', function () {
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());
Assert::false($rect->isSquare());
});
test('ogólny kwadrat', function () {
$rect = new Rectangle(5, 5);
Assert::same(25.0, $rect->getArea());
Assert::true($rect->isSquare());
});
test('wymiary nie mogą być ujemne', function () {
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
);
Assert::exception(
fn() => new Rectangle(10, -1),
InvalidArgumentException::class,
);
});
Jeśli potrzebujesz przed lub po każdym teście uruchomić kod, przekaż go funkcji setUp()
resp.
tearDown()
:
setUp(function () {
// kod inicjalizacyjny, który uruchomi się przed każdym test()
});
Druga warianta jest obiektowa. Stworzymy sobie tzw. TestCase, czyli klasę, gdzie poszczególne całości reprezentują metody, których nazwy zaczynają się 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);
}
}
// Uruchomienie metod testowych
(new RectangleTest)->run();
Do testowania wyjątków tym razem użyliśmy adnotacji @throw
. Więcej dowiesz się w rozdziale TestCase.
Funkcje pomocnicze
Nette Tester zawiera kilka klas i funkcji, które mogą ułatwić na przykład testowanie zawartości dokumentu HTML, testowanie funkcji pracujących z plikami i tak dalej.
Ich opis znajdziesz na stronie Klasy pomocnicze.
Adnotacje i pomijanie testów
Uruchamianie testów może być wpływane przez adnotacje w postaci komentarza phpDoc na początku pliku. Może wyglądać na przykład tak:
/**
* @phpExtension pdo, pdo_pgsql
* @phpVersion >= 7.2
*/
Podane adnotacje mówią, że test ma być uruchomiony tylko z wersją PHP 7.2 lub wyższą i jeśli obecne są rozszerzenia
PHP pdo i pdo_pgsql. Tymi adnotacjami kieruje się narzędzie do uruchamiania
testów z wiersza poleceń, które w przypadku, gdy warunki nie są spełnione, test pomija i w wypisie oznacza literą
s
– skipped. Jednak przy ręcznym uruchomieniu testu nie mają żadnego wpływu.
Opis adnotacji znajdziesz na stronie Adnotacje testów.
Test można również pominąć na podstawie spełnienia własnego warunku za pomocą Environment::skip()
. Na
przykład ta pominie testy na Windows:
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
Tester\Environment::skip('Requires UNIX.');
}
Struktura katalogów
Zalecamy przy tylko trochę większych bibliotekach lub projektach podzielić katalog z testami jeszcze na podkatalogi według przestrzeni nazw testowanej klasy:
└── tests/
├── NamespaceOne/
│ ├── MyClass.getUsers.phpt
│ ├── MyClass.setUsers.phpt
│ └── ...
│
├── NamespaceTwo/
│ ├── MyClass.creating.phpt
│ ├── MyClass.dropping.phpt
│ └── ...
│
├── bootstrap.php
└── ...
Będziesz mógł wtedy uruchamiać testy z pojedynczej przestrzeni nazw, czyli podkatalogu:
tester tests/NamespaceOne
Sytuacje specjalne
Test, który nie wywoła ani jednej metody asercji, jest podejrzany i zostanie oceniony jako błędny:
Error: This test forgets to execute an assertion.
Jeśli naprawdę test bez wywoływania asercji ma być uważany za ważny, wywołaj na przykład
Assert::true(true)
.
Również zdradliwe może być używanie exit()
i die()
do zakończenia testu z komunikatem błędu.
Na przykład exit('Error in connection')
zakończy test z kodem powrotu 0, co sygnalizuje sukces. Użyj
Assert::fail('Error in connection')
.