Skip to content
This repository

[WIP] Adding a testing/database cookbook #2407

Merged
merged 2 commits into from about 1 year ago

4 participants

Sebastian Ryan Weaver Wouter J Christophe Coevoet
Sebastian

This PR adds a new cookbook for testing with a database involved.

Q A
Doc fix? no
New docs? yes
Applies to 2.0+
Fixed tickets #480

This PR is not ready for merge yet

Wouter J WouterJ commented on the diff
cookbook/testing/database.rst
((28 lines not shown))
  28 +
  29 + public function __construct(ObjectManager $entityManager)
  30 + {
  31 + $this->entityManager = $entityManager;
  32 + }
  33 +
  34 + public function calculateTotalSalary($id)
  35 + {
  36 + $employeeRepository = $this->entityManager->getRepository('AcmeDemoBundle::Employee');
  37 + $employee = $userRepository->find($id);
  38 +
  39 + return $employee->getSalary() + $employee->getBonus();
  40 + }
  41 + }
  42 +
  43 +As the ``ObjectManager`` gets injected into the class through the constructor, it's
2
Wouter J Collaborator
WouterJ added a note

Use some api link here

Christophe Coevoet
stof added a note

@WouterJ API links don't work for Doctrine interfaces

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
cookbook/testing/database.rst
((90 lines not shown))
  90 + ``__call``, you cannot define a mock method on ``find()`` but must instead use
  91 + ``__call``. It's possible to implement the ``find()`` method in your ``Repository``
  92 + and use it when mocking. In this case, make sure to mock your ``Repository``
  93 + class instead of the generic ``EntityRepository``.
  94 +
  95 +Changing database settings for functional tests
  96 +-----------------------------------------------
  97 +
  98 +If you have functional tests, you want them to interact with a real database.
  99 +Most of the time you want to use a dedicated database connection to make sure
  100 +not to overwrite data you entered when developing the application and also
  101 +to be able to clear the database before every test.
  102 +
  103 +To do this, you can specify the database configuration inside your ``app/config/config_test.yml``::
  104 +
  105 + doctrine:
1
Wouter J Collaborator
WouterJ added a note

should be:

[...] inside your ``app/config/config_test.yml``:

.. code-block:: yaml

    doctrine:
        # ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Wouter J WouterJ commented on the diff
cookbook/testing/database.rst
((95 lines not shown))
  95 +Changing database settings for functional tests
  96 +-----------------------------------------------
  97 +
  98 +If you have functional tests, you want them to interact with a real database.
  99 +Most of the time you want to use a dedicated database connection to make sure
  100 +not to overwrite data you entered when developing the application and also
  101 +to be able to clear the database before every test.
  102 +
  103 +To do this, you can specify the database configuration inside your ``app/config/config_test.yml``::
  104 +
  105 + doctrine:
  106 + dbal:
  107 + host: localhost
  108 + dbname: testdb
  109 + user: testdb
  110 + password: testdb
1
Wouter J Collaborator
WouterJ added a note

Please add other formats as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
cookbook/testing/database.rst
((47 lines not shown))
  47 +
  48 + class SalaryCalculatorTest extends \PHPUnit_Framework_TestCase
  49 + {
  50 +
  51 + public function testCalculateTotalSalary()
  52 + {
  53 + // First, mock the object to be used in the test
  54 + $employee = $this->getMock('\Acme\DemoBundle\Entity\Employee');
  55 + $employee->expects($this->once())
  56 + ->method('getSalary')
  57 + ->will($this->returnValue(1000));
  58 + $employee->expects($this->once())
  59 + ->method('getBonus')
  60 + ->will($this->returnValue(1100));
  61 +
  62 + // Now, mock tthe repository so it returns the mock of the employee
1
Christophe Coevoet
stof added a note

typo: the

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
cookbook/testing/database.rst
((56 lines not shown))
  56 + ->method('getSalary')
  57 + ->will($this->returnValue(1000));
  58 + $employee->expects($this->once())
  59 + ->method('getBonus')
  60 + ->will($this->returnValue(1100));
  61 +
  62 + // Now, mock tthe repository so it returns the mock of the employee
  63 + $employeeRepository = $this->getMockBuilder('\Doctrine\ORM\EntityRepository')
  64 + ->disableOriginalConstructor()
  65 + ->getMock();
  66 + $employeeRepository->expects($this->once())
  67 + ->method('__call')
  68 + ->will($this->returnValue($employee));
  69 +
  70 + // Last, mock the EntityManager to return the mock of the repository
  71 + $entityManager = $this->getMockBuilder('\Doctrine\ORM\EntityManager')
1
Christophe Coevoet
stof added a note

As your typehint is the ObjectManager, I would mock the ObjectManager interface only

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
cookbook/testing/database.rst
((52 lines not shown))
  52 + {
  53 + // First, mock the object to be used in the test
  54 + $employee = $this->getMock('\Acme\DemoBundle\Entity\Employee');
  55 + $employee->expects($this->once())
  56 + ->method('getSalary')
  57 + ->will($this->returnValue(1000));
  58 + $employee->expects($this->once())
  59 + ->method('getBonus')
  60 + ->will($this->returnValue(1100));
  61 +
  62 + // Now, mock tthe repository so it returns the mock of the employee
  63 + $employeeRepository = $this->getMockBuilder('\Doctrine\ORM\EntityRepository')
  64 + ->disableOriginalConstructor()
  65 + ->getMock();
  66 + $employeeRepository->expects($this->once())
  67 + ->method('__call')
1
Christophe Coevoet
stof added a note

You test uses find, not a method going through the __call implementation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
cookbook/testing/database.rst
((74 lines not shown))
  74 + $entityManager->expects($this->once())
  75 + ->method('getRepository')
  76 + ->will($this->returnValue($employeeRepository));
  77 +
  78 + $salaryCalculator = new SalaryCalculator($entityManager);
  79 + $this->assertEquals(1100, $salaryCalculator->calculateTotalSalary(1));
  80 + }
  81 + }
  82 +
  83 +We are building our mocks from the inside out, first creating the employee which
  84 +gets returned by the ``Repository`` which itself gets returned by the ``EntityManager``.
  85 +This way, no real class is involved in testing.
  86 +
  87 +.. note::
  88 +
  89 + As a ``Repository`` doesn't have a ``find()`` method but uses the magic method
1
Christophe Coevoet
stof added a note

Wrong. find is not a magic method (it is even part of the common ObjectRepository interface)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Ryan Weaver weaverryan merged commit 53174e0 into from
Ryan Weaver
Collaborator

Hey Sebastian!

I've merged this in now, since it already solves much of #480. I only had minor tweaks at sha: 0506361. This is a very nice entry.

Thanks!

Sebastian Sgoettschkes deleted the branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
1  cookbook/map.rst.inc
@@ -138,6 +138,7 @@
138 138 * :doc:`/cookbook/testing/http_authentication`
139 139 * :doc:`/cookbook/testing/insulating_clients`
140 140 * :doc:`/cookbook/testing/profiling`
  141 + * :doc:`/cookbook/testing/database`
141 142 * :doc:`/cookbook/testing/doctrine`
142 143 * :doc:`/cookbook/testing/bootstrap`
143 144 * (email) :doc:`/cookbook/email/testing`
134 cookbook/testing/database.rst
Source Rendered
... ... @@ -0,0 +1,134 @@
  1 +.. index::
  2 + single: Tests; Database
  3 +
  4 +How to test code which interacts with the database
  5 +==================================================
  6 +
  7 +If your code interacts with the database, e.g. reads data from or stores data into
  8 +it, you need to adjust your tests to take this into account. There are many ways
  9 +how to deal with this. In a unit test, you can create a mock for a ``Repository``
  10 +and use it to return expected objects. In a functional test, you may need to
  11 +prepare a test database with predefined values, so a test always has the same data
  12 +to work with.
  13 +
  14 +Mocking the ``Repository`` in a Unit Test
  15 +-----------------------------------------
  16 +
  17 +If you want to test code which depends on a doctrine ``Repository`` in isolation, you
  18 +need to mock the ``Repository``. Normally you get the ``Repository`` from the ``EntityManager``,
  19 +so you would need to mock those as well. Suppose the class you want to test looks like this::
  20 +
  21 + namespace Acme\DemoBundle\Salary;
  22 +
  23 + use Doctrine\Common\Persistence\ObjectManager;
  24 +
  25 + class SalaryCalculator
  26 + {
  27 + private $entityManager;
  28 +
  29 + public function __construct(ObjectManager $entityManager)
  30 + {
  31 + $this->entityManager = $entityManager;
  32 + }
  33 +
  34 + public function calculateTotalSalary($id)
  35 + {
  36 + $employeeRepository = $this->entityManager->getRepository('AcmeDemoBundle::Employee');
  37 + $employee = $userRepository->find($id);
  38 +
  39 + return $employee->getSalary() + $employee->getBonus();
  40 + }
  41 + }
  42 +
  43 +As the ``ObjectManager`` gets injected into the class through the constructor, it's
  44 +easy to pass a mock object within a test::
  45 +
  46 + use Acme\DemoBundle\Salary\SalaryCalculator;
  47 +
  48 + class SalaryCalculatorTest extends \PHPUnit_Framework_TestCase
  49 + {
  50 +
  51 + public function testCalculateTotalSalary()
  52 + {
  53 + // First, mock the object to be used in the test
  54 + $employee = $this->getMock('\Acme\DemoBundle\Entity\Employee');
  55 + $employee->expects($this->once())
  56 + ->method('getSalary')
  57 + ->will($this->returnValue(1000));
  58 + $employee->expects($this->once())
  59 + ->method('getBonus')
  60 + ->will($this->returnValue(1100));
  61 +
  62 + // Now, mock the repository so it returns the mock of the employee
  63 + $employeeRepository = $this->getMockBuilder('\Doctrine\ORM\EntityRepository')
  64 + ->disableOriginalConstructor()
  65 + ->getMock();
  66 + $employeeRepository->expects($this->once())
  67 + ->method('find')
  68 + ->will($this->returnValue($employee));
  69 +
  70 + // Last, mock the EntityManager to return the mock of the repository
  71 + $entityManager = $this->getMockBuilder('\Doctrine\Common\Persistence\ObjectManager')
  72 + ->disableOriginalConstructor()
  73 + ->getMock();
  74 + $entityManager->expects($this->once())
  75 + ->method('getRepository')
  76 + ->will($this->returnValue($employeeRepository));
  77 +
  78 + $salaryCalculator = new SalaryCalculator($entityManager);
  79 + $this->assertEquals(1100, $salaryCalculator->calculateTotalSalary(1));
  80 + }
  81 + }
  82 +
  83 +We are building our mocks from the inside out, first creating the employee which
  84 +gets returned by the ``Repository`` which itself gets returned by the ``EntityManager``.
  85 +This way, no real class is involved in testing.
  86 +
  87 +Changing database settings for functional tests
  88 +-----------------------------------------------
  89 +
  90 +If you have functional tests, you want them to interact with a real database.
  91 +Most of the time you want to use a dedicated database connection to make sure
  92 +not to overwrite data you entered when developing the application and also
  93 +to be able to clear the database before every test.
  94 +
  95 +To do this, you can specify a database configuration which overwrites the default
  96 +configuration:
  97 +
  98 +.. code-block:: yaml
  99 +
  100 + # app/config/config_test.yml
  101 + doctrine:
  102 + # ...
  103 + dbal:
  104 + host: localhost
  105 + dbname: testdb
  106 + user: testdb
  107 + password: testdb
  108 +
  109 +.. code-block:: xml
  110 +
  111 + <!-- app/config/config_test.xml -->
  112 + <doctrine:config>
  113 + <doctrine:dbal
  114 + host="localhost"
  115 + dbname="testdb"
  116 + user="testdb"
  117 + password="testdb"
  118 + >
  119 + </doctrine:config>
  120 +
  121 +.. code-block:: php
  122 +
  123 + // app/config/config_test.php
  124 + $configuration->loadFromExtension('doctrine', array(
  125 + 'dbal' => array(
  126 + 'host' => 'localhost',
  127 + 'dbname' => 'testdb',
  128 + 'user' => 'testdb',
  129 + 'password' => 'testdb',
  130 + ),
  131 + ));
  132 +
  133 +Make sure that your database runs on localhost and has the defined database and
  134 +user credentials set up.
1  cookbook/testing/index.rst
Source Rendered
@@ -7,5 +7,6 @@ Testing
7 7 http_authentication
8 8 insulating_clients
9 9 profiling
  10 + database
10 11 doctrine
11 12 bootstrap

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.