Skip to content

Commit 9e1d06b

Browse files
committed
[Doctrine] Put all docs about testing databases in a single article
1 parent de1f9a8 commit 9e1d06b

File tree

5 files changed

+168
-155
lines changed

5 files changed

+168
-155
lines changed

_build/redirection_map

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,3 +456,4 @@
456456
/templating/namespaced_paths /templates
457457
/templating/embedding_controllers /templates
458458
/templating/inheritance /templates
459+
/testing/doctrine /testing/database

configuration.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,8 @@ operating systems.
602602
:doc:`Symfony profiler </profiler>`. In practice this shouldn't be a
603603
problem because the web profiler must **never** be enabled in production.
604604

605+
.. _configuration-multiple-env-files:
606+
605607
Managing Multiple .env Files
606608
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
607609

doctrine.rst

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -799,58 +799,10 @@ relationships.
799799

800800
For info, see :doc:`/doctrine/associations`.
801801

802-
.. _doctrine-fixtures:
802+
Database Testing
803+
----------------
803804

804-
Dummy Data Fixtures
805-
-------------------
806-
807-
Doctrine provides a library that allows you to programmatically load testing
808-
data into your project (i.e. "fixture data"). Install it with:
809-
810-
.. code-block:: terminal
811-
812-
$ composer require --dev doctrine/doctrine-fixtures-bundle
813-
814-
Then, use the ``make:fixtures`` command to generate an empty fixture class:
815-
816-
.. code-block:: terminal
817-
818-
$ php bin/console make:fixtures
819-
820-
The class name of the fixtures to create (e.g. AppFixtures):
821-
> ProductFixture
822-
823-
Customize the new class to load ``Product`` objects into Doctrine::
824-
825-
// src/DataFixtures/ProductFixture.php
826-
namespace App\DataFixtures;
827-
828-
use Doctrine\Bundle\FixturesBundle\Fixture;
829-
use Doctrine\Common\Persistence\ObjectManager;
830-
831-
class ProductFixture extends Fixture
832-
{
833-
public function load(ObjectManager $manager)
834-
{
835-
$product = new Product();
836-
$product->setName('Priceless widget!');
837-
$product->setPrice(14.50);
838-
$product->setDescription('Ok, I guess it *does* have a price');
839-
$manager->persist($product);
840-
841-
// add more products
842-
843-
$manager->flush();
844-
}
845-
}
846-
847-
Empty the database and reload *all* the fixture classes with:
848-
849-
.. code-block:: terminal
850-
851-
$ php bin/console doctrine:fixtures:load
852-
853-
For information, see the "`DoctrineFixturesBundle`_" documentation.
805+
Read the article about :doc:`testing code that interacts with the database </testing/database>`.
854806

855807
Learn more
856808
----------

testing/database.rst

Lines changed: 162 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,127 @@
44
How to Test Code that Interacts with the Database
55
=================================================
66

7-
If your code interacts with the database, e.g. reads data from or stores data
8-
into it, you need to adjust your tests to take this into account. There are
9-
many ways to deal with this. In a unit test, you can create a mock for
10-
a ``Repository`` and use it to return expected objects. In a functional test,
11-
you may need to prepare a test database with predefined values to ensure that
12-
your test always has the same data to work with.
7+
Configuring a Database for Tests
8+
--------------------------------
139

14-
.. note::
10+
Tests that interact with the database should use their own separate database to
11+
not mess with the databases used in the other :ref:`configuration environments <configuration-environments>`.
12+
To do that, edit or create the ``.env.test.local`` file at the root directory of
13+
your project and define the new value for the ``DATABASE_URL`` env var:
1514

16-
If you want to test your queries directly, see :doc:`/testing/doctrine`.
15+
.. code-block:: bash
16+
17+
# .env.test.local
18+
DATABASE_URL=mysql://USERNAME:PASSWORD@127.0.0.1/DB_NAME
1719
1820
.. tip::
1921

20-
A popular technique to improve the performance of tests that interact with
21-
the database is to begin a transaction before every test and roll it back
22-
after the test has finished. This makes it unnecessary to recreate the
23-
database or reload fixtures before every test. A community bundle called
24-
`DoctrineTestBundle`_ provides this feature.
22+
A common practice is to append the ``_test`` suffix to the original database
23+
names in tests. If the database name in production is called ``project_acme``
24+
the name of the testing database could be ``project_acme_test``.
2525

26-
Mocking the ``Repository`` in a Unit Test
27-
-----------------------------------------
26+
The above assumes that each developer/machine uses a different database for the
27+
tests. If the entire team uses the same settings for tests, edit or create the
28+
``.env.test`` file instead and commit it to the shared repository. Learn more
29+
about :ref:`using multiple .env files in Symfony applications <configuration-multiple-env-files>`.
2830

29-
If you want to test code which depends on a Doctrine repository in isolation,
30-
you need to mock the ``Repository``. Normally you inject the ``EntityManager``
31-
into your class and use it to get the repository. This makes things a little
32-
more difficult as you need to mock both the ``EntityManager`` and your repository
33-
class.
31+
Resetting the Database Automatically Before each Test
32+
-----------------------------------------------------
3433

35-
.. tip::
34+
Tests should be independent from each other to avoid side effects. However, if
35+
some test modifies the database (for example by adding or removing an element)
36+
it could impact the other tests. Run the following command to install a bundle
37+
that ensures that each test is run with a new and unmodified copy of the database:
38+
39+
.. code-block:: terminal
40+
41+
$ composer require --dev dama/doctrine-test-bundle
42+
43+
Now, enable it as a PHPUnit extension (PHPUnit 8 or higher) or a listener
44+
(PHPUnit 7 or lower):
45+
46+
.. code-block:: xml
47+
48+
<!-- phpunit.xml.dist -->
49+
<phpunit>
50+
<!-- ... -->
51+
52+
<!-- Add this in PHPUnit 8 or higher -->
53+
<extensions>
54+
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" />
55+
</extensions>
56+
57+
<!-- Add this in PHPUnit 7 or lower -->
58+
<listeners>
59+
<listener class="\DAMA\DoctrineTestBundle\PHPUnit\PHPUnitListener" />
60+
</listeners>
61+
</phpunit>
62+
63+
This bundle uses a clever trick to create a new database for each test without
64+
scarifying performance: it begins a transaction before every test case and rolls
65+
it back automatically after the test finished. Read more in the documentation of
66+
the `DAMADoctrineTestBundle`_.
67+
68+
.. _doctrine-fixtures:
3669

37-
It is possible (and a good idea) to inject your repository directly by
38-
registering your repository as a :doc:`factory service </service_container/factories>`.
39-
This is a little bit more work to setup, but makes testing easier as you
40-
only need to mock the repository.
70+
Dummy Data Fixtures
71+
-------------------
72+
73+
Instead of using the real data from the production database, it's common to use
74+
fake or dummy data in the test database. This is usually called *"fixtures data"*
75+
and Doctrine provides a library to create and load them. Install it with:
76+
77+
.. code-block:: terminal
78+
79+
$ composer require --dev doctrine/doctrine-fixtures-bundle
80+
81+
Then, use the ``make:fixtures`` command to generate an empty fixture class:
82+
83+
.. code-block:: terminal
84+
85+
$ php bin/console make:fixtures
86+
87+
The class name of the fixtures to create (e.g. AppFixtures):
88+
> ProductFixture
89+
90+
Customize the new class to load ``Product`` objects into Doctrine::
91+
92+
// src/DataFixtures/ProductFixture.php
93+
namespace App\DataFixtures;
94+
95+
use Doctrine\Bundle\FixturesBundle\Fixture;
96+
use Doctrine\Common\Persistence\ObjectManager;
97+
98+
class ProductFixture extends Fixture
99+
{
100+
public function load(ObjectManager $manager)
101+
{
102+
$product = new Product();
103+
$product->setName('Priceless widget!');
104+
$product->setPrice(14.50);
105+
$product->setDescription('Ok, I guess it *does* have a price');
106+
$manager->persist($product);
107+
108+
// add more products
109+
110+
$manager->flush();
111+
}
112+
}
113+
114+
Empty the database and reload *all* the fixture classes with:
115+
116+
.. code-block:: terminal
117+
118+
$ php bin/console doctrine:fixtures:load
119+
120+
For more information, read the `DoctrineFixturesBundle documentation`_.
121+
122+
Mocking a Doctrine Repository in Unit Tests
123+
-------------------------------------------
124+
125+
**Unit testing Doctrine repositories is not recommended**. Repositories are
126+
meant to be tested against a real database connection. However, in case you
127+
still need to do this, look at the following example.
41128

42129
Suppose the class you want to test looks like this::
43130

@@ -66,8 +153,8 @@ Suppose the class you want to test looks like this::
66153
}
67154
}
68155

69-
Since the ``EntityManagerInterface`` gets injected into the class through the constructor,
70-
you can pass a mock object within a test::
156+
Since the ``EntityManagerInterface`` gets injected into the class through the
157+
constructor, you can pass a mock object within a test::
71158

72159
// tests/Salary/SalaryCalculatorTest.php
73160
namespace App\Tests\Salary;
@@ -95,6 +182,8 @@ you can pass a mock object within a test::
95182
->willReturn($employee);
96183

97184
// Last, mock the EntityManager to return the mock of the repository
185+
// (this is not needed if the class being tested injects the
186+
// repository it uses instead of the entire object manager)
98187
$objectManager = $this->createMock(ObjectManager::class);
99188
// use getMock() on PHPUnit 5.3 or below
100189
// $objectManager = $this->getMock(ObjectManager::class);
@@ -112,26 +201,55 @@ the employee which gets returned by the ``Repository``, which itself gets
112201
returned by the ``EntityManager``. This way, no real class is involved in
113202
testing.
114203

115-
Changing Database Settings for Functional Tests
116-
-----------------------------------------------
204+
Mocking a Doctrine Repository in Functional Tests
205+
-------------------------------------------------
117206

118-
If you have functional tests, you want them to interact with a real database.
119-
Most of the time you want to use a dedicated database connection to make sure
120-
not to overwrite data you entered when developing the application and also
121-
to be able to clear the database before every test.
207+
In :ref:`functional tests <functional-tests>` you'll make queries to the
208+
database using the actual Doctrine repositories, instead of mocking them. To do
209+
so, get the entity manager via the service container as follows::
122210

123-
To do this, you can override the value of the ``DATABASE_URL`` env var in the
124-
``phpunit.xml.dist`` to use a different database for your tests:
211+
// tests/Repository/ProductRepositoryTest.php
212+
namespace App\Tests\Repository;
125213

126-
.. code-block:: xml
214+
use App\Entity\Product;
215+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
127216

128-
<?xml version="1.0" charset="utf-8" ?>
129-
<phpunit>
130-
<php>
131-
<!-- the value is the Doctrine connection string in DSN format -->
132-
<env name="DATABASE_URL" value="mysql://USERNAME:PASSWORD@127.0.0.1/DB_NAME"/>
133-
</php>
134-
<!-- ... -->
135-
</phpunit>
217+
class ProductRepositoryTest extends KernelTestCase
218+
{
219+
/**
220+
* @var \Doctrine\ORM\EntityManager
221+
*/
222+
private $entityManager;
223+
224+
protected function setUp()
225+
{
226+
$kernel = self::bootKernel();
227+
228+
$this->entityManager = $kernel->getContainer()
229+
->get('doctrine')
230+
->getManager();
231+
}
232+
233+
public function testSearchByCategoryName()
234+
{
235+
$products = $this->entityManager
236+
->getRepository(Product::class)
237+
->searchByCategoryName('foo')
238+
;
239+
240+
$this->assertCount(1, $products);
241+
}
242+
243+
protected function tearDown()
244+
{
245+
parent::tearDown();
246+
247+
// doing this is recommended to avoid memory leaks
248+
$this->entityManager->close();
249+
$this->entityManager = null;
250+
}
251+
}
136252

137253
.. _`DoctrineTestBundle`: https://github.com/dmaicher/doctrine-test-bundle
254+
.. _`DAMADoctrineTestBundle`: https://github.com/dmaicher/doctrine-test-bundle
255+
.. _`DoctrineFixturesBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html

testing/doctrine.rst

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,63 +4,3 @@
44
How to Test Doctrine Repositories
55
=================================
66

7-
Unit testing Doctrine repositories in a Symfony project is not recommended.
8-
When you're dealing with a repository, you're really dealing with something
9-
that's meant to be tested against a real database connection.
10-
11-
Fortunately, you can test your queries against a real database, as described
12-
below.
13-
14-
Functional Testing
15-
------------------
16-
17-
If you need to actually execute a query, you will need to boot the kernel
18-
to get a valid connection. In this case, you'll extend the ``KernelTestCase``
19-
to have the Symfony environment available::
20-
21-
// tests/Repository/ProductRepositoryTest.php
22-
namespace App\Tests\Repository;
23-
24-
use App\Entity\Product;
25-
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
26-
27-
class ProductRepositoryTest extends KernelTestCase
28-
{
29-
/**
30-
* @var \Doctrine\ORM\EntityManager
31-
*/
32-
private $entityManager;
33-
34-
/**
35-
* {@inheritDoc}
36-
*/
37-
protected function setUp()
38-
{
39-
$kernel = self::bootKernel();
40-
41-
$this->entityManager = $kernel->getContainer()
42-
->get('doctrine')
43-
->getManager();
44-
}
45-
46-
public function testSearchByCategoryName()
47-
{
48-
$products = $this->entityManager
49-
->getRepository(Product::class)
50-
->searchByCategoryName('foo')
51-
;
52-
53-
$this->assertCount(1, $products);
54-
}
55-
56-
/**
57-
* {@inheritDoc}
58-
*/
59-
protected function tearDown()
60-
{
61-
parent::tearDown();
62-
63-
$this->entityManager->close();
64-
$this->entityManager = null; // avoid memory leaks
65-
}
66-
}

0 commit comments

Comments
 (0)