Написание тестов

Написание тестов для 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').