Письмові тести

Написання тестів для 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);
	}
}

// Запуск методів тестування
(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').