Написание тестов
Написание тестов для Nette Tester уникально тем, что каждый тест — это PHP-скрипт, который можно запустить самостоятельно. Это скрывает большой потенциал. Уже когда вы пишете тест, вы можете его просто запустить и выяснить, правильно ли он работает. Если нет, его можно легко отладить в IDE и найти ошибку.
Тест можно даже открыть в браузере. Но прежде всего — запустив его, вы выполните тест. Вы сразу узнаете, прошел он или не удался.
Во вводной главе мы показали действительно тривиальный тест работы с массивом. Теперь мы создадим собственный класс, который будем тестировать, хотя он тоже будет простым.
Начнем с типичной структуры каталогов для библиотеки или проекта. Важно отделить тесты от остального кода, например, из-за развертывания, потому что тесты на рабочий сервер мы загружать не хотим. Структура может быть, например, такой:
├── src/ # код, который будем тестировать
│ ├── Rectangle.php
│ └── ...
├── tests/ # тесты
│ ├── bootstrap.php
│ ├── RectangleTest.php
│ └── ...
├── vendor/
└── composer.json
А теперь создадим отдельные файлы. Начнем с тестируемого класса,
который поместим в файл 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;
}
}
И создадим для него тест. Имя файла с тестом должно соответствовать
маске *Test.php
или *.phpt
, выберем, например, вариант
RectangleTest.php
:
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
// общий прямоугольник
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea()); # проверим ожидаемые результаты
Assert::false($rect->isSquare());
Как видите, так называемые методы
утверждений вроде Assert::same()
используются для подтверждения
того, что фактическое значение соответствует ожидаемому значению.
Остался последний шаг, и это файл bootstrap.php
. Он содержит код,
общий для всех тестов, например, автозагрузку классов, конфигурацию
среды, создание временного каталога, вспомогательные функции и тому
подобное. Все тесты загружают bootstrap и далее занимаются только
тестированием. Bootstrap может выглядеть следующим образом:
<?php
require __DIR__ . '/vendor/autoload.php'; # загружает автозагрузчик Composer
Tester\Environment::setup(); # инициализация Nette Tester
// и другая конфигурация (это только пример, в нашем случае не нужны)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');
Указанный bootstrap предполагает, что автозагрузчик Composer сможет
загрузить и класс Rectangle.php
. Этого можно достичь, например, настройкой секции autoload в
composer.json
и т.д.
Тест теперь можно запустить из командной строки как любой другой самостоятельный PHP-скрипт. Первый запуск выявит возможные синтаксические ошибки, и если нигде нет опечатки, выведется:
$ php RectangleTest.php
OK
Если бы мы изменили в тесте утверждение на неверное
Assert::same(123, $rect->getArea());
, произошло бы следующее:
$ php RectangleTest.php Failed: 200.0 should be 123 in RectangleTest.php(5) Assert::same(123, $rect->getArea()); FAILURE
При написании тестов хорошо охватить все крайние ситуации. Например, когда входом будет ноль, отрицательное число, в других случаях, например, пустая строка, null и т.д. На самом деле это заставляет вас задуматься и решить, как должен вести себя код в таких ситуациях. Тесты затем фиксируют поведение.
В нашем случае отрицательное значение должно выбросить исключение, что мы проверим с помощью Assert::exception():
// ширина не должна быть отрицательной
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
'The dimension must not be negative.',
);
И аналогичный тест добавим для высоты. Наконец, протестируем, что
isSquare()
вернет true
, если оба размера одинаковы. Попробуйте в
качестве упражнения написать такие тесты.
Более читаемые тесты
Размер файла с тестом может расти и быстро стать нечитаемым. Поэтому практично отдельные тестируемые области сгруппировать в самостоятельные функции.
Сначала покажем более простой, но элегантный вариант, а именно с
помощью глобальной функции test()
. Tester ее не создает
автоматически, чтобы не произошло коллизии, если бы у вас в коде была
функция с таким же именем. Ее создаст метод setupFunctions()
, который
вызовите в файле bootstrap.php
:
Tester\Environment::setup();
Tester\Environment::setupFunctions();
С помощью этой функции мы можем красиво разбить тестовый файл на именованные части. При запуске будут последовательно выводиться описания.
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
test('общий прямоугольник', function () {
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());
Assert::false($rect->isSquare());
});
test('общий квадрат', function () {
$rect = new Rectangle(5, 5);
Assert::same(25.0, $rect->getArea());
Assert::true($rect->isSquare());
});
test('размеры не должны быть отрицательными', function () {
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
);
Assert::exception(
fn() => new Rectangle(10, -1),
InvalidArgumentException::class,
);
});
Если вам нужно перед или после каждого теста запустить код, передайте
его функции setUp()
соответственно tearDown()
:
setUp(function () {
// инициализационный код, который запустится перед каждым test()
});
Второй вариант — объектно-ориентированный. Мы создадим так называемый TestCase, то есть класс, где отдельные части представляют собой методы, названия которых начинаются на 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);
}
}
// Запуск тестовых методов
(new RectangleTest)->run();
Для тестирования исключений мы на этот раз использовали аннотацию
@throw
. Больше вы узнаете в главе TestCase.
Вспомогательные функции
Nette Tester содержит несколько классов и функций, которые могут вам облегчить, например, тестирование содержимого HTML-документа, тестирование функций, работающих с файлами, и так далее.
Их описание вы найдете на странице Вспомогательные классы.
Аннотации и пропуск тестов
Выполнение тестов может быть подвержено влиянию аннотаций в виде phpDoc-комментария в начале файла. Он может выглядеть, например, так:
/**
* @phpExtension pdo, pdo_pgsql
* @phpVersion >= 7.2
*/
Указанные аннотации говорят, что тест должен быть запущен только с PHP
версии 7.2 или выше и если присутствуют PHP-расширения pdo и pdo_pgsql. Этими
аннотациями руководствуется средство
запуска тестов из командной строки, которое в случае, если условия не
выполнены, тест пропускает и в выводе помечает буквой s
– skipped.
Однако при ручном запуске теста они не имеют никакого влияния.
Описание аннотаций вы найдете на странице Аннотации тестов.
Тест можно пропустить также на основании выполнения собственного
условия с помощью Environment::skip()
. Например, так пропустим тесты на
Windows:
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
Tester\Environment::skip('Requires UNIX.');
}
Структура каталогов
Рекомендуем у хотя бы немного больших библиотек или проектов разделить каталог с тестами еще на подкаталоги по пространству имен тестируемого класса:
└── tests/
├── NamespaceOne/
│ ├── MyClass.getUsers.phpt
│ ├── MyClass.setUsers.phpt
│ └── ...
│
├── NamespaceTwo/
│ ├── MyClass.creating.phpt
│ ├── MyClass.dropping.phpt
│ └── ...
│
├── bootstrap.php
└── ...
Вы сможете так запускать тесты из одного пространства имен, то есть подкаталога:
tester tests/NamespaceOne
Специальные ситуации
Тест, который не вызвал ни одного метода утверждения, подозрителен и будет оценен как ошибочный:
Error: This test forgets to execute an assertion.
Если действительно тест без вызова утверждений должен считаться
действительным, вызовите, например, Assert::true(true)
.
Также может быть коварно использовать exit()
и die()
для
завершения теста с сообщением об ошибке. Например,
exit('Error in connection')
завершит тест с кодом возврата 0, что
сигнализирует об успехе. Используйте Assert::fail('Error in connection')
.