Developing robust web applications with automated testing in Symfony and PHPUnit - Part 1

When developing web applications, it is highly beneficial to have a suite of automated tests in place that can be used to ensure everything is functioning correctly - depending on the quality and coverage of the tests. This can save a huge amount of development time, as it means features of the web application do not need to be manually re-tested by the developer throughout development and before launch. These tests can also be run in the production environment, to ensure everything works correctly before customers can access the website.

One of the most important aspects of testing is that tests are 100% reproducible - they should not rely on any outside factors that could affect the outcome of the test, such as a database.

Building a reproducible test environment with Doctrine fixtures

When running functional tests in Symfony, they may affect the database - such as when testing a user registration process. The test would submit the registration form in the exact same way every time, with the same desired user credentials. This would become a problem if the registration process enforced that a unique username/email should be used. When running the test for a second time, the username/email would already exist in the database - causing registration to fail, even though the code was exactly the same. You may also want to test pages that select and list certain entities correctly, in the right order, etc.

This is where Doctrine fixtures come in - see DoctrineFixturesBundle. Fixtures allow you to populate your database with a known set of data, defined beforehand, clearing any preexisting data. If we were to load fixtures before every test that hits the database, then we could guarantee that the database is in a known state - meaning tests will be completely reproducible every time.

The problem of relying on a database for testing can also be solved by mocking out the database, which we'll look at in a later chapter. Mocking has the advantage of not relying on a database at all, which is arguably better for testing - but having a test database is still a useful option to have. Mocking will also be faster, as the whole database doesn't need to be recreated from scratch before every test is executed. On the other hand, this is less indicative of a real run through the system - as the database methods may behave differently to how they've been mocked.

Lets say we have the following entity, a Developer:

  namespace Codevate\DeveloperBundle\Entity;

  use Doctrine\ORM\Mapping as ORM;

  public class Developer
  {
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /** 
     * @var string
     *
     * @ORM\Column(username="username", type="string", length=256, nullable=false)
     */
    private $username;

    /** 
     * @var string
     *
     * @ORM\Column(bio="bio", type="text", nullable=false)
     */
    private $bio;

    // Getters and setters omitted
  }

A fixture for loading developer entities into the database could look like the following:

  namespace Codevate\DeveloperBundle\DataFixtures\ORM;

  use Codevate\DeveloperBundle\Entity\Developer
  use Doctrine\Common\Persistence\ObjectManager;
  use Doctrine\Common\DataFixtures\FixtureInterface;

  class LoadDeveloperData implements FixtureInterface
  {
    public function load(ObjectManager $manager)
    {
      $androidDeveloper = new Developer();
      $androidDeveloper->setUsername('androidDev');
      $androidDeveloper->setBio('I love Android development!');
      $manager->persist($androidDeveloper);

      $iosDeveloper = new Developer();
      $iosDeveloper->setUsername('iosDev');
      $iosDeveloper->setBio('I develop apps for iPhone, iPad and iPod.');
      $manager->persist($iosDeveloper);

      $manager->flush();
    }
  }

As you can see, fixtures are very simple. They are passed an instance of the Doctrine Object manager, which can be used to persist entities, and save them with a call to flush. In this example, we create two developers - an Android developer and an iOS developer. When this fixture is loaded, all developers will be cleared out of the database and the new developers in the fixture will be inserted.

Unfortunately, Doctrine does not providfe a nice way to load fixtures in tests. They are usually loaded in from the terminal, using the Symfony command php app/console doctrine:fixtures:load. The bundle LiipFunctionalTestBundle provides several useful tools for working with databases in functional tests. The functionality we're interested in is the ability to create a separate test database, and load our fixtures into it. Having a separate database helps to save our data from manual testing to be wiped out whenever tests are executed.

After installing the bundle, adding the following to config_test.yml will set up the bundle to create a test SQLite database for tests. Using an SQLite database instead of the same type as your primary database (e.g. MySQL) helps to keep things snappy for testing. The bundle also has a setting to cache the test database once it's been created, which can be used to speed up future tests.

  doctrine:
    dbal:
      default_connection: default
      connections:
        default:
          driver:   pdo_sqlite
          path:     %kernel.cache_dir%/test.db

  liip_functional_test:
      cache_sqlite_db: true

The bundle provides a replacement base WebTestCase class for your tests in inherit from, instead of the standard Symfony one. This provides the method loadfixtures where we can pass in an array of our fixture classes. As you'll likely want to do this often, it's helpful to create common base class for all of your functional tests to load all fixtures in the setUp method. This ensures that you'll never forget to load fixtures in a test. The following shows an example of this:

  namespace Codevate\DeveloperBundle\Tests;

  use Liip\FunctionalTestBundle\Test\WebTestCase as BaseWebTestCase;

  class WebTestCase extends BaseWebTestCase
  {
    public function setUp()
    {
      $this->loadFixtures(array(
        'Codevate\DeveloperBundle\DataFixtures\ORM\LoadDeveloperData'
      ));
    }
  }

We can now have a reproducible test environment for our functional tests set up. In the next part of this series, we'll be looking at using the Symfony client and crawler components to build functional tests that rely on a database, using fixtures to ensure the database is in a known state beforehand.

Want more on this topic?

comments powered by Disqus

Let’s talk about how our software can work for you.

Get in touch