Вспомогательные классы

HttpAssert

Класс Tester\HttpAssert предоставляет инструменты для тестирования HTTP-серверов. Он позволяет легко выполнять HTTP-запросы и проверять коды состояния, заголовки и содержимое тела ответа с помощью текучего интерфейса.

# Базовый HTTP-запрос и проверка ответа
$response = Tester\HttpAssert::fetch('https://example.com/api/users');
$response
	->expectCode(200)
	->expectHeader('Content-Type', contains: 'json')
	->expectBody(contains: 'users');

Метод fetch() по умолчанию создаёт GET-запрос, но все параметры можно настроить:

HttpAssert::fetch(
	'https://api.example.com/users',
	method: 'POST',
	headers: [
		'Authorization' => 'Bearer token123',  # ассоциативный массив
		'Accept: application/json',            # либо строковый формат
	],
	cookies: ['session' => 'abc123'],
	follow: false,                             # не следовать за перенаправлениями
	body: '{"name": "John"}'
)
	->expectCode(201);

Коды состояния можно проверять методами expectCode() и denyCode(). Передать можно либо конкретное число, либо проверяющую функцию:

$response
	->expectCode(200)                           # точный код
	->expectCode(fn($code) => $code < 400)      # собственная проверка
	->denyCode(404)                             # не должно быть 404
	->denyCode(fn($code) => $code >= 500);      # не должно быть ошибки сервера

Для проверки заголовков служат методы expectHeader() и denyHeader(). Вы можете проверить, существует ли заголовок, сверить его точное значение или найти часть его содержимого:

$response
	->expectHeader('Content-Type')                     # заголовок должен существовать
	->expectHeader('Content-Type', 'application/json') # точное значение
	->expectHeader('Content-Type', contains: 'json')   # содержит текст
	->expectHeader('Server', matches: 'nginx %a%')     # соответствует образцу
	->denyHeader('X-Powered-By')                       # заголовка не должно быть
	->denyHeader('X-Debug', contains: 'sensitive')     # не должен содержать текст
	->denyHeader('X-Debug', matches: '~debug~i');      # не должен соответствовать образцу

Проверка тела ответа работает похоже, методами expectBody() и denyBody():

$response
	->expectBody('OK')                              # точное значение
	->expectBody(contains: '"status": "success"')   # содержит фрагмент JSON
	->expectBody(matches: '%A% hello %A%')          # соответствует образцу
	->expectBody(fn($body) => json_decode($body) !== null) # собственная проверка
	->denyBody('Error occurred')                    # не должно иметь точное значение
	->denyBody(contains: 'error')                   # не должно содержать текст
	->denyBody(matches: '~exception|fatal~i');      # не должно соответствовать образцу

Параметр follow управляет тем, как HttpAssert обрабатывает HTTP-перенаправления:

# Проверка перенаправления без следования за ним (по умолчанию)
HttpAssert::fetch('https://example.com/redirect', follow: false)
	->expectCode(301)
	->expectHeader('Location', 'https://example.com/new-url');

# Следование за всеми перенаправлениями до конечного ответа
HttpAssert::fetch('https://example.com/redirect', follow: true)
	->expectCode(200)
	->expectBody(contains: 'final content');

DomQuery

Tester\DomQuery – класс, расширяющий SimpleXMLElement, с удобным поиском в HTML или XML с помощью CSS-селекторов.

