Вспомогательные классы
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);