Nette Documentation Preview

syntax
Написання тестів
****************

.[perex]
Написання тестів для Nette Tester унікальне тим, що кожен тест — це PHP-скрипт, який можна запустити окремо. Це приховує великий потенціал. Вже коли ви пишете тест, ви можете його просто запускати та з'ясовувати, чи працює він правильно. Якщо ні, його можна легко крокувати в IDE та шукати помилку.

Тест можна навіть відкрити в браузері. Але головне — тим, що ви його запускаєте, ви виконуєте тест. Ви миттєво дізнаєтеся, чи він пройшов, чи зазнав невдачі.

У вступному розділі ми [показали |guide#Чим Tester унікальний] справді тривіальний тест роботи з масивом. Тепер ми створимо власний клас, який будемо тестувати, хоча він також буде простим.

Почнемо з типової структури директорій для бібліотеки або проєкту. Важливо відокремити тести від решти коду, наприклад, через розгортання, оскільки тести на робочий сервер завантажувати ми не хочемо. Структура може бути, наприклад, такою:

```
├── src/           # код, який будемо тестувати
│   ├── Rectangle.php
│   └── ...
├── tests/         # тести
│   ├── bootstrap.php
│   ├── RectangleTest.php
│   └── ...
├── vendor/
└── composer.json
```

А тепер створимо окремі файли. Почнемо з тестованого класу, який розмістимо у файлі `src/Rectangle.php`

```php .{file: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 .{file:tests/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|assertions] як `Assert::same()` використовуються для підтвердження того, що фактичне значення відповідає очікуваному значенню.

Залишився останній крок — це файл `bootstrap.php`. Він містить код, спільний для всіх тестів, наприклад, автозавантаження класів, конфігурацію середовища, створення тимчасової директорії, допоміжні функції тощо. Всі тести завантажують bootstrap і далі займаються лише тестуванням. Bootstrap може виглядати наступним чином:

```php .{file:tests/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');
```

.[note]
Наведений bootstrap передбачає, що автозавантажувач Composer зможе завантажити й клас `Rectangle.php`. Цього можна досягти, наприклад, [налаштуванням секції autoload |best-practices:composer#Автозавантаження] у `composer.json` тощо.

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

/--pre .[terminal]
$ php RectangleTest.php

<span style="color:#FFF; background-color:#090">OK</span>
\--

Якщо ми змінимо в тесті твердження на хибне `Assert::same(123, $rect->getArea());`, станеться ось що:

/--pre .[terminal]
$ php RectangleTest.php

<span style="color: #FFF">Failed: </span><span style="color: #FF0">200.0</span><span style="color: #FFF"> should be </span><span style="color: #FF0">123</span>

<span style="color: #CCC">in </span><span style="color: #FFF">RectangleTest.php(5)</span><span style="color: #808080"> Assert::same(123, $rect->getArea());</span>

<span style="color: #FFF; background-color: #900">FAILURE</span>
\--


При написанні тестів добре врахувати всі граничні ситуації. Наприклад, коли входом буде нуль, від'ємне число, в інших випадках, наприклад, порожній рядок, null тощо. Власне, це змушує вас замислитися і вирішити, як має поводитися код у таких ситуаціях. Тести потім фіксують поведінку.

У нашому випадку від'ємне значення має викинути виняток, що ми перевіримо за допомогою [Assert::exception() |Assertions#Assert::exception]:

```php .{file:tests/RectangleTest.php}
// ширина не має бути від'ємною
Assert::exception(
	fn() => new Rectangle(-1, 20),
	InvalidArgumentException::class,
	'The dimension must not be negative.',
);
```

І аналогічний тест додамо для висоти. Нарешті, протестуємо, що `isSquare()` поверне `true`, якщо обидва розміри однакові. Спробуйте як вправу написати такі тести.


Зрозуміліші тести
=================

Розмір файлу з тестом може зростати і швидко стати незрозумілим. Тому практично згрупувати окремі тестовані області в самостійні функції.

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

```php .{file:tests/bootstrap.php}
Tester\Environment::setup();
Tester\Environment::setupFunctions();
```

За допомогою цієї функції ми можемо гарно розділити тестовий файл на пойменовані частини. При запуску будуть послідовно виводитися описи.

```php .{file:tests/RectangleTest.php}
<?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()`:

```php
setUp(function () {
	// ініціалізаційний код, який запуститься перед кожним test()
});
```

Другий варіант — об'єктний. Створимо так званий TestCase, тобто клас, де окремі частини представляють методи, назви яких починаються на `test-`.

```php .{file:tests/RectangleTest.php}
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 |TestCase].


Допоміжні функції
=================

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

Їхній опис знайдете на сторінці [Допоміжні класи|helpers].


Анотації та пропуск тестів
==========================

Запуск тестів може бути вплинутий анотаціями у вигляді phpDoc коментаря на початку файлу. Він може виглядати, наприклад, так:

```php .{file:tests/RectangleTest.php}
/**
 * @phpExtension pdo, pdo_pgsql
 * @phpVersion >= 7.2
 */
```

Наведені анотації говорять, що тест має бути запущений лише з PHP версії 7.2 або вище і якщо присутні розширення PHP pdo та pdo_pgsql. Цими анотаціями керується [запускач тестів з командного рядка|running-tests], який у випадку, якщо умови не виконані, тест пропустить і у виводі позначить літерою `s` - skipped. Однак при ручному запуску тесту вони не мають жодного впливу.

Опис анотацій знайдете на сторінці [Анотації тестів|test-annotations].

Тест можна пропустити також на основі виконання власної умови за допомогою `Environment::skip()`. Наприклад, ця пропустить тести на Windows:

```php
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
	└── ...
```

Ви зможете запускати тести з єдиного простору імен, тобто піддиректорії:

/--pre .[terminal]
tester tests/NamespaceOne
\--


Спеціальні ситуації
===================

Тест, який не викликає жодного методу assertion, є підозрілим і оцінюється як помилковий:

/--pre .[terminal]
<span style="color: #FFF; background-color: #900">Error: This test forgets to execute an assertion.</span>
\--

Якщо дійсно тест без виклику assertion має вважатися валідним, викличте, наприклад, `Assert::true(true)`.

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

Написання тестів

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