Письменные тесты

Написание тестов для Nette Tester уникально тем, что каждый тест представляет собой PHP-скрипт, который может быть запущен отдельно. Это имеет большой потенциал. Написав тест, вы можете просто запустить его, чтобы проверить, работает ли он правильно. Если нет, вы можете легко пройтись по тесту в IDE и поискать ошибку.

Вы даже можете открыть тест в браузере. Но самое главное – запустив его, вы выполните тест. Вы сразу же узнаете, прошел он или не прошел.

Во вводной главе мы показали действительно тривиальный тест на использование массива PHP. Теперь мы создадим свой собственный класс, который мы будем тестировать, хотя он также будет простым.

Начнем с типичной схемы каталога библиотеки или проекта. Важно отделить тесты от остального кода, например, из-за развертывания, потому что мы не хотим загружать тесты на сервер. Структура может быть следующей:

├── src/           # code that we will test
│   ├── Rectangle.php
│   └── ...
├── tests/         # 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('Размерность не должна быть отрицательной.');
		}
		$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. Он содержит общий код для всех тестов. Например, автозагрузка классов, конфигурация окружения, создание временной директории, хелперы и тому подобное. Каждый тест загружает бутстрап и уделяет внимание только тестированию. Бутстрап может выглядеть следующим образом:

<?php
require __DIR__ . '/vendor/autoload.php'; # загрузить автозагрузку Composer

Tester\Environment::setup(); # инициализация Nette Tester

// и другие конфигурации (просто пример, в нашем случае они не нужны)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');

Этот бутстрап предполагает, что автозагрузчик Composer сможет загрузить и класс Rectangle.php. Это может быть достигнуто, например, установкой секции autoload в composer.json, и т.д.

Теперь мы можем запустить тест из командной строки, как любой другой отдельный PHP-скрипт. Первый запуск выявит любые синтаксические ошибки, и если вы не допустили опечаток, вы увидите:

$ php RectangleTest.php

OK

Если мы изменим в тесте утверждение на false 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,
	'Размер не должен быть отрицательным',
);

И мы добавляем аналогичный тест для высоты. Наконец, мы проверяем, что isSquare() возвращает true, если оба измерения одинаковы. Попробуйте написать такие тесты в качестве упражнения.

Хорошо организованные тесты

Размер файла с тестами может увеличиться и быстро стать загроможденным. Поэтому целесообразно группировать отдельные тестируемые области в отдельные функции.

Сначала мы покажем более простой, но элегантный вариант, используя глобальную функцию test(). Tester не создает ее автоматически, чтобы избежать коллизии, если в вашем коде есть функция с таким же именем. Он создается только методом setupFunctions(), который вы вызываете в файле bootstrap.php:

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

Используя эту функцию, мы можем красиво разделить тестовый файл на именованные блоки. При выполнении функции метки будут отображаться одна за другой.

<?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('размеры не должны быть отрицательными', 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);
	}
}

// Run test methods
(new RectangleTest)->run();

На этот раз мы использовали аннотацию @throw для проверки на исключения. Более подробную информацию смотрите в главе TestCase.

Функции-помощники

Nette Tester включает в себя несколько классов и функций, которые могут облегчить вам тестирование, например, помощники для тестирования содержимого HTML-документа, для тестирования функций работы с файлами и так далее.

Их описание вы можете найти на странице Helpers.

Аннотирование и пропуск тестов

На выполнение тестов могут влиять аннотации в комментарии phpDoc в начале файла. Например, он может выглядеть следующим образом:

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

Аннотации гласят, что тест должен выполняться только с PHP версии 7.2 или выше и при наличии PHP расширений pdo и pdo_pgsql. Эти аннотации контролируются программой запуска тестов командной строки, которая, если условия не выполняются, пропускает тест и помечает его буквой s – пропущен. Однако они не имеют никакого эффекта, когда тест выполняется вручную.

Описание аннотаций приведено в разделе Аннотации тестов.

Тест также может быть пропущен на основании собственного условия с помощью 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

Edge Cases

Тест, который не вызывает ни одного метода утверждения, является подозрительным и будет оценен как ошибочный:

Error: This test forgets to execute an assertion.

Если тест без вызова утверждений действительно должен считаться корректным, вызовите, например, Assert::true(true).

Также коварным может быть использование exit() и die() для завершения теста с сообщением об ошибке. Например, exit('Error in connection') завершает тест с кодом выхода 0, который сигнализирует об успехе. Используйте Assert::fail('Error in connection').