44How 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. 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
42128Suppose 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
112200returned by the ``EntityManager ``. This way, no real class is involved in
113201testing.
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:PASSWORD@127.0.0.1/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