Behat для PHP: полное руководство с примерами
1. Что это и зачем нужна
Behat — это мощный PHP-фреймворк для Behaviour-Driven Development (BDD, разработка, управляемая поведением). Он позволяет описывать поведение приложения на понятном, почти естественном языке (Gherkin), а затем автоматически проверять, соответствует ли код этому описанию. Behat выступает в роли моста между нетехническими участниками команды (менеджерами, аналитиками, заказчиками) и разработчиками, предоставляя единый "живой" документ, который одновременно является и спецификацией, и набором тестов.
Зачем нужен Behat? В первую очередь, для улучшения коммуникации и снижения риска неверно понятых требований. Вместо того чтобы писать технические unit-тесты, которые проверяют, как работает функция, вы пишете сценарии, описывающие, что должна делать система с точки зрения пользователя. Например, вместо "testLogin()" вы пишете "Пользователь вводит корректные данные и нажимает 'Войти'". Это делает тесты читаемыми для всех участников процесса. Кроме того, Behat отлично подходит для интеграционного и E2E (end-to-end) тестирования веб-приложений, API и консольных команд.
Behat тесно интегрируется с Mink — библиотекой для эмуляции браузера, что позволяет тестировать веб-интерфейсы. Однако, его можно использовать и для тестирования чистого PHP-кода без веб-контекста. Основная идея BDD, реализованная в Behat: "Сначала пишем сценарий, потом пишем код, который делает сценарий зеленым".
2. Установка
Установка Behat производится через Composer — стандартный менеджер пакетов для PHP. Для этого выполните в терминале в корне вашего проекта следующую команду:
composer require --dev behat/behat
Эта команда установит Behat и все его зависимости в директорию vendor/ и добавит запись в файл composer.json.
Если вы планируете тестировать веб-приложения, вам также понадобится Mink и один из его драйверов (например, Goutte для быстрой работы без JavaScript или Selenium2 для работы с реальным браузером):
composer require --dev behat/mink-extension behat/mink-goutte-driver behat/mink-selenium2-driver
После установки, инициализируйте Behat в вашем проекте:
vendor/bin/behat --init
Эта команда создаст структуру директорий: features/ (для файлов .feature) и features/bootstrap/ (для классов контекста FeatureContext.php).
3. Быстрый старт — минимальный рабочий пример
Создайте файл features/hello.feature:
Feature: Hello World
In order to see how Behat works
As a developer
I need to be able to run a simple test
Scenario: Say hello to the user
Given I have a greeting program
When I run it with name "World"
Then I should see "Hello, World!"
Теперь отредактируйте файл features/bootstrap/FeatureContext.php:
greeting = function($name) {
return "Hello, " . $name . "!";
};
}
/**
* @When I run it with name :name
*/
public function iRunItWithName($name)
{
$this->output = call_user_func($this->greeting, $name);
}
/**
* @Then I should see :expectedOutput
*/
public function iShouldSee($expectedOutput)
{
if ($this->output !== $expectedOutput) {
throw new Exception("Expected '$expectedOutput', but got '$this->output'");
}
}
}
Запустите тест:
vendor/bin/behat
Вы должны увидеть зеленый вывод, означающий, что сценарий прошел успешно.
4. Основные методы/функции/классы
В Behat ключевую роль играют аннотации (атрибуты в PHP 8+) и класс FeatureContext. Рассмотрим 8 основных элементов, которые вы будете использовать постоянно.
4.1. Аннотация @Given
Сигнатура: @Given /^pattern$/
Что делает: Определяет шаг, который устанавливает начальное состояние системы (предусловие).
Пример:
/**
* @Given I am logged in as :username
*/
public function iAmLoggedInAs($username)
{
// Логика для входа пользователя $username
$this->currentUser = $username;
$this->session->login($username);
}
4.2. Аннотация @When
Сигнатура: @When /^pattern$/
Что делает: Определяет шаг, который описывает действие (событие), выполняемое пользователем или системой.
Пример:
/**
* @When I press the :button button
*/
public function iPressTheButton($button)
{
$this->getSession()->getPage()->pressButton($button);
}
4.3. Аннотация @Then
Сигнатура: @Then /^pattern$/
Что делает: Определяет шаг, который проверяет ожидаемый результат (постусловие). Если проверка не проходит, выбрасывается исключение.
Пример:
/**
* @Then I should see :text in the :selector element
*/
public function iShouldSeeInElement($text, $selector)
{
$element = $this->getSession()->getPage()->find('css', $selector);
if (!$element || strpos($element->getText(), $text) === false) {
throw new Exception("Text '$text' not found in element '$selector'");
}
}
4.4. Класс Behat\Behat\Context\Context
Сигнатура: interface Context
Что делает: Базовый интерфейс, который должен реализовать каждый контекст (класс с шагами). Он маркирует класс как контейнер для шагов.
Пример:
use Behat\Behat\Context\Context;
class ApiContext implements Context
{
// ... шаги для тестирования API
}
4.5. Класс Behat\Gherkin\Node\TableNode
Сигнатура: public function getHash(): array
Что делает: Позволяет передавать в шаги табличные данные из Gherkin-сценариев. Метод getHash() преобразует таблицу в массив ассоциативных массивов.
Пример (Gherkin):
Given the following users exist:
| username | email |
| alice | alice@test.com |
| bob | bob@test.com |
Пример (PHP):
/**
* @Given the following users exist:
*/
public function theFollowingUsersExist(TableNode $table)
{
foreach ($table->getHash() as $row) {
$user = new User($row['username'], $row['email']);
$this->userRepository->save($user);
}
}
4.6. Класс Behat\Gherkin\Node\PyStringNode
Сигнатура: public function getRaw(): string
Что делает: Позволяет передавать в шаги многострочные строки (например, JSON, XML, текст). Метод getRaw() возвращает строку как есть.
Пример (Gherkin):
Given I have the following JSON payload:
"""
{
"name": "John",
"age": 30
}
"""
Пример (PHP):
/**
* @Given I have the following JSON payload:
*/
public function iHaveTheFollowingJsonPayload(PyStringNode $string)
{
$this->payload = json_decode($string->getRaw(), true);
}
4.7. Метод assertSession() (из Mink)
Сигнатура: public function assertSession(?string $name = null): Behat\Mink\WebAssert
Что делает: Возвращает объект WebAssert, который предоставляет удобные методы для проверки состояния веб-страницы (наличие текста, элементов, статуса ответа и т.д.). Доступен только при использовании MinkExtension.
Пример:
/**
* @Then the response status code should be :code
*/
public function theResponseStatusCodeShouldBe($code)
{
$this->assertSession()->statusCodeEquals($code);
}
/**
* @Then I should see :text
*/
public function iShouldSee($text)
{
$this->assertSession()->pageTextContains($text);
}
4.8. Метод getSession() (из Mink)
Сигнатура: public function getSession(?string $name = null): Behat\Mink\Session
Что делает: Возвращает текущий объект сессии Mink, который представляет собой "браузер". Через него можно манипулировать страницей: переходить по ссылкам, заполнять формы, нажимать кнопки.
Пример:
/**
* @When I visit :url
*/
public function iVisit($url)
{
$this->getSession()->visit($this->locatePath($url));
}
/**
* @When I fill in :field with :value
*/
public function iFillInWith($field, $value)
{
$this->getSession()->getPage()->fillField($field, $value);
}
5. Реальный практический пример (100-200 строк кода)
Рассмотрим тестирование простого REST API для управления задачами (TODO list). Предположим, у нас есть приложение на Laravel или Symfony.
Файл: features/todo_api.feature
Feature: TODO API
In order to manage my tasks
As an API client
I need to be able to create, read, update and delete tasks
Background:
Given the database is clean
And I am an API client
Scenario: Create a new task
When I send a POST request to "/api/tasks" with body:
"""
{"title": "Buy milk", "completed": false}
"""
Then the response status code should be 201
And the response should contain JSON:
"""
{"title": "Buy milk", "completed": false}
"""
And I should have 1 task in the database
Scenario: List all tasks
Given there are tasks in the database:
| title | completed |
| Task 1 | false |
| Task 2 | true |
When I send a GET request to "/api/tasks"
Then the response status code should be 200
And the response should contain 2 tasks
Scenario: Delete a task
Given there is a task with ID 1 in the database
When I send a DELETE request to "/api/tasks/1"
Then the response status code should be 204
And I should have 0 tasks in the database
Файл: features/bootstrap/ApiContext.php
getSession()->setRequestHeader('Accept', 'application/json');
$this->getSession()->setRequestHeader('Content-Type', 'application/json');
}
/**
* @When I send a POST request to :url with body:
*/
public function iSendAPostRequestToWithBody($url, PyStringNode $body)
{
$this->getSession()->visit($url);
$this->getSession()->getPage()->setContent($body->getRaw());
$this->getSession()->executeScript("document.forms[0].submit();");
// Для реального API лучше использовать:
// $this->response = $this->getSession()->getDriver()->getClient()->getResponse();
}
/**
* @When I send a GET request to :url
*/
public function iSendAGetRequestTo($url)
{
$this->getSession()->visit($url);
}
/**
* @When I send a DELETE request to :url