Написання тестів
Написання тестів для 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());
Як бачите, так звані методи assertion як
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
Спеціальні ситуації
Тест, який не викликає жодного методу assertion, є підозрілим і оцінюється як помилковий:
Error: This test forgets to execute an assertion.
Якщо дійсно тест без виклику assertion має вважатися валідним, викличте,
наприклад, Assert::true(true)
.
Також може бути підступним використовувати exit()
та die()
для завершення тесту з повідомленням про помилку. Наприклад,
exit('Error in connection')
завершить тест з кодом повернення 0, що
сигналізує про успіх. Використовуйте Assert::fail('Error in connection')
.