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