Привет, Хабр!
Сегодня рассмотрим Pest — минималистичный, но выразительный тестовый фреймворк для PHP. Он построен поверх PHPUnit и переосмысляет подход к написанию тестов: делает их лаконичнее, читаемее и проще в поддержке.
Pest — не альтернатива PHPUnit, а надстройка над ним. Он предоставляет декларативный DSL, сохраняя все фичи PHPUnit. Это позволяет использовать существующие PHPUnit-фичи, включая assertions, мок-объекты, аннотации, и при этом писать тесты в более компактной форме.
Как устроен синтаксис
Основные строительные блоки: test, it, expect, хуки (beforeEach, afterEach, beforeAll, afterAll) и fluent-методы, расширяющие возможности через ->with(), ->skip(), ->only(), ->throws() и др.
test() и it(): базовые единицы
test('2 + 2 равно 4', function () { expect(2 + 2)->toBe(4); }); it('возвращает true для положительного числа', function () { $value = 10; expect($value > 0)->toBeTrue(); });
Функции test() и it() идентичны по функциональности. Разница — только в стилистике описания. test() чаще используют для unit и feature-тестов, it() — для BDD-стиля.
Аргументы:
-
string $description — описание теста (обязателен)
-
Closure $closure — логика теста, опционально с параметрами
expect(): хелпер-обёртка над assertions
В Pest отсутствует привычный assert*-синтаксис. Вместо этого используется fluent-интерфейс expect(...), в основе которого лежат matchers.
Примеры:
expect($value)->toBe(42); // === expect($array)->toContain('foo'); // in_array expect($text)->toStartWith('Hello'); // str_starts_with expect($response)->toThrow(SomeException::class); // expectException
Также доступно not():
expect($list)->not()->toContain('bar');
Полный список встроенных матчеров:
-
toBe,toEqual,toMatchArray,toBeInstanceOf,toBeTrue,toBeFalse -
toContain,toStartWith,toEndWith,toHaveCount,toBeEmpty,toBeNull -
toThrow,toThrow(fn($e) => $e->getCode() === 403)— для кастомной проверки исключений -
not()— инвертирует любой следующий матч
Можно легко писать собственные matchers.
Хуки: beforeEach, beforeAll и другие
Pest предлагает familiar-интерфейс для инициализации окружения через хуки. Они заменяют необходимость переопределять setUp() в каждом классе.
beforeEach(function () { $this->user = User::factory()->create(); }); afterEach(function () { // clean up });
Есть четыре типа хуков:
-
beforeEach()— перед каждым тестом в пределах файла -
afterEach()— после каждого теста -
beforeAll()/afterAll()— аналогично, но один раз на весь файл
Контекст внутри Closure передаётся как $this, то есть доступны свойства и методы, объявленные в классе TestCase, если вы используете uses(...)->in(...).
Группировка: describe() и dataset()
Для логической группировки тестов можно использовать describe():
describe('User API', function () { beforeEach(function () { $this->user = User::factory()->create(); }); it('возвращает 200', function () { $response = $this->getJson("/api/users/{$this->user->id}"); $response->assertOk(); }); it('содержит имя пользователя', function () { $response = $this->getJson("/api/users/{$this->user->id}"); expect($response['name'])->toBe($this->user->name); }); });
Функция describe() создаёт скоуп с shared-хуками и переменными.
Параметризация
Pest предлагает нативную поддержку параметризованных тестов через метод ->with(...).
it('делится на 2', function ($number) { expect($number % 2)->toBe(0); })->with([2, 4, 6]);
Кейсы можно именовать:
->with([ 'двойка' => 2, 'четвёрка' => 4, 'шестёрка' => 6, ])
Для повторного использования: dataset(...)
dataset('even numbers', [2, 4, 6]); it('делится на 2', function ($number) { expect($number % 2)->toBe(0); })->with('even numbers');
Поддерживаются генераторы:
dataset('слайды', function () { yield 'слайд 1' => ['title' => 'Intro']; yield 'слайд 2' => ['title' => 'Overview']; });
Фильтрация и управление выполнением
Pest предоставляет fluent-интерфейс для управления выполнением тестов:
-
->skip()— пропустить тест -
->only()— запускать только этот тест -
->throws(...)— проверка на исключение -
->repeat(n)— запускать тест n раз -
->depends(...)— зависимость от других тестов
Пример:
test('не реализован')->skip(); test('бросает исключение', function () { throw new InvalidArgumentException(); })->throws(InvalidArgumentException::class);
Кастомные matchers и expectations
Собственный DSL можно расширять через expect()->extend():
expect()->extend('toBeEven', function () { return $this->toBeInt()->and($this->value % 2 === 0); }); test('42 — чётное', function () { expect(42)->toBeEven(); });
Это позволяет наращивать выразительность тестов в стиле документации.
Где и как это применяют
Юнит-тест бизнес-логики без зависимостей
Задача: проверить, что метод User->isAdult() возвращает true при возрасте ≥18.
test('пользователь совершеннолетний', function () { $user = new User(age: 20); expect($user->isAdult())->toBeTrue(); });
Такой юнит легко поддерживать и рефакторить.
Тест API-эндпоинта через Laravel HTTP Kernel
Задача: проверить, что /api/posts возвращает 200 OK и содержит JSON-массив.
test('GET /api/posts возвращает список', function () { Post::factory()->count(3)->create(); $response = $this->getJson('/api/posts'); $response->assertOk(); $response->assertJsonIsArray(); });
$this — это Laravel TestCase, если предварительно указан uses(Tests\TestCase::class)->in(...). Pest умеет в DI и Laravel-контекст.
Параметризованный тест алгоритма
Задача: проверить функцию isPalindrome(string $input) на разных кейсах.
function isPalindrome(string $input): bool { return strrev($input) === $input; } it('распознаёт палиндромы', function ($word, $expected) { expect(isPalindrome($word))->toBe($expected); })->with([ ['level', true], ['racecar', true], ['hello', false], ['radar', true], ]);
Поддерживается передача нескольких аргументов в with(), включая именование кейсов.
Проверка исключений и ошибок
Задача: метод Account->withdraw() должен выбрасывать InsufficientFundsException, если баланс < суммы списания.
test('выбрасывает исключение при недостатке средств', function () { $account = new Account(balance: 100); $account->withdraw(200); })->throws(InsufficientFundsException::class);
Поддерживается throws(Class::class) и throws(fn(Exception $e) => $e->getCode() === 403).
Тест с зависимостями через Laravel DI
Задача: проверить, что TimeService возвращает текущий объект Carbon.
test('TimeService возвращает Carbon', function (TimeService $service) { $now = $service->now(); expect($now)->toBeInstanceOf(Carbon::class); });
Если TimeService зарегистрирован в Laravel-контейнере — он будет внедрён в тест как аргумент.
Хуки и шаринг состояния между тестами
Задача: создать пост один раз и использовать в нескольких тестах одного файла.
beforeEach(function () { $this->post = Post::factory()->create([ 'title' => 'Hello World', ]); }); test('пост существует', function () { expect($this->post)->not()->toBeNull(); }); test('заголовок корректный', function () { expect($this->post->title)->toBe('Hello World'); });
Упрощает работу с общим состоянием и избавляет от дублирования фабрик.
Тестирование кастомного matcher’а
Задача: проверить, что число чётное, используя кастомный DSL.
expect()->extend('toBeEven', function () { return $this->value % 2 === 0; }); test('42 — чётное', function () { expect(42)->toBeEven(); });
Переиспользуемость и читаемость улучшаются на уровне DSL.
Тестирование взаимодействия через mock-объект
Задача: убедиться, что Mailer->send() вызывается с нужными аргументами.
test('отправка уведомления', function () { $mailer = Mockery::mock(Mailer::class); $mailer->shouldReceive('send') ->once() ->with('user@example.com', Mockery::type(Message::class)); $notifier = new Notifier($mailer); $notifier->notify('user@example.com'); });
Pest совместим с Mockery и любыми сторонними библиотеками.
UI и Browser тесты
Задача: проверить, что кнопка на главной странице присутствует.
test('кнопка "Войти" есть на главной', function () { $this->browse(function ($browser) { $browser->visit('/') ->assertSee('Войти'); }); });
Pest совместим с Laravel Dusk — важно просто подключить соответствующую TestCase.
Заключение
Pest — лаконичный DSL-слой поверх PHPUnit, который убирает шаблонный код, ускоряет написание тестов и повышает читаемость за счёт выразительных матчеров, хуков и параметризации; инструмент подходит как для unit-, так и для интеграционных и e2e-сценариев, легко встраивается в Laravel-экосистему и CI-конвейеры, совместим с Mockery, Dusk и параллельным запуском; если у вас уже есть кейсы или грабли, которые вы разгребали с Pest, делитесь опытом в комментариях.
Если вы уже знакомы с PHPUnit и хотите писать тесты быстрее, чище и выразительнее — возможно, вам стоит обратить внимание не только на Pest, но и на системный рост в профессии PHP‑разработчика.
16 июля пройдёт открытый урок «Что нужно знать, чтобы стать тимлидом на PHP» в рамках курса PHP Developer. Professional. Разберём, как выстраивать архитектуру, управлять командой, автоматизировать процессы и оставаться в курсе современных инструментов разработки — от тестов до деплоя.
А если хотите понять, насколько курс подходит именно вам, начните с небольшого входного теста.
ссылка на оригинал статьи https://habr.com/ru/articles/923170/
Добавить комментарий