Nette Documentation Preview

syntax
Писмени тестове
***************

.[perex]
Писането на тестове за Nette Tester е уникално с това, че всеки тест е PHP скрипт, който може да се изпълнява отделно. Това има голям потенциал.
След като сте написали тест, можете просто да го стартирате, за да проверите дали работи правилно. Ако това не е така, можете лесно да преминете през теста в IDE и да потърсите грешка.

Можете дори да отворите теста в браузър. Но най-важното е, че след като го стартирате, стартирате теста. Веднага ще разберете дали е преминал успешно или не.

В уводната глава [показахме |guide#What-Makes-Tester-Unique] един наистина тривиален тест за масиви на PHP. Сега ще създадем наш собствен клас, който ще тестваме, въпреки че той също е прост.

Ще започнем с типична схема на директория на библиотека или проект. Важно е да се отделят тестовете от останалата част от кода, например заради внедряването, тъй като не искаме да качваме тестовете на сървъра. Структурата може да бъде следната:

```
├── src/           # code that we will test
│   ├── Rectangle.php
│   └── ...
├── tests/         # 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('Размерность не должна быть отрицательной.');
		}
		$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());
```

Както можете да видите, [методите за утвърждаване, |Assertions] като например `Assert::same()`, се използват за утвърждаване, че действителната стойност съвпада с очакваната стойност.

Последната стъпка е да се създаде файлът `bootstrap.php`. Тя съдържа общ код за всички тестове. Например, автоматично зареждане на класове, конфигурация на средата, създаване на временна директория, помощни програми и други подобни. Всеки тест зарежда bootstrap и се фокусира само върху тестването. Един bootstrap може да изглежда по следния начин:

```php .{file:tests/bootstrap.php}
<?php
require __DIR__ . '/vendor/autoload.php'; # зареждане на Composer autoloader

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

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

.[note]
Този bootstrap предполага, че автозареждащият модул на Composer също ще може да зареди класа `Rectangle.php`. Това може да се постигне например [чрез задаване на секцията за автоматично зареждане на |best-practices:composer#Autoloading] `composer.json`, и т.н.

Сега можем да стартираме теста от командния ред като всеки друг PHP скрипт. Първото стартиране ще открие всички синтактични грешки и ако не сте допуснали никакви грешки, ще видите

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

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

Ако променим твърдението в теста на false `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>
\--


Когато пишете тестове, е полезно да уловите всички екстремни ситуации. Например, ако входът е нула, отрицателно число, в други случаи - празен низ, нула и т.н. Всъщност тя ви принуждава да мислите и да решавате как да се държи кодът в такива ситуации. След това тестовете коригират поведението.

В нашия случай отрицателната стойност трябва да предизвика изключение, което проверяваме с [Assert::exception() |Assertions#Assert-exception]:

```php .{file:tests/RectangleTest.php}
// ширината не трябва да е отрицателно число
Assert::exception(
	fn() => new Rectangle(-1, 20),
	InvalidArgumentException::class,
	'Размерът не трябва да е отрицателно число,
);
```

Добавяме и подобен тест за височина. Накрая проверяваме дали `isSquare()` връща `true`, ако двете измервания са еднакви. Опитайте се да напишете такива тестове като упражнение.


Добре организирани тестове .[#toc-well-arranged-tests]
======================================================

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

Първо ще покажем по-прост, но по-елегантен вариант, като използваме глобалната функция `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('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()`:

```php
setUp(function () {
	// инициализиране на код, който да се изпълнява преди всеки тест()
});
```

Второй вариант - объектный. Мы создадим так называемый TestCase, который представляет собой класс, где отдельные единицы представлены методами, имена которых начинаются с test-.

```php .{file:tests/RectangleTest.php}
клас RectangleTest разширява Tester\TestCase
{
	публична функция testGeneralOblong()
	{
		$rect = нов правоъгълник(10, 20);
		Assert::same(200.0, $rect->getArea());
		Assert::false($rect->isSquare());
	}

	публична функция testGeneralSquare()
	{
		$rect = нов правоъгълник(5, 5);
		Assert::same(25.0, $rect->getArea());
		Assert::true($rect->isSquare());
	}

	/** @throws InvalidArgumentException */
	публична функция testWidthMustNotBeNegative()
	{
		$rect = нов правоъгълник(-1, 20);
	}

	/** @throws InvalidArgumentException */
	публична функция testHeightMustNotBeNegative()
	{
		$rect = нов правоъгълник(10, -1);
	}
}

// Изпълнение на тестови методи
(нов RectangleTest)->run();
```

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


Функции-помощники .[#toc-helpers-functions]
===========================================

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

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


Аннотирование и пропуск тестов .[#toc-annotation-and-skipping-tests]
====================================================================

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

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

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

Описание аннотаций приведено в разделе [Аннотации тестов |Test Annotations].

Тест также может быть пропущен на основании собственного условия с помощью `Environment::skip()`. Например, мы пропустим этот тест на Windows:

```php
if (defined('PHP_WINDOWS_VERSION_BUILD') {
	Tester\Environment::skip('Изисква UNIX.');
}
```


Структура каталогов .[#toc-directory-structure]
===============================================

Для немного больших библиотек или проектов мы рекомендуем разделить тестовый каталог на подкаталоги в соответствии с пространством имен тестируемого класса:

```
└── тестове/.
	├── NamespaceOne/
	│ ├── MyClass.getUsers.phpt
	│ ├── MyClass.setUsers.phpt
	│ └── ...
	│
	├── NamespaceTwo/
	│ ├── MyClass.creating.phpt
	│ ├── MyClass.dropping.phpt
	│ └── ...
	│
	├── bootstrap.php
	└── ...
```

Ще можете да стартирате тестове от същото пространство от имена, т.е. от поддиректория:

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


Крайни случаи .[#toc-edge-cases]
================================

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

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

Ако един тест без извикване на твърдения наистина трябва да се счита за правилен, извикайте например `Assert::true(true)`.

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

Писмени тестове

Писането на тестове за 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. Тя съдържа общ код за всички тестове. Например, автоматично зареждане на класове, конфигурация на средата, създаване на временна директория, помощни програми и други подобни. Всеки тест зарежда bootstrap и се фокусира само върху тестването. Един bootstrap може да изглежда по следния начин:

<?php
require __DIR__ . '/vendor/autoload.php'; # зареждане на Composer autoloader

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

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

Този bootstrap предполага, че автозареждащият модул на Composer също ще може да зареди класа Rectangle.php. Това може да се постигне например чрез задаване на секцията за автоматично зареждане на 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

Когато пишете тестове, е полезно да уловите всички екстремни ситуации. Например, ако входът е нула, отрицателно число, в други случаи – празен низ, нула и т.н. Всъщност тя ви принуждава да мислите и да решавате как да се държи кодът в такива ситуации. След това тестовете коригират поведението.

В нашия случай отрицателната стойност трябва да предизвика изключение, което проверяваме с 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 () {
	// инициализиране на код, който да се изпълнява преди всеки тест()
});

Второй вариант – объектный. Мы создадим так называемый TestCase, который представляет собой класс, где отдельные единицы представлены методами, имена которых начинаются с test-.

клас RectangleTest разширява Tester\TestCase
{
	публична функция testGeneralOblong()
	{
		$rect = нов правоъгълник(10, 20);
		Assert::same(200.0, $rect->getArea());
		Assert::false($rect->isSquare());
	}

	публична функция testGeneralSquare()
	{
		$rect = нов правоъгълник(5, 5);
		Assert::same(25.0, $rect->getArea());
		Assert::true($rect->isSquare());
	}

	/** @throws InvalidArgumentException */
	публична функция testWidthMustNotBeNegative()
	{
		$rect = нов правоъгълник(-1, 20);
	}

	/** @throws InvalidArgumentException */
	публична функция testHeightMustNotBeNegative()
	{
		$rect = нов правоъгълник(10, -1);
	}
}

// Изпълнение на тестови методи
(нов 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('Изисква UNIX.');
}

Структура каталогов

Для немного больших библиотек или проектов мы рекомендуем разделить тестовый каталог на подкаталоги в соответствии с пространством имен тестируемого класса:

└── тестове/.
	├── NamespaceOne/
	│ ├── MyClass.getUsers.phpt
	│ ├── MyClass.setUsers.phpt
	│ └── ...
	│
	├── NamespaceTwo/
	│ ├── MyClass.creating.phpt
	│ ├── MyClass.dropping.phpt
	│ └── ...
	│
	├── bootstrap.php
	└── ...

Ще можете да стартирате тестове от същото пространство от имена, т.е. от поддиректория:

tester tests/NamespaceOne

Крайни случаи

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

Error: This test forgets to execute an assertion.

Ако един тест без извикване на твърдения наистина трябва да се счита за правилен, извикайте например Assert::true(true).

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