Symfony 3.4 PHPUnit testing database data

Hey guys, this post is a follow up from my last phpunit in Symfony2 post. This time I’ve had to install a test environment in a Symfony 3.4 project and I’ve had some weird problems, so I thought of writing this blog post once I solved them all, it may be helpful to someone out there ­čÖé

Step 1 –┬áInstalling a fresh Symfony 3.4 project.

In your symfony project, run the composer require

symfony new my_project 3.4

Now run the phpunit command to run tests.

cd my_project
# if you have the phpunit phar installed globally
phpunit tests/
# if you use the phar file directly
php phpunit.phar tests/

Right off the bat, it’ll give you an error…

PHPUnit 5.7.21 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 128 ms, Memory: 14.00MB

There was 1 error:

1) Tests\AppBundle\Controller\DefaultControllerTest::testIndex
Error: Call to undefined method Symfony\Component\Yaml\Parser::parseFile()

This problem is due to an incompatibility with code dependencies from PHPUnit and Symfony 3.2+ code, specifically with the Yaml component.

Don’t panic though, PHPUnit is working to solve this issue.

More info on the PHPUnit issue here.
More info on Symfony’s explanation here.

In the meantime, we can use the PHPUnit bridge to test our application.

cd my_project
composer require --dev symfony/phpunit-bridge

Once your composer json and lock have updated, it’s time to run the test!

./vendor/bin/simple-phpunit

It works!!

Step 2 –┬áInstalling doctrine’s fixtures bundle.

First run the composer require

composer require --dev doctrine/doctrine-fixtures-bundle

Once your composer json and lock have updated, it’s time to install it in our Kernel.

// app/AppKernel.php

// ...
if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
    // ...
    $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle();
}

Now it’s installed, let’s create our first fixture. Let’s suposse we installed FOSUserBundle to manage our users, so for instance we could create a couple users like so:

// AppBundle/DataFixtures/ORM/LoadUserData.php

namespace AppBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use AppBundle\Entity\User;

class LoadUserData implements FixtureInterface, ContainerAwareInterface
{
    private $data = [
        'ROLE_USER' => [
            'username' => 'user',
            'email' => 'user@slowcode.io',
            'plainPassword' => 'user@slowcode.io'
        ],
        'ROLE_ADMIN' => [
            'username' => 'admin',
            'email' => 'admin@slowcode.io',
            'plainPassword' => 'admin@slowcode.io'
        ],
    ];

    private $container;

    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }

    public function load(ObjectManager $manager)
    {
        $userManager = $this->container->get('fos_user.user_manager');

        foreach ($this->data as $role => $attrs) {
            $user = $userManager->createUser();
            foreach ($attrs as $attr => $val) {
                $function = 'set'. ucwords($attr);
                $user->$function($val);
                $user->setEnabled(true);
            }
            $user->setRoles(['ROLE_USER', $role]);
            $userManager->updateUser($user, true);
        }
    }
}

Now if we run the fixtures like so

php bin/console doctrine:fixtures:load --append

We’ll get another error:

In LoadDataFixturesDoctrineCommand.php line 95:
                                                
  [InvalidArgumentException]                    
  Could not find any fixture services to load. 

To fix this, we’ll need to add this to services.yml

# services.yml
services:
    # ...
    AppBundle\DataFixtures\:
        resource: '../../src/AppBundle/DataFixtures'
        tags: ['doctrine.fixture.orm']

Yes, you guessed it, the magic here relies on the tag. Doctrine will load all services that have that ‘doctrine.fixture.orm’ tag. On the other hand, it’ll also load all the classes that implement the ORMFixtureInterface (for instance, those classes extending the Fixture class). More info on all of this in the DoctrineFixtureBundle doc page.

Step 3 –┬áSetting a separate database for testing.

Add these lines on your config_test.yml file.

# config_test.yml
# ...
# Doctrine Configuration
doctrine:
    dbal:
        driver:   "%test_database_driver%"
        host:     "%test_database_host%"
        port:     "%test_database_port%"
        dbname:   "%test_database_name%"
        user:     "%test_database_user%"
        password: "%test_database_password%"

This will point to the new test database.

Then add the data in parameters.yml (don’t forget parameters.yml.dist for future installs)

# parameters.yml
parameters:
    # ...
    test_database_driver: pdo_mysql
    test_database_host: localhost
    test_database_port: null
    test_database_name: test_db_name
    test_database_user: test_db_user
    test_database_password: test_db_pw

Step 4 – Creating a┬áTestCase that we will extend when ddbb fixtures are needed.

This is useful for reusing code, instead of writing it on every file. It also makes all the fixture tests more consistent.

 //tests/AppBundle/DataFixtures/DataFixtureTestCase.php
namespace Tests\AppBundle\DataFixtures;

use Doctrine\ORM\EntityManager;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Bundle\FrameworkBundle\Client;
use Symfony\Component\DependencyInjection\ContainerInterface;

class DataFixtureTestCase extends WebTestCase
{
    /** @var  Application $application */
    protected static $application;

    /** @var  Client $client */
    protected $client;
    
    /** @var  ContainerInterface $container */
    protected $container;

    /** @var  EntityManager $entityManager */
    protected $entityManager;

    /**
     * {@inheritDoc}
     */
    public function setUp()
    {
        self::runCommand('doctrine:database:drop --force');
        self::runCommand('doctrine:database:create');
        self::runCommand('doctrine:schema:create');
        self::runCommand('doctrine:fixtures:load --append --no-interaction');

        $this->client = static::createClient();
        $this->container = $this->client->getContainer();
        $this->entityManager = $this->container->get('doctrine.orm.entity_manager');

        parent::setUp();
    }

    protected static function runCommand($command)
    {
        $command = sprintf('%s --quiet', $command);

        return self::getApplication()->run(new StringInput($command));
    }

    protected static function getApplication()
    {
        if (null === self::$application) {
            $client = static::createClient();

            self::$application = new Application($client->getKernel());
            self::$application->setAutoExit(false);
        }

        return self::$application;
    }
    
    /**
     * {@inheritDoc}
     */
    protected function tearDown()
    {
        self::runCommand('doctrine:database:drop --force');

        parent::tearDown();

        $this->entityManager->close();
        $this->entityManager = null; // avoid memory leaks
    }
}

This will create the database and install the fixtures on every test that extends this TestCase. This is important as you want a consistent database with the same data every time!

Step 5 – Creating the unit, functional and integration tests

After creating your test fixtures, now you can test your services logic easily by extending the TestCase we just created, like so.

// tests/AppBundle/User/FooTest.php

class FooTest extends DataFixtureTestCase
{
    protected $fooService;

    /**
     * {@inheritDoc}
     */
    public function setUp()
    {
        parent::setUp();
        $this->fooService = $this->container->get('app.service.foo');
    }

    public function test_get_two_users()
    {
        $users = $this->entityManager->getRepository(User::class)->findAll();
        $this->assertEquals(2, count($users));
    }

  
}

Now you just need to run the phpunit command like so

./vendor/bin/simple-phpunit

I’ve created a bash wrapper script called ‘test’ that contains just this line of code so now I just need to run

bash test

Happy coding and testing! ­čÖé


Also published on Medium.

Leave a Reply

Your email address will not be published. Required fields are marked *