Behat: The Easy and Effective Way to Write Acceptance Tests
Tags: php, bdd, symfony, behat
Published: 2023-03-12 20:11:39

Behavior-Driven Development (BDD) is a software development methodology that focuses on defining the desired behavior of an application in terms of concrete examples. It is based on the idea that the behavior of a software system can be described in a language that is accessible to both technical and non-technical stakeholders. This approach helps to ensure that the software system is aligned with the business requirements and expectations.

If you are interested in adopting Behavior-Driven Development in your PHP project, Behat and Gherkin provide a powerful set of tools for implementing it. They allow developers to focus on the behavior of their application rather than the implementation details. In this blog post, I will provide a step-by-step guide on how to install and configure Behat for writing acceptance tests in Symfony application using the Gherkin language. By following the instructions outlined in this post, you will be able to set up Behat and start writing effective and efficient acceptance tests for your Symfony application.

# Behat

Behat is an open-source BDD framework for PHP that allows developers to write and execute tests in a human-readable language called Gherkin. Behat tests can be written in a simple, non-technical language that is easy to understand by stakeholders, such as product owners or business analysts. Behat allows developers to focus on the behavior of the application rather than the implementation details.

# Gherkin

Tests written in Gherkin are structured in a scenario outline format, consisting of a series of steps that describe the behavior of an application. The steps are written in a plain natural language and are organized into Given, When, and Then clauses, that describe the preconditions, actions, and expected outcomes of the test. The scenarios can be further organized into feature files, that group related scenarios together and provide a clear and concise overview of the behavior being tested.

# Installing dependencies

To set up our test environment we have to install a few external dependencies. 

Let's start with Behat:

```
composer require behat/behat --dev
```

---

Another dependency is `MinkExtension` - a Behat extension that provides integration between Behat and Mink (a web-testing framework for PHP). It allows developers to write Behat scenarios that interact with web pages and test the functionalities of web applications. `MinkExtension` provides a set of pre-defined steps that can be used to perform common web interactions, such as clicking links, filling in forms, and validating page content.

```
composer require friends-of-behat/mink-extension --dev
```

---

`BehatChromeExtension` is a Behat extension that provides integration between Behat and Google Chrome browser. It allows developers to execute Behat tests using the Chrome browser as the testing environment. With `BehatChromeExtension`, developers can simulate user interactions with the web application and test its behavior in a real-world scenario. This extension provides a more realistic testing environment and can help to uncover issues that may not be visible in a headless browser environment.

```
composer require dmore/behat-chrome-extension --dev
```

---

`FriendsOfBehat/SymfonyExtension` is the extension that provides integration between Behat and Symfony framework. It allows developers to write Behat scenarios that interact with Symfony applications and test their functionality. The extension provides a set of pre-defined steps that can be used to perform common Symfony-specific interactions, such as interacting with Doctrine, using the service container, and testing routes. The extension also provides support for Behat's dependency injection and configuration management features, making it easier to manage and maintain Behat tests in Symfony projects.

```
composer require friends-of-behat/symfony-extension --dev
```

After installing above extension, below file structure will be created: 

```
|config/
| - services_test.yaml
|features/
| - demo.feature
|tests/
| - Behat/
|   - DemoContext.php
|behat.yml.dist
```

By default, SymfonyExtension attempts to load the `bootstrap.php` file, which does not exist in a Symfony application yet. However, we can use the file that we use for PHPUnit.

To run tests that will click through our application, a browser will be required. In this case, Google Chrome will be used. Below you can see a code snippet from the Dockerfile that installs the browser.

```dockerfile
# Install chromedriver
RUN apt-get -y install gnupg
RUN wget -qO - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/googlechrome-linux-keyring.gpg
RUN echo "deb [arch=amd64 signed-by=/usr/share/keyrings/googlechrome-linux-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list
RUN apt-get -y update && apt-get -y install google-chrome-stable

```

# Behat configuration

Behat is configured in the `behat.yml` file. In my case, the file looks like this:

```yaml
default:
  suites:
    ui_posts:
      contexts:
          - App\Behat\Context\PostsContext  
          - App\Behat\Context\FixturesContext
      filters:
        tags: "@posts"
#      paths: ['%paths.base%/features/posts']
  extensions:
    FriendsOfBehat\SymfonyExtension:
      bootstrap: tests/bootstrap.php
      kernel:
        class: 'App\Kernel'
        environment: test
        debug: false      

    DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension: ~

    Behat\MinkExtension:
      browser_name: chrome
      base_url: http://localhost
      sessions:
        default:
          chrome:
            api_url: "http://localhost:9222"
```

