How to Test Code that Interacts with the Database

Configuring a Database for Tests

Tests that interact with the database should use their own separate database to not mess with the databases used in the other configuration environments. To do that, edit or create the .env.test.local file at the root directory of your project and define the new value for the DATABASE_URL env var:

1
2
# .env.test.local
DATABASE_URL=mysql://USERNAME:PASSWORD@127.0.0.1/DB_NAME

Tip

A common practice is to append the _test suffix to the original database names in tests. If the database name in production is called project_acme the name of the testing database could be project_acme_test.

The above assumes that each developer/machine uses a different database for the tests. If the entire team uses the same settings for tests, edit or create the .env.test file instead and commit it to the shared repository. Learn more about using multiple .env files in Symfony applications.

Resetting the Database Automatically Before each Test

Tests should be independent from each other to avoid side effects. For example, if some test modifies the database (by adding or removing an entity) it could change the results of other tests. Run the following command to install a bundle that ensures that each test is run with the same unmodified database:

1
$ composer require --dev dama/doctrine-test-bundle

Now, enable it as a PHPUnit extension or listener:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!-- phpunit.xml.dist -->
<phpunit>
    <!-- ... -->

    <!-- Add this for PHPUnit 7.5 or higher -->
    <extensions>
        <extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
    </extensions>

    <!-- Add this for PHPUnit 7.0 until 7.4 -->
    <listeners>
        <listener class="\DAMA\DoctrineTestBundle\PHPUnit\PHPUnitListener"/>
    </listeners>
</phpunit>

This bundle uses a clever trick to avoid side effects without sacrificing performance: it begins a database transaction before every test and rolls it back automatically after the test finishes to undo all changes. Read more in the documentation of the DAMADoctrineTestBundle.

Dummy Data Fixtures

Instead of using the real data from the production database, it’s common to use fake or dummy data in the test database. This is usually called “fixtures data” and Doctrine provides a library to create and load them. Install it with:

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

Then, use the make:fixtures command to generate an empty fixture class:

1
2
3
4
$ php bin/console make:fixtures

The class name of the fixtures to create (e.g. AppFixtures):
> ProductFixture

Customize the new class to load Product objects into Doctrine:

// src/DataFixtures/ProductFixture.php
namespace App\DataFixtures;

use App\Entity\Product;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class ProductFixture extends Fixture
{
    public function load(ObjectManager $manager)
    {
        $product = new Product();
        $product->setName('Priceless widget');
        $product->setPrice(14.50);
        $product->setDescription('Ok, I guess it *does* have a price');
        $manager->persist($product);

        // add more products

        $manager->flush();
    }
}

Empty the database and reload all the fixture classes with:

1
$ php bin/console doctrine:fixtures:load

For more information, read the DoctrineFixturesBundle documentation.

Mocking a Doctrine Repository in Unit Tests

Unit testing Doctrine repositories is not recommended. Repositories are meant to be tested against a real database connection. However, in case you still need to do this, look at the following example.

Suppose the class you want to test looks like this:

// src/Salary/SalaryCalculator.php
namespace App\Salary;

use App\Entity\Employee;
use Doctrine\Persistence\ObjectManager;

class SalaryCalculator
{
    private $objectManager;

    public function __construct(ObjectManager $objectManager)
    {
        $this->objectManager = $objectManager;
    }

    public function calculateTotalSalary($id)
    {
        $employeeRepository = $this->objectManager
            ->getRepository(Employee::class);
        $employee = $employeeRepository->find($id);

        return $employee->getSalary() + $employee->getBonus();
    }
}

Since the EntityManagerInterface gets injected into the class through the constructor, you can pass a mock object within a test:

// tests/Salary/SalaryCalculatorTest.php
namespace App\Tests\Salary;

use App\Entity\Employee;
use App\Salary\SalaryCalculator;
use Doctrine\Persistence\ObjectManager;
use Doctrine\Persistence\ObjectRepository;
use PHPUnit\Framework\TestCase;

class SalaryCalculatorTest extends TestCase
{
    public function testCalculateTotalSalary()
    {
        $employee = new Employee();
        $employee->setSalary(1000);
        $employee->setBonus(1100);

        // Now, mock the repository so it returns the mock of the employee
        $employeeRepository = $this->createMock(ObjectRepository::class);
        // use getMock() on PHPUnit 5.3 or below
        // $employeeRepository = $this->getMock(ObjectRepository::class);
        $employeeRepository->expects($this->any())
            ->method('find')
            ->willReturn($employee);

        // Last, mock the EntityManager to return the mock of the repository
        // (this is not needed if the class being tested injects the
        // repository it uses instead of the entire object manager)
        $objectManager = $this->createMock(ObjectManager::class);
        // use getMock() on PHPUnit 5.3 or below
        // $objectManager = $this->getMock(ObjectManager::class);
        $objectManager->expects($this->any())
            ->method('getRepository')
            ->willReturn($employeeRepository);

        $salaryCalculator = new SalaryCalculator($objectManager);
        $this->assertEquals(2100, $salaryCalculator->calculateTotalSalary(1));
    }
}

In this example, you are building the mocks from the inside out, first creating the employee which gets returned by the Repository, which itself gets returned by the EntityManager. This way, no real class is involved in testing.

Functional Testing of A Doctrine Repository

In functional tests you’ll make queries to the database using the actual Doctrine repositories, instead of mocking them. To do so, get the entity manager via the service container as follows:

// tests/Repository/ProductRepositoryTest.php
namespace App\Tests\Repository;

use App\Entity\Product;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class ProductRepositoryTest extends KernelTestCase
{
    /**
     * @var \Doctrine\ORM\EntityManager
     */
    private $entityManager;

    protected function setUp(): void
    {
        $kernel = self::bootKernel();

        $this->entityManager = $kernel->getContainer()
            ->get('doctrine')
            ->getManager();
    }

    public function testSearchByName()
    {
        $product = $this->entityManager
            ->getRepository(Product::class)
            ->findOneBy(['name' => 'Priceless widget'])
        ;

        $this->assertSame(14.50, $product->getPrice());
    }

    protected function tearDown(): void
    {
        parent::tearDown();

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