Symfony2 unit database tests

September 30, 2011Benjamin Grandfond5 min read

Today I will explain how to test your entities in a Symfony2 and Doctrine2 project.

To achieve our work, we will work on a location model which will look somewhat like this:

Location:

  • address: string, required
  • zip code: string, required
  • city: string, required
  • country: string, required

Test Driven Development

In the test driven development (TDD) world, a best practice is to start writing your test case before writing any code. So we will write our test case in the Tests/Entity folder of our bundle:

/**
 * Location class test
 *
 * @author Benjamin Grandfond 
 * @since 2011-07-16
 */
namespace ParisStreetPingPong\Bundle\PsppBundle\Entity;

class LocationTest extends \PHPUnit_Framework_TestCase
{
    protected $location;

    public function setUp()
    {
        parent::setUp();

        $this->location = new Location();
    }

    public function testGetAddress()
    {
        $address = '80 Rue Curial';

        $this->location->setAddress($address);

        $this->assertEquals($address, $this->location->getAddress());
    }
}

Note that the aim of this blog post is not to write a test case that covers 100% of the code, but show how to to write a database test case easily.

Once your test is written, if you run it it should not pass; don't worry, we will write the code to make it work ;) Instead of manually creating a file as you would usually do, you can use PHPUnit! It handles the creation of classes from the test case:

$ phpunit --skeleton-class src/Theodo/Bundle/MyBundle/Tests/Entity/LocationTest.php

This will generate your Location.php class in the same folder as the LocationTest.php file, you only need to move it to the Entity folder of your bundle. The tree of your application should look like:

src/Theodo/Bundle/MyBundle
|-- Entity
|  |-- Location.php
|-- Tests
|  |-- Entity
|  |  |-- LocationTest.php

And your Location.php should already contains some code :

So now, you only need to add properties with Doctrine annotations! I recommended against using the YAML or XML formats to describe your model because, when you will generate your getters and setters, Doctrine will append properties and methods to the existing source, so you will have to copy/paste a lot to clean up the code...

Finally, your class should look like this:

namespace Theodo\Bundle\MyBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="location")
 */
class Location
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     */
    protected $address;

    /**
     * @ORM\Column(type="string", length="7", name="zip_code")
     */
    protected $zipCode;

    /**
     * @ORM\Column(type="string")
     */
    protected $city;

    /**
     * @ORM\Column(type="string")
     */
    protected $country;

    /**
     * Set $address
     *
     * @param string $address
     */
    public function setAddress($address)
    {
        $this->address = $address;
    }
 
    /**
     * Get $address
     *
     * @return String
     */
    public function getAddress()
    {

        return $this->address;
    }
}

Actually, this sample does not prove the real utility of the skeleton class generation with PHPUnit because the class could have been generated with Doctrine generate entities command, but you can use it with a class which does not deal with Doctrine.

If you launch your test now it should pass, but we didn't do anything that needs the database. So I will add a $localization property to the Location class which will contain the full address.

// MyBundle\Entity\Location

/**
 * @ORM\Entity
 */
class Location
{
 ...
/**
 * @ORM\Column(type="text", nullable="true")
 */
protected $localization;

...
}

Now we will complete our Location test, and after we will implement the generateLocalization() which should be called on the prePersist event.

Configuration

The first thing you must do when you run a test that use database insertion with Symfony2 and Doctrine2, is to set up the database connection. To do so, you have configure the doctrine DBAL handling the connection in the config_test.yml file:

imports:
    - { resource: config_dev.yml }

framework:
    test: ~
    session:
        storage_id: session.storage.filesystem

web_profiler:
    toolbar: false
    intercept_redirects: false

swiftmailer:
    disable_delivery: true

doctrine:
    dbal:
        driver:       sqlite
        host:         localhost
        dbname:    db_test
        user:         db_user
        password: db_pwd
        charset:     UTF8
        memory:    true

So, to run our tests, we will use SQLite in memory. While you are free to use something else, it will not be as efficient and easy to setup. Also you won't need to use transactions to revert the data as they were before the test, you can delete anything and recreate it very quickly.

PHPUnit test case

Now that the configuration is done, you will use the kernel of your Symfony2 application which will load this configuration, Doctrine and the full application. We will do this in another class that must be abstract to not being considered as a test case by PHPUnit. It will also allow us to use it anytime we need to test something with databases interactions.

/**
 * TestCase is the base test case for the bundle test suite.
 *
 * @author Benjamin Grandfond
 * @since  2011-07-29
 */

namespace ParisStreetPingPong\PsppBundle\Tests;

require_once dirname(__DIR__).'/../../../../app/AppKernel.php';

use Doctrine\ORM\Tools\SchemaTool;

abstract class TestCase extends \PHPUnit_Framework_TestCase
{
    /**
     * @var Symfony\Component\HttpKernel\AppKernel
     */
    protected $kernel;

    /**
     * @var Doctrine\ORM\EntityManager
     */
    protected $entityManager;

    /**
     * @var Symfony\Component\DependencyInjection\Container
     */
    protected $container;

    public function setUp()
    {
        // Boot the AppKernel in the test environment and with the debug.
        $this->kernel = new \AppKernel('test', true);
        $this->kernel->boot();

        // Store the container and the entity manager in test case properties
        $this->container = $this->kernel->getContainer();
        $this->entityManager = $this->container->get('doctrine')->getEntityManager();

        // Build the schema for sqlite
        $this->generateSchema();

        parent::setUp();
    }

    public function tearDown()
    {
        // Shutdown the kernel.
        $this->kernel->shutdown();

        parent::tearDown();
    }

    protected function generateSchema()
    {
        // Get the metadata of the application to create the schema.
        $metadata = $this->getMetadata();

        if ( ! empty($metadata)) {
            // Create SchemaTool
            $tool = new SchemaTool($this->entityManager);
            $tool->createSchema($metadata);
        } else {
            throw new Doctrine\DBAL\Schema\SchemaException('No Metadata Classes to process.');
        }
    }

    /**
     * Overwrite this method to get specific metadata.
     *
     * @return Array
     */
    protected function getMetadata()
    {
        return $this->entityManager->getMetadataFactory()->getAllMetadata();
    }
}

Complete the test

/**
 * Location class test
 *
 * @author Benjamin Grandfond 
 * @since 2011-07-16
 */
namespace ParisStreetPingPong\Bundle\PsppBundle\Entity;

use ParisStreetPingPong\PsppBundle\Tests\TestCase;

require_once dirname(__DIR__).'/TestCase.php';

class LocationTest extends TestCase
{
    ...
    public function testGenerateLocalization()
    {
        $this->location->setAddress('14 Rue Notre-Dame-des-Victoires');
        $this->location->setZipCode('75002');
        $this->location->setCity('Paris');
        $this->location->setCountry('FR');

        // Save the location 
        $this->entityManager->persist($this->location);
        $this->entityManager->flush();

        $this->assertEquals('14 Rue Notre-Dame-des-Victoires 75002 Paris FR', $this->location->getLocalization());
    }
}

generateLocalization implementation

And now we only need to complete our Location entity and launch again our test that must pass :)

// MyBundle\Entity\Location

/**
 * @ORM\Entity @ORM\HasLifecycleCallbacks
 */
class Location
{
 ...

    /** @ORM\PrePersist */
    public function generateLocalization()
    {
        $localization = $this->getAddress().' ';
        $localization .= $this->getZipCode().' ';
        $localization .= $this->getCity().' ';
        $localization .= $this->getCountry();

        $this->setLocalization($localization);
    }
}
Benjamin Grandfond

Benjamin Grandfond

Web Developer at Theodo