Testy pisarskie
Pisanie testów dla Nette Tester jest o tyle wyjątkowe, że każdy test jest skryptem PHP, który może być uruchamiany niezależnie. Kryje się w tym spory potencjał. Nawet gdy napiszesz test, możesz go po prostu uruchomić i sprawdzić, czy działa poprawnie. Jeśli tak nie jest, możesz łatwo przejść przez to w IDE i poszukać błędu.
Możesz nawet otworzyć test w przeglądarce. Ale co najważniejsze – uruchamiając go, wykonujesz test. Od razu dowiesz się, czy przeszedł, czy nie.
W rozdziale wprowadzającym pokazaliśmy naprawdę trywialny test pracy z tablicą. Teraz stworzymy własną klasę do testowania, choć ona również będzie prosta.
Zaczniemy od typowej struktury katalogów dla biblioteki lub projektu. Ważne jest, aby oddzielić testy od reszty kodu, na przykład do deploymentu, ponieważ nie chcemy wrzucać testów na żywy serwer. Struktura może być na przykład:
├── src/ # kód, který budeme testovat
│ ├── Rectangle.php
│ └── ...
├── tests/ # testy
│ ├── bootstrap.php
│ ├── RectangleTest.php
│ └── ...
├── vendor/
└── composer.json
A teraz tworzymy poszczególne pliki. Zaczynamy od testowanej klasy, którą umieszczamy 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 stworzyć dla niego test. Nazwa pliku testowego powinna odpowiadać masce *Test.php
lub *.phpt
, na
przykład wybierz RectangleTest.php
:
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
// rodzajowy podłużny
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea()); # walidacja oczekiwanych wyników
Assert::false($rect->isSquare());
Jak widać, metody asercji takie jak Assert::same()
są używane do potwierdzenia,
że rzeczywista wartość pasuje do wartości oczekiwanej.
Pozostaje ostatni krok, którym jest plik bootstrap.php
. Zawiera on kod wspólny dla wszystkich testów, taki jak
autoloading klas, konfiguracja środowiska, tworzenie katalogu tymczasowego, funkcje pomocnicze i tak dalej. Wszystkie testy
ładują bootstrap i kontynuują tylko testowanie. Bootstrap może wyglądać jak poniżej:
<?php
require __DIR__ . '/vendor/autoload.php'; # załaduj autoloader Composera
Tester\Environment::setup(); # zainicjuj Tester Nette
// i inne konfiguracje (to tylko przykład, nie są one potrzebne w naszym przypadku)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');
Powyższy bootstrap zakłada, że autoloader Composera będzie w stanie załadować również klasę
Rectangle.php
. Można to zrobić na przykład poprzez
ustawienie sekcji autoload w composer.json
itp.
Test może być teraz uruchamiany z linii poleceń jak każdy inny samodzielny skrypt PHP. Uruchomienie go po raz pierwszy ujawni wszelkie błędy składniowe, a jeśli nigdzie nie ma literówki, zostanie wydrukowany:
$ php RectangleTest.php
OK
Gdybyśmy zmienili stwierdzenie w teście na false Assert::same(123, $rect->getArea());
to co by
się stało:
$ php RectangleTest.php Failed: 200.0 should be 123 in RectangleTest.php(5) Assert::same(123, $rect->getArea()); FAILURE
Podczas pisania testów dobrze jest objąć wszystkie skrajności. Na przykład, jeśli dane wejściowe są zerowe, liczba ujemna, w innych przypadkach pusty ciąg, null, itp. W rzeczywistości zmusza cię do myślenia i decydowania, jak kod powinien zachowywać się w takich sytuacjach. Testy następnie naprawiają zachowanie.
W naszym przypadku wartość ujemna ma rzucić wyjątek, co sprawdzamy za pomocą Assert::exception():
// szerokość nie może być ujemna
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
'Wymiar nie może być ujemny',
);
I dodać podobny test dla wzrostu. Na koniec testujemy, że isSquare()
zwraca true
, jeśli oba
wymiary są takie same. Spróbuj napisać takie testy jako ćwiczenie.
Bardziej przejrzyste testy
Rozmiar pliku testowego może rosnąć i szybko stać się niezorganizowany. Dlatego praktycznym rozwiązaniem jest pogrupowanie różnych obszarów testowych w osobne funkcje.
Najpierw pokażemy prostszy, ale elegancki wariant, wykorzystujący funkcję globalną test()
. Tester nie tworzy
go automatycznie, aby uniknąć kolizji, jeśli miałeś funkcję o tej samej nazwie w swoim kodzie. Tworzy go dopiero metoda
setupFunctions()
, którą wywołujesz w pliku bootstrap.php
:
Tester\Environment::setup();
Tester\Environment::setupFunctions();
Dzięki tej funkcji możemy ładnie podzielić plik testowy na nazwane jednostki. Po uruchomieniu etykiety zostaną wymienione jedna po drugiej.
<?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,
);
});
Jeśli potrzebujesz uruchomić kod przed lub po każdym teście, przekaż go do funkcji setUp()
lub
tearDown()
:
setUp(function () {
// kod inicjalizacyjny, który uruchamia się przed każdym testem()
});
Drugą opcją jest orientacja obiektowa. Tworzymy coś, co nazywa się TestCase, czyli klasę, w której jednostki reprezentują metody, których nazwy zaczynają się od 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ścizna testowa metody
(new RectangleTest)->run();
Tym razem do testowania wyjątków wykorzystaliśmy adnotację @throw
. Zobacz rozdział TestCase, aby dowiedzieć się więcej.
Funkcje pomocnicze
Nette Tester zawiera kilka klas i funkcji, które mogą ułatwić Ci np. testowanie zawartości dokumentu HTML, testowanie funkcji pracujących z plikami itd.
Ich opis można znaleźć na stronie Klasy pomocnicze.
Dodawanie adnotacji i pomijanie testów
Na uruchomienie testów mogą mieć wpływ adnotacje w postaci komentarzy phpDoc na początku pliku. Na przykład może to wyglądać tak:
/**
* @phpExtension pdo, pdo_pgsql
* @phpVersion >= 7.2
*/
Te adnotacje mówią, że test powinien być uruchomiony tylko z PHP w wersji 7.2 lub wyższej i jeśli rozszerzenia PHP pdo
i pdo_pgsql są obecne. Za tymi adnotacjami podąża runner testów z linii poleceń, który
pominie test, jeśli warunki nie są spełnione i oznaczy wyjście za pomocą s
– skipped. Jednak podczas
ręcznego uruchamiania testu nie mają one żadnego efektu.
Opis adnotacji znajduje się na stronie Adnotacje do testów.
Możesz również zlecić pominięcie testu w oparciu o spełnienie niestandardowego warunku, używając
Environment::skip()
. Na przykład pomija to testy w systemie Windows:
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
Tester\Environment::skip('Requires UNIX.');
}
Struktura katalogu
Zalecamy, aby w przypadku nieco większych bibliotek lub projektów, katalog testowy był dodatkowo podzielony 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
└── ...
W ten sposób możesz uruchomić testy z pojedynczej przestrzeni nazw lub podkatalogu:
tester tests/NamespaceOne
Sytuacje szczególne
Test, który nie wywołuje żadnej z metod asercji jest podejrzany i zostanie oceniony jako niepowodzenie:
Error: This test forgets to execute an assertion.
Jeśli test bez wywoływania asercji ma być naprawdę uznany za ważny, wywołaj na przykład
Assert::true(true)
.
Podstępne może być również użycie exit()
i die()
, aby wyjść z testu z komunikatem
o błędzie. Na przykład exit('Error in connection')
zakończy test z kodem zwrotnym 0, oznaczającym sukces.
Skorzystaj z Assert::fail('Error in connection')
.