# создаём DomQuery из строки HTML
$dom = Tester\DomQuery::fromHtml('
	<article class="post">
		<h1>Title</h1>
		<div class="content">Text</div>
	</article>
');

# проверяем существование элемента CSS-селекторами
Assert::true($dom->has('article.post'));
Assert::true($dom->has('h1'));

# находим элементы как массив объектов DomQuery
$headings = $dom->find('h1');
Assert::same('Title', (string) $headings[0]);

# проверяем, соответствует ли элемент селектору (начиная с версии 2.5.3)
$content = $dom->find('.content')[0];
Assert::true($content->matches('div'));
Assert::false($content->matches('p'));

# находим ближайшего предка, соответствующего селектору (начиная с 2.5.5)
$article = $content->closest('.post');
Assert::true($article->matches('article'));

Для XML-документов используйте метод fromXml():

$dom = Tester\DomQuery::fromXml('<catalog><item>First</item></catalog>');
Assert::true($dom->has('item'));

FileMock

Tester\FileMock эмулирует файлы в памяти и облегчает тестирование кода, использующего функции вроде fopen(), file_get_contents(), parse_ini_file() и им подобные. Пример использования:

# Тестируемый класс
class Logger
{
	public function __construct(
		private string $logFile,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->logFile, $message . "\n", FILE_APPEND);
	}
}

# Новый пустой файл
$file = Tester\FileMock::create('');

$logger = new Logger($file);
$logger->log('Login');
$logger->log('Logout');

# Проверяем созданное содержимое
Assert::same("Login\nLogout\n", file_get_contents($file));

Необязательный второй параметр $extension задаёт расширение файла в порождённом URL, что удобно, когда тестируемый код принимает по нему решения:

$file = Tester\FileMock::create('{"key": "value"}', 'json');

Assert::with()

Это не утверждение, а помощник для тестирования приватных методов и свойств объектов.

class Entity
{
	private $enabled;
	// ...
}

$ent = new Entity;

Assert::with($ent, function () {
	Assert::true($this->enabled); // доступ к приватному $ent->enabled
});

Helpers::purge()

Метод purge() создаёт указанный каталог, а если тот уже существует, удаляет всё его содержимое. Он удобен для создания временного каталога. Например, в tests/bootstrap.php:

@mkdir(__DIR__ . '/tmp');  # @ - каталог может уже существовать

define('TempDir', __DIR__ . '/tmp/' . getmypid());
Tester\Helpers::purge(TempDir);

Environment::lock()

Тесты выполняются параллельно. Иногда, однако, нам нужно, чтобы выполнение тестов не пересекалось. Обычно тесты базы данных требуют подготовить содержимое базы и обеспечить, чтобы во время выполнения теста в базу не вмешивался другой тест. В таких тестах мы используем Tester\Environment::lock($name, $dir):

Tester\Environment::lock('database', __DIR__ . '/tmp');

Первый параметр – имя блокировки, второй – путь к каталогу для хранения блокировки. Тест, который получит блокировку первым, продолжает работу, остальные тесты должны дождаться его завершения.

Environment::bypassFinals()

Классы или методы, помеченные как final, трудно тестировать. Вызов Tester\Environment::bypassFinals() в начале теста приводит к тому, что при загрузке кода ключевые слова final опускаются.

require __DIR__ . '/bootstrap.php';

Tester\Environment::bypassFinals();

class MyClass extends NormallyFinalClass  # <-- NormallyFinalClass больше не final
{
	// ...
}

Environment::setup()

  • улучшает читаемость дампов ошибок (в том числе раскрашивание); иначе выводится стандартный стек вызовов PHP
  • включает проверку того, что в тесте были вызваны утверждения; иначе проходят и тесты без утверждений (например, забытых)
  • автоматически запускает сбор сведений о выполненном коде (при использовании --coverage) (описано далее)
  • выводит в конце скрипта состояние OK или FAILURE

Environment::setupFunctions()

Создаёт глобальные функции test(), testException(), testNoError(), setUp() и tearDown(), в которые вы можете разбить свои тесты.

test('описание теста', function () {
	Assert::same(123, foo());
	Assert::false(bar());
	// ...
});

Environment::VariableRunner

Позволяет выяснить, был ли тест запущен напрямую или через Tester.

if (getenv(Tester\Environment::VariableRunner)) {
	# запущен через Tester
} else {
	# запущен как-то иначе
}

Environment::VariableThread

Tester выполняет тесты параллельно в заданном количестве потоков. Если нас интересует номер потока, мы узнаём его из переменной окружения:

echo "Running in thread number " . getenv(Tester\Environment::VariableThread);