Continuous integration for Symfony applications with Behat
Fullstack Developer and Tech Author
Behat is an open-source testing framework that supports Behavior-Driven Development. Focused on requirements communication, it has a reputation for helping engineers build towards great systems, versus building systems and testing their greatness. Symfony remains one of the top PHP frameworks. It is agnostic and allows you to work with any testing framework. In this tutorial, we will set up a continuous integration pipeline for a Symfony application with a functional test powered by Behat. The application will return a list of customers. To keep things simple, we won’t interact with a database. Instead, we will hardcode the customer’s details.
Chronologically, we will:
- Create a new Symfony application
- Install Behat via Composer and initialize it within our application
- Create a GitHub repository
- Create an endpoint with default data and write a test for it
- Run the test locally, then configure CircleCI to automate it
Prerequisites
To successfully achieve the objectives of this tutorial, you will need the following:
- A GitHub account
- A CircleCI account
- Composer installed globally on your computer
- A basic understanding of building applications with Symfony
- Not mandatory but an initial experience with Behat is a plus
Installing a Symfony application
Using Composer, create a new Symfony application by running the following command:
composer create-project symfony/website-skeleton symfony-behat
Once the installation process has been completed, you will have a new application with all its dependencies installed in a folder named symfony-behat
.
Move into the project folder and install the required tools to facilitate the functional testing of the application by running the following commands:
// move into project
cd symfony-behat
// install web server and Behat
composer require behat/behat symfony/web-server-bundle --dev ^4.4.2
The last command above will install:
behat/behat
: The latest version of Behatsymfony/web-server-bundle
: The web server for running a Symfony application locally
Initializing Behat
After installing Behat, the first thing to do is initialize it within our application. This is crucial. It comes with a boilerplate that we can build on and it configures the test suite that will tell Behat where to find and how to test our application. Initialize using the following command:
vendor/bin/behat --init
You will see the following output:
+d features - place your *.feature files here
+d features/bootstrap - place your context classes here
+f features/bootstrap/FeatureContext.php - place your definitions, transformations and hooks here
Behat created a features
directory that will hold the test script for features. It will also create a FeatureContext
class in the features/bootstrap
folder.
Some important concepts in Behat
If you are new to Behat, the following definitions will be quite helpful:
- Feature: A file that represents a unit of functionality that includes the definitions, scenarios, and steps to facilitate testing that particular functionality.
- Scenario: A collection of steps that recreate conditions and user behavior patterns.
- Step: Plain language patterns that set up preconditions, trigger events, or make assertions about your application. Steps are responsible for the real behavior of a site.
- Keyword: A specific set of words used as the beginning of step patterns for readability and to group steps into preconditions, actions, and assertions, e.g.,
Given
,When
,Then
. - Context: A class that provides new ways to interact with your application. Primarily this means providing additional steps.
Creating the feature file for the customer endpoint
One of the expected features of our application is that any user should be able to visit the customer
endpoint as an unauthenticated user, and then be able to view the list of customers without any issue. This is the feature’s story and in this section, we will create a feature file with the details of how we prefer the customer
endpoint to function. Navigate to the features
folder and create a file named customer.feature
within it. Paste the following content into the new file:
Feature: List of customers
In order to retrieve the list of customers
As a user
I must visit the customers page
Scenario: I want a list of customers
Given I am an unauthenticated user
When I request a list of customers from "http://localhost:8000/customer"
Then The results should include a customer with ID "1"
The language used by Behat to describe the expected behavior of sets of features within an application is referred to as Gherkin. It is a business readable domain specific language created specifically for behavior description. From the file above, we described one of the features expected of our application and created a context to describe the business value that the proposed feature will bring to our system. Then we used the Scenario
keyword to define the determinable business situation of the feature. The steps that follow describe what needs to be done for the feature to be actualized.
Creating the feature scenario’s steps definition
Now that we have the feature of our application properly outlined, we need to define the step definitions within the FeatureContext
. If you execute Behat right now with:
vendor/bin/behat
you will see the following output:
Feature: List of customers
In order to retrieve the list of customers
As a user
I must visit the customers page
Scenario: I want a list of customers
Given I am an unauthenticated user
When I request a list of customers from "http://localhost:8000/customer"
Then The results should include a customer with ID "1"
1 scenario (1 undefined)
3 steps (3 undefined)
0m0.02s (9.58Mb)
>> default suite has undefined steps. Please choose the context to generate snippets:
[0] None
[1] FeatureContext
>
The output indicates that Behat recognizes our scenario with the three steps that we defined. However, the FeatureContext
class has some missing methods that represent each of the steps created in the customer.feature
file. Behat provides a route to easily map every scenario step with an actual method called step definitions.
You can either create these methods manually or allow Behat to automatically generate them for you. For this tutorial, we are opting for the latter. To proceed, select option 1
.
--- FeatureContext has missing steps. Define them with these snippets:
/**
* @Given I am an unauthenticated user
*/
public function iAmAnUnauthenticatedUser()
{
throw new PendingException();
}
/**
* @When I request a list of customers from :arg1
*/
public function iRequestAListOfCustomersFrom($arg1)
{
throw new PendingException();
}
/**
* @Then The results should include a customer with ID :arg1
*/
public function theResultsShouldIncludeACustomerWithId($arg1)
{
throw new PendingException();
}
Copy these methods and update the FeatureContext.php
file with them:
<?php
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Behat\Behat\Tester\Exception\PendingException;
use Symfony\Component\HttpClient\HttpClient;
/**
* Defines application features from the specific context.
*/
class FeatureContext implements Context
{
/**
* Initializes context.
*
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
*/
public function __construct()
{
}
/**
* @Given I am an unauthenticated user
*/
public function iAmAnUnauthenticatedUser()
{
throw new PendingException();
}
/**
* @When I request a list of customers from :arg1
*/
public function iRequestAListOfCustomersFrom($arg1)
{
throw new PendingException();
}
/**
* @Then The results should include a customer with ID :arg1
*/
public function theResultsShouldIncludeACustomerWithId($arg1)
{
throw new PendingException();
}
}
These are just the definitions of methods derived from each step in the customer.feature
file. With the methods properly defined, we still need to add the required code to complete our scenario. Replace the contents of features/bootstrap/FeatureContext.php
with the following code:
<?php
use Behat\Behat\Context\Context;
use Symfony\Component\HttpClient\HttpClient;
/**
* Defines application features from the specific context.
*/
class FeatureContext implements Context
{
protected $response;
/**
* Initializes context.
*
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
*/
public function __construct()
{
}
/**
* @Given I am an unauthenticated user
*/
public function iAmAnUnauthenticatedUser()
{
$httpClient = HttpClient::create();
$this->response = $httpClient->request("GET", "http://localhost:8000/customer");
if ($this->response->getStatusCode() != 200) {
throw new Exception("Not able to access");
}
return true;
}
/**
* @When I request a list of customers from :arg1
*/
public function iRequestAListOfCustomersFrom($arg1)
{
$httpClient = HttpClient::create();
$this->response = $httpClient->request("GET", $arg1);
$responseCode = $this->response->getStatusCode();
if ($responseCode != 200) {
throw new Exception("Expected a 200, but received " . $responseCode);
}
return true;
}
/**
* @Then The results should include a customer with ID :arg1
*/
public function theResultsShouldIncludeACustomerWithId($arg1)
{
$customers = json_decode($this->response->getContent());
foreach($customers as $customer) {
if ($customer->id == $arg1) {
return true;
}
}
throw new Exception('Expected to find customer with an ID of ' . $arg1 . ' , but didnt');
}
}
From the scenario created in the customer.feature
file, we begin by creating a method named iAmAnUnauthenticatedUser()
for the first step. This will determine whether the customer
endpoint has been created and whether a user who is not authenticated can access it.
public function iAmAnUnauthenticatedUser()
{
$httpClient = HttpClient::create();
$this->response = $httpClient->request("GET", "http://localhost:8000/customer");
if ($this->response->getStatusCode() != 200) {
throw new Exception("Not able to access");
}
return true;
}
Next, we created a method to assert that we can retrieve a list of customers from the customer
endpoint.
public function iRequestAListOfCustomersFrom($arg1)
{
$httpClient = HttpClient::create();
$this->response = $httpClient->request("GET", $arg1);
$responseCode = $this->response->getStatusCode();
if ($responseCode != 200) {
throw new Exception("Expected a 200, but received " . $responseCode);
}
return true;
}
Lastly, to be sure that the list of customers retrieved contains the expected record, we will write another method to check for an item with a specific id
.
public function theResultsShouldIncludeACustomerWithId($arg1)
{
$customers = json_decode($this->response->getContent());
foreach($customers as $customer) {
if ($customer->id == $arg1) {
return true;
}
}
throw new Exception('Expected to find customer with an ID of ' . $arg1 . ' , but didnt');
}
Running Behat right now will definitely fail. We have not yet created the customer
endpoint to return the appropriate records.
Creating a customer controller
Generate a controller for the customer endpoint by running the following command:
php bin/console make:controller CustomerController
Replace the contents of the src/Controller/CustomerController.php
file with the following code:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class CustomerController extends AbstractController
{
/**
* @Route("/customer", name="customer")
*/
public function index()
{
$customers = [
[
'id' => 1,
'name' => 'Olususi Oluyemi',
'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation',
],
[
'id' => 2,
'name' => 'Camila Terry',
'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation',
],
[
'id' => 3,
'name' => 'Joel Williamson',
'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation',
],
[
'id' => 4,
'name' => 'Deann Payne',
'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation',
],
[
'id' => 5,
'name' => 'Donald Perkins',
'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation',
]
];
$response = new Response();
$response->headers->set('Content-Type', 'application/json');
$response->headers->set('Access-Control-Allow-Origin', '*');
$response->setContent(json_encode($customers));
return $response;
}
}
Here, we defined a route /customer
, created a default list of customers, and returned it in a JSON format.
Running the feature test locally
Our feature test requires us to make a call to a particular endpoint. For that, we need to keep the server running. Run the following command to start the server:
php bin/console server:run
Once you are done, in another tab or window of your terminal, execute Behat with the following command:
vendor/bin/behat
You will see the following output.
Feature: List of customers
In order to retrieve the list of customers
As a user
I must visit the customers page
Scenario: I want a list of customers
Given I am an unauthenticated user
When I request a list of customers from "http://localhost:8000/customer"
Then The results should include a customer with ID "1"
1 scenario (1 passed)
3 steps (3 passed)
0m0.10s (10.01Mb)
Our test now runs as expected. It’s time to create a GitHub repository and push this application’s codebase to it. Follow this guide to learn how to push a project to GitHub.
Adding a CircleCI configuration
We first need to update the .env.test
file, since our pipeline will need it. Replace the contents with the following:
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
APP_ENV=dev
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7
To add a CircleCI configuration, create a .circleci
folder inside the root directory of your application and add a new file named config.yml
within it. Open the newly created file and paste the following code:
version: 2
jobs:
build:
docker:
- image: circleci/php:7.4-node-browsers
steps:
- checkout
- run: sudo apt update
- run: sudo docker-php-ext-install zip
- restore_cache:
keys:
- v1-dependencies-{{ checksum "composer.json" }}
- v1-dependencies-
- run:
name: "Create Environment file"
command: mv .env.test .env
- run:
name: "Install Dependencies"
command: composer install -n --prefer-dist
- save_cache:
key: v1-dependencies-{{ checksum "composer.json" }}
paths:
- ./vendor
- run:
name: Run web server
command: php bin/console server:run
background: true
# run Behat test
- run:
name: "Run Behat test"
command: vendor/bin/behat
In the above config file, we pulled the circleci/php:7.4-node-browsers
Docker image from the CircleCI image registry and installed all the required packages for our test environment. We then proceeded to install all the dependencies for our project.
Included is a command to start a local server and run it in the background.
- run:
name: Run web server
command: php bin/console server:run
background: true
The last portion is a command to execute Behat for our feature test.
# run Behat test
- run:
name: "Run Behat test"
command: vendor/bin/behat
Go ahead and update the repository with the new code. In the next section, we will set up our project on CircleCI.
Connecting a project to CircleCI
Log into your account on CircleCI. From the console, locate the project created on GitHub and click Set Up Project.
You will be redirected to a new page. Click Start Building.
A prompt will appear giving the option to either add the config file to a new branch on the repository or do it manually. Click Add Manually to proceed.
Click Start Building.
This will run successfully without glitches.
Click on the job to view the details of the build.
Conclusion
In this tutorial, we followed the fundamental philosophy of behavior driven development (BDD) for building a Symfony application with Behat and we automated the testing with CircleCI. There is so much more you can do with Behat for your application. The information here is enough to get you started building the right features for your applications and this same approach can also be used for any of your other PHP projects.