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:

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.

GitHub create new repository

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.

Application 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.

Creating entity using makerbundle

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 the Customer 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 the firstName and the lastName 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.

Run test unit

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

Run test functional

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 push all updates to the project’s repository on 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.

CircleCI project dashboard

This will show the page below.

CircleCI project configuration page

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.

Start building prompt

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.

Build running

Here it is after a successful run.

Build successful

Build results

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.


Oluyemi is a tech enthusiast, programming freak, and a web development junkie who loves to embrace new technology.