Automatic testing for Symfony applications
Fullstack Developer and Tech Author
Having to implement new web or mobile application features is inevitable and can often be very important. This new code poses a threat to the possibility of building a bug-free application, and can break the already implemented features if care is not taken. One of the ways to build better and more reliable applications is by testing your code using unit and functional tests.
Testing is a recommended best practice because it ensures that new releases meet quality and performance goals. Regardless of the programming language or framework that an application is built with, the test-driven development approach is similar across the board.
In this tutorial, we will create a new Symfony application project to manage customer details. While this won’t be an application with a complete CRUD functionality, we will create a couple of methods, a controller, and then introduce both unit and functional tests to ensure that appropriate responses are returned from each function as expected. To top it all off, we will automate our test by leveraging the use of CircleCI.
Prerequisites
To get the most out of this post, ensure that you have:
- A basic knowledge of building applications with Symfony
- Composer installed globally on your computer
- A GitHub account
- A CircleCI account
Getting started
Start by using Composer to create a new Symfony application using the following command:
composer create-project symfony/website-skeleton new-symfony-app
The preceding command will create a new folder named new-symfony-app
in the root folder where you ran the command. It will also install all of the required dependencies.
Next, move into the newly created project and install a web server to run the application:
// move into project
cd new-symfony-app
// installl web server
composer require symfony/web-server-bundle --dev ^4.4.2
Now that you have a new project set up, proceed to initialize the project with Git locally and link it up with GitHub. But before that, head over to GitHub and create a repository for your project.
Feel free to use the same name for the project on GitHub. Now, initialize Git locally within your project folder with:
git init
This will set up the project as a Git repository. Next, commit your changes locally with:
git add .
git commit -m "Initial commit"
Register the remote repository using the git remote add
command on the terminal within your project folder. This command takes two argument:
- A remote name, for example,
origin
- A remote URL, for example,
https://github.com/<your_username>/<your_repo>.git
In my case:
// add remote origin
git remote add origin https://github.com/yemiwebby/new-symfony-app.git
Next, push the local project to its master branch as created on your GitHub account using the following command:
// push to the repo
git push -u origin master
Now that we have a new Symfony project set up and pushed to a remote repository, in the next section we will create a controller and an entity class for the Customer.
Creating a controller
Controllers are responsible for handling HTTP requests and returning the appropriate responses. To automatically generate a controller for this application, use the maker bundle that comes installed with Symfony by running the following command:
php bin/console make:controller CustomerController
This will create two new files for you:
- A controller located in
src/Controller/CustomerController.php
- A view page in
templates/customer/index.html.twig
Open the CustomerController.php
file and replace the contents with:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class CustomerController extends AbstractController
{
/**
* @Route("/", name="customer", methods={"GET"})
*/
public function index()
{
$customerName = "John Doe";
return $this->render('customer/index.html.twig', [
'name' => $customerName,
]);
}
}
Symfony uses route annotation as the most preferred option when defining the routes for an application. Here, we defined a route /
with the name, customer
. This will instruct Symfony to direct any request to the homepage to this method defined here. So when a user’s request hits this route, the index()
will render a view that will contain a $customerName
hardcoded within the file above.
Next, open the view file located in templates/customer/index.html.twig
and replace the contents with the following:
{% extends 'base.html.twig' %}
{% block title %} Customer Page {% endblock %}
{% block body %}
<div class="example-wrapper">
<h1>Hello, Welcome to the customer page</h1>
<h2>Customer name is : {{ name }}</h2>
</div>
{% endblock %}
We updated the view to dynamically render the name
variable representing the default name of a customer. You can run the application locally using the following command:
php bin/console server:run
Now, navigate to http://localhost:8000
to view the homepage.
This page renders the name of a customer hardcoded within the controller and passes it to the view. It is a similar process of dynamically rendering contents within the view of an application with MVC structure. We will write a functional test for this behaviour in a bit.
Creating an entity
Here, we will create a model that will represent a Customer
object. Run the following command for that purpose:
php bin/console make:entity Customer
You will be prompted to add some fields to the src/Entity/Customer.php
file. You can do this manually, if you prefer. Otherwise, enter firstName
and lastName
as the fields for the Customer
class and Symfony MakerBundle will handle the rest. Check the image below.
For confirmation, here is what the Customer.php
file will look like. Update yours to match what is seen here:
<?php
namespace App\Entity;
use App\Repository\CustomerRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=CustomerRepository::class)
*/
class Customer
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $firstName;
/**
* @ORM\Column(type="string", length=255)
*/
private $lastName;
public function getId(): ?int
{
return $this->id;
}
public function getFirstName(): ?string
{
return $this->firstName;
}
public function setFirstName(string $firstName): self
{
$this->firstName = $firstName;
return $this;
}
public function getLastName(): ?string
{
return $this->lastName;
}
public function setLastName(string $lastName): self
{
$this->lastName = $lastName;
return $this;
}
public function getCustomerFullName(): string
{
return $this->getFirstName() . '' . $this->getLastName();
}
}
The getters
and setters
created were automatically generated by Symfony. We included a new method to return the full name of a customer named getCustomerFullName()
. We will write unit tests for the individual functions defined here later in the tutorial.
Creating tests using PHPUnit
Symfony integrates with PHPUnit, a testing framework for PHP. Before writing any tests, execute PHPUnit by running the following command:
./bin/phpunit
Because we are running the command above for the first time, it will install PHPUnit and all its required dependencies for our application.
Writing unit tests
A unit test by definition is often referred to as a test of a individual component (unit) of code. Writing a unit test for a Symfony application is similar to the standard PHPUnit unit test. Here, we will write tests for the methods created within the Customer Entity file.
To begin, create a new folder name Entity
within the tests
folder. And within the newly created folder, create a new file and call it CustomerTest.php
. Open the file and use the following content for it:
<?php
namespace App\Tests\Entity;
use App\Entity\Customer;
use PHPUnit\Framework\TestCase;
class CustomerTest extends TestCase
{
public function testSettingCustomerFirstName()
{
$customer = new Customer();
$firstName = "John";
$customer->setFirstName($firstName);
$this->assertEquals($firstName, $customer->getFirstName());
}
public function testSettingCustomerLastName()
{
$customer = new Customer();
$lastName = "Doe";
$customer->setLastName($lastName);
$this->assertEquals($lastName, $customer->getLastName());
}
public function testReturnsCustomerFullName()
{
$customer = new Customer();
$customer->setFirstName("John");
$customer->setLastName("Deo");
$fullName = $customer->getFirstName() . '' . $customer->getLastName();
$this->assertSame($fullName, $customer->getCustomerFullName());
}
}
In the file above, we wrote three different tests:
testSettingCustomerFirstName()
: Here we created an instance of theCustomer
class and set a dummy value representing the first name of a customer. We then proceeded to use the assertions method from PHPUnit to confirm the name.testSettingCustomerLastName()
: Similar to the first method defined here, we tested both the getter and setter method created for customer’s last name.testReturnsCustomerFullName()
: Lastly, this test asserts that both thefirstName
and thelastName
of a customer can be retrieved successfully.
Running the tests locally
Earlier we ran our test using ./bin/phpunit
command from the terminal. While this is still relevant and appropriate, we will restructure things a little bit and automate this command by adding it to the scripts
section within the composer.json
file. Open composer.json
and add the test command to the scripts
section:
{
...
"scripts": {
...,
"test": [
"./bin/phpunit"
]
}
}
Henceforth, the test command will be available as composer test
.
Before we add our project to CircleCI, let’s write a very simple functional test as well.
Writing functional tests
With a functional test, you can command a browser to surf through your website, click some links, fill out forms, and assert the things it sees on the page. To start, create another folder within the tests
folder and name it Controller
, it will house the test script for the CustomerController
. Proceed to create a new file within the newly created folder and call it CustomerControllerTest.php
. Open the file and use the following content for it:
<?php
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class CustomerControllerTest extends WebTestCase
{
public function testShowCustomer()
{
$client = static::createClient();
$client->request('GET', '/');
$this->assertResponseStatusCodeSame(200, $client->getResponse()->getStatusCode());
$this->assertStringContainsString('Customer name is : John Doe', $client->getResponse()->getContent());
}
}
In the code snippet above, we validated that the HTTP response was successful as it returned status code 200
. Next, we validated that the page contains the expected content that was displayed earlier in the tutorial.
You can run the test again using:
composer test
Automating the tests with CircleCI
Now that all our tests have proven successful locally, we need to ensure that once we push the updates of our code to the GitHub repository, the tests will run automatically.
To create a configuration file that will be used by CircleCI, navigate into the root of your project, and create a folder named .circleci
. Inside this folder, create a file named config.yml
and use the following content for it:
version: 2
jobs:
build:
docker:
# Specify the version you desire here
- image: circleci/php:7.4-node-browsers
steps:
- checkout
- run: sudo apt update # PHP CircleCI 2.0 Configuration File# PHP CircleCI 2.0 Configuration File sudo apt install zlib1g-dev libsqlite3-dev
- run: sudo docker-php-ext-install zip
# Download and cache dependencies
- restore_cache:
keys:
# "composer.lock" can be used if it is committed to the repo
- v1-dependencies-{{ checksum "composer.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run:
name: "Install Dependencies"
command: composer install -n --prefer-dist
- save_cache:
key: v1-dependencies-{{ checksum "composer.json" }}
paths:
- ./vendor
# run tests with phpunit
- run:
name: "Run tests"
command: composer test
What we have done here is to specify the version of PHP that needs to be installed as a part of the Docker image that will be pulled from CircleCI image registry. Next, we specify that all the dependencies be installed for our project and then finally include a command to run the tests.
You can now commit all the changes made so far and pushing the project to GitHub.
Set up project on CircleCI
Here we will connect our Symfony application to CircleCI. Navigate to CircleCI website and log in with your account. On the CircleCI console, go to the Add Projects page and click Set Up Project.
This will show the page below.
Now, click Start Building and you will see a prompt asking you to add the provided CircleCI configuration to your project on a new branch or set up the configuration manually. Go ahead and select Add Manually.
Finally, click Start Building since we have included the configuration file in our project before pushing to GitHub. This will run a continuous integration (CI) pipeline based on the config file and show you the pipeline status.
Here it is running.
Here it is after a successful run.
There you have it. All the tests created are running properly.
Conclusion
Symfony is considered one of the oldest, most robust PHP frameworks known for its elegant structures and reusable components. Here in this tutorial, we created a new Symfony application and learned how to utilize PHPUnit to write unit and functional tests for some of our defined methods and for the controller. We then proceeded to setup an automated CI pipeline using CircleCI.
The knowledge gained from this post will not make you an expert at testing without a personal driven effort for more knowledge from your end, as we barely scratched the surface here. However, what you have learned in this post will give you the fundamental knowledge of how testing is structured in a typical Symfony application and how to automate it. Feel free to explore the source code here on GitHub and check the official documentation of CircleCI to learn more.