Behat для PHP: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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                    

Рекомендаций