I have configured a test suite to test a simple CRUD blog application. If we have multiple test suites, we can tag them (in this case, `@posts`) to ensure that only tests with that specific tag are run in the context of that suite. Alternatively, we can set paths to the feature directories. Otherwise, all test suites will be run.

In the `extensions` section, we can configure additional add-ons, such as the `SymfonyExtension` or `MinkExtension`. The `ChromeExtension` has default configuration.

# Writing the first test

As I mentioned earlier, we write our test scenarios in Gherkin, a language that's understandable for the business. In case of our simple CRUD, we want to test the functionality of displaying, adding, editing, and deleting posts. To do this, we need to create a `posts.feature` file in the `features` directory.

In BDD, there are no formal requirements on how to write user stories, but there is a widely-used standard. First, we add a Feature section where we name our functionality. Then, we describe the functionality and the business value it provides. We can do this in three lines: "In order to...", "As a...", "I want to...".

```gherkin
@posts
Feature: Managing posts
  In order to manage posts
  As a writer
  I want to add, edit, delete and display blog posts
```

After this section, we can add test scenarios. The first one for me will be the ability to navigate to the add post page from the listing page.

```gherkin
  Scenario: Writer goes to create post page
    Given I am on "/post/"
    When I follow "Create new"
    Then I should be on "/post/new"
```

As we can see, our scenario is written in a natural language that is understandable to humans. But how can we make Behat understand what we want to test? This is where contexts come in. In the configuration file shown earlier, you may notice a reference to the `App\Behat\Context\PostsContext` class. Contexts are used to translate natural language into a language that Behat can understand.

In this test, `PostsContext` extends `MinkContext`, which has implementations of each step in the above scenario, as well as many others, that allows us to reuse basic steps without having to write them from scratch.

```php
use Behat\MinkExtension\Context\MinkContext;

class PostsContext extends MinkContext
{
}
```

In the `MinkContext` class, we can find a method:

```php
/**
 * @Given /^(?:|I )am on "(?P<page>[^"]+)"$/
 * @When /^(?:|I )go to "(?P<page>[^"]+)"$/
 */
public function visit($page)
{
    $this->visitPath($page);
}
```

When we write `Given I am on "/post/"` in the `posts.feature` file in our scenario, Behat will find and call this method based on the annotations contained in the `PostsContext` class.

Let's now write another scenario that adds a post and verifies if it is displayed on the listing.

```gherkin
  Scenario: Writer wants to add a new post and see it on the posts page
    Given I am on "/post/new"
    When I fill in "Title" with "An example post"
    And I fill in "Content" with "Lorem ipsum"
    And I press "Save"
    Then I should be on "/post/"
    And On the posts list I can see post with title "An example post" 1 time
```

In this scenario, we add a new post by filling out the form, save it and check if it appears on the listing page. The last step, which checks how many times the title is found on the page, cannot be found in MinkExtension, and we have to write it ourselves. It may look like this:

```php
/**
 * @Then /^On the posts list I can see post with title "([^"]*)" (\d+) times$/
 * @Then /^On the posts list I can see post with title "([^"]*)" (\d+) time$/
 * @throws ExpectationException
 */
public function onThePostsListICanSeeTitleNTimes(string $title, int $count): void
{
    $element = $this->getSession()->getPage();

    $result = $element->findAll('xpath', "//*[contains(text(), '$title')]");

    $resultCount = \count($result);

    if ($resultCount == $count && str_contains(reset($result)->getText(), $title)) {
        return;
    }

    throw new ExpectationException(sprintf('"%s" was expected to appear %d times, got %d',
        $title,
        $count,
        $resultCount
    ), $this->getSession());
}
```

Sometimes it may be necessary to group several steps into one, for example when accessing our application requires authentication, which requires going to the login page, entering a username and password, and submitting the form. We can do this in the following way:

```php
/**
 * @Given I am logged in as admin
 */
public function iAmLoggedInAsAdmin(): void
{
    $this->visit('/login');
    $this->fillField('email', 'admin@admin');
    $this->fillField('password', 'admin1');
    $this->pressButton('Sign in');
}
```

We can add the above step before executing each test scenario. To do this, add a Background section in the `posts.feature` file before the first scenario.

```gherkin
  Background:
    Given I am logged in as admin
```

The entire `posts.feature` file now looks like this:

```gherkin
@posts
Feature: Managing posts
  In order to manage posts
  As a writer
  I want to add, edit, delete and display blog posts

  Background:
    Given I am logged in as admin

  Scenario: I want to go to create post page
    Given I am on "/post/"
    When I follow "Create new"
    Then I should be on "/post/new"

  Scenario: Writer wants to add a new post and see it on the posts page
    Given I am on "/post/new"
    When I fill in "Title" with "An example post"
    And I fill in "Content" with "Lorem ipsum"
    And I press "Save"
    Then I should be on "/post/"
    And On the posts list I can see post with title "An example post" 1 time
```

The `PostsContext` file looks like this:

```php
use Behat\Mink\Exception\ExpectationException;
use Behat\MinkExtension\Context\MinkContext;

class PostsContext extends MinkContext
{
    /**
     * @Given I am logged in as admin
     */
    public function iAmLoggedInAsAdmin(): void
    {
        $this->visit('/login');
        $this->fillField('email', 'admin@admin');
        $this->fillField('password', 'admin1');
        $this->pressButton('Sign in');
    }

    /**
     * @Then /^On the posts list I can see post with title "([^"]*)" (\d+) times$/
     * @Then /^On the posts list I can see post with title "([^"]*)" (\d+) time$/
     * @throws ExpectationException
     */
    public function onThePostsListICanSeeTitleNTimes(string $title, int $count): void
    {
        $element = $this->getSession()->getPage();

        $result = $element->findAll('xpath', "//*[contains(text(), '$title')]");

        $resultCount = \count($result);

        if ($resultCount == $count && \str_contains(\reset($result)->getText(), $title)) {
            return;
        }

        throw new ExpectationException(\sprintf('"%s" was expected to appear %d times, got %d',
            $title,
            $count,
            $resultCount
        ), $this->getSession());
    }
}
```

# Adding fixtures

Before executing each scenario, we want to log in. However, it may turn out that there are no users in the database.

Furthermore, after each test run, another post with the same title and content would be added to the database. This would cause the test to fail on the second run.

It's good to clean the database and add test data (Fixtures) before running each scenario. I wrote about using fixtures in Symfony in this post: [Using Fixtures In Testing Symfony Application](https://dev.to/jszutkowski/using-fixtures-in-testing-symfony-application-47n2)

To load test data, let's create a `FixturesContext`, which will be responsible for this task. In my case, it looks like this:

```php
namespace App\Behat\Context;

use Behat\Behat\Context\Context;
use Liip\TestFixturesBundle\Services\DatabaseToolCollection;

class FixturesContext implements Context
{
    public function __construct(
        private readonly DatabaseToolCollection $databaseToolCollection,
    ) {}

    /**
     * @BeforeScenario @fixtures
     */
    public function loadFixtures(): void
    {
        $this->databaseToolCollection->get()->loadFixtures([]);
    }
}
```

The above class contains one method which is responsible for loading fixtures. It has the `@BeforeScenario` annotation, which means that it will be executed before each scenario. We can limit loading fixtures only to the scenarios that need them by adding a specific tag (in this case, `@fixtures`). Now, before each scenario with this tag, sample data will be loaded.

The above example is not very flexible and would load the same fixtures every time, even if we don't need all of them. Instead, we can write another step in the FixturesContext, to load the fixtures we need in the particular scenario, without tagging it with `@fixtures` annotation:
```php
    /**
     * @Given the fixture :className is appended
     */
    public function theFixtureIsAppended(string $fixtureClass): void
    {
        $this->databaseToolCollection->get()->loadFixtures([
            $fixtureClass
        ], true);
    }
```

Then we can use it in test scenario:

```gherkin
And the fixture "Fixtures\PostsData" is appended
```

# Running tests

To run the tests, we need to perform two steps. The first one is to start the browser. We can do it by running the following command:

```
google-chrome-stable --disable-gpu --headless --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 --no-sandbox
```

When the browser is running, we can run the tests. We do this by executing the following command (please remember to set `APP_ENV=test` in the `.env` file)

```
vendor/bin/behat
```

In the terminal, we should see the results of our tests. It should look something like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xl909twuhhocj3xa3nv7.png)

# Summary

Behat is a tool that enables writing tests in a way that is understandable both for business and developers.

To start writing simple tests, you need to install a few packages and create test scenarios. Many steps are handled by MinkExtension, which certainly makes it easier for the first approach to BDD.