Skip to content

Commit 986df88

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

File tree

5 files changed

+168
-165
lines changed

5 files changed

+168
-165
lines changed

_build/redirection_map

+1
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

+2
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

+4-54
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
----------
@@ -870,8 +822,7 @@ Learn more
870822
doctrine/mongodb_session_storage
871823
doctrine/resolve_target_entity
872824
doctrine/reverse_engineering
873-
874-
* `DoctrineFixturesBundle`_
825+
testing/database
875826

876827
.. _`Doctrine`: http://www.doctrine-project.org/
877828
.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt
@@ -880,7 +831,6 @@ Learn more
880831
.. _`Doctrine Query Language`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html
881832
.. _`Reserved SQL keywords documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#quoting-reserved-words
882833
.. _`DoctrineMongoDBBundle docs`: https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html
883-
.. _`DoctrineFixturesBundle`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html
884834
.. _`Transactions and Concurrency`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/transactions-and-concurrency.html
885835
.. _`DoctrineMigrationsBundle`: https://github.com/doctrine/DoctrineMigrationsBundle
886836
.. _`NativeQuery`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/native-sql.html

testing/database.rst

+161-45
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,126 @@
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:[email protected]/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. For example,
35+
if some test modifies the database (by adding or removing an entity) it could
36+
change the results of other tests. Run the following command to install a bundle
37+
that ensures that each test is run with the same unmodified database:
38+
39+
.. code-block:: terminal
40+
41+
$ composer require --dev dama/doctrine-test-bundle
42+
43+
Now, enable it as a PHPUnit extension or listener:
44+
45+
.. code-block:: xml
46+
47+
<!-- phpunit.xml.dist -->
48+
<phpunit>
49+
<!-- ... -->
50+
51+
<!-- Add this in PHPUnit 8 or higher -->
52+
<extensions>
53+
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
54+
</extensions>
55+
56+
<!-- Add this in PHPUnit 7 or lower -->
57+
<listeners>
58+
<listener class="\DAMA\DoctrineTestBundle\PHPUnit\PHPUnitListener"/>
59+
</listeners>
60+
</phpunit>
61+
62+
This bundle uses a clever trick to avoid side effects without scarifying
63+
performance: it begins a database transaction before every test and rolls it
64+
back automatically after the test finishes to undo all changes. Read more in the
65+
documentation of the `DAMADoctrineTestBundle`_.
66+
67+
.. _doctrine-fixtures:
3668

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

42128
Suppose the class you want to test looks like this::
43129

@@ -66,8 +152,8 @@ Suppose the class you want to test looks like this::
66152
}
67153
}
68154

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

72158
// tests/Salary/SalaryCalculatorTest.php
73159
namespace App\Tests\Salary;
@@ -95,6 +181,8 @@ you can pass a mock object within a test::
95181
->willReturn($employee);
96182

97183
// Last, mock the EntityManager to return the mock of the repository
184+
// (this is not needed if the class being tested injects the
185+
// repository it uses instead of the entire object manager)
98186
$objectManager = $this->createMock(ObjectManager::class);
99187
// use getMock() on PHPUnit 5.3 or below
100188
// $objectManager = $this->getMock(ObjectManager::class);
@@ -112,26 +200,54 @@ the employee which gets returned by the ``Repository``, which itself gets
112200
returned by the ``EntityManager``. This way, no real class is involved in
113201
testing.
114202

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

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.
206+
In :ref:`functional tests <functional-tests>` you'll make queries to the
207+
database using the actual Doctrine repositories, instead of mocking them. To do
208+
so, get the entity manager via the service container as follows::
122209

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:
210+
// tests/Repository/ProductRepositoryTest.php
211+
namespace App\Tests\Repository;
125212

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

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:[email protected]/DB_NAME"/>
133-
</php>
134-
<!-- ... -->
135-
</phpunit>
216+
class ProductRepositoryTest extends KernelTestCase
217+
{
218+
/**
219+
* @var \Doctrine\ORM\EntityManager
220+
*/
221+
private $entityManager;
222+
223+
protected function setUp()
224+
{
225+
$kernel = self::bootKernel();
226+
227+
$this->entityManager = $kernel->getContainer()
228+
->get('doctrine')
229+
->getManager();
230+
}
231+
232+
public function testSearchByName()
233+
{
234+
$product = $this->entityManager
235+
->getRepository(Product::class)
236+
->findOneBy(['name' => 'Priceless widget'])
237+
;
238+
239+
$this->assertSame(14.50, $product->getPrice());
240+
}
241+
242+
protected function tearDown()
243+
{
244+
parent::tearDown();
245+
246+
// doing this is recommended to avoid memory leaks
247+
$this->entityManager->close();
248+
$this->entityManager = null;
249+
}
250+
}
136251

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

0 commit comments

Comments
 (0)