Skip to content

Commit a0be65e

Browse files
committed
Merge branch 'marekkalnik-cookbook/form/unit-testing' into 2.1
2 parents 162cc6b + 3ccb220 commit a0be65e

File tree

3 files changed

+248
-0
lines changed

3 files changed

+248
-0
lines changed

cookbook/form/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Form
1111
create_custom_field_type
1212
create_form_type_extension
1313
use_virtuals_forms
14+
unit_testing

cookbook/form/unit_testing.rst

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
.. index::
2+
single: Form; Form testing
3+
4+
How to Unit Test your Forms
5+
===========================
6+
7+
The Form Component consists of 3 core objects: a form type (implementing
8+
:class:`Symfony\\Component\\Form\\FormTypeInterface`), the
9+
:class:`Symfony\\Component\\Form\\Form` and the
10+
:class:`Symfony\\Component\\Form\\FormView`.
11+
12+
The only class that is usually manipulated by programmers is the form type class
13+
which serves as a form blueprint. It is used to generate the ``Form`` and the
14+
``FormView``. You could test it directly by mocking its interactions with the
15+
factory but it would be complex. It is better to pass it to FormFactory like it
16+
is done in a real application. It is simple to bootstrap and you can trust
17+
the Symfony components enough to use them as a testing base.
18+
19+
There is already a class that you can benefit from for simple FormTypes
20+
testing: :class:`Symfony\\Component\\Form\\Tests\\Extension\\Core\\Type\\TypeTestCase`.
21+
It is used to test the core types and you can use it to test your types too.
22+
23+
.. note::
24+
25+
Depending on the way you installed your Symfony or Symfony Form Component
26+
the tests may not be downloaded. Use the --prefer-source option with
27+
composer if this is the case.
28+
29+
The Basics
30+
----------
31+
32+
The simplest ``TypeTestCase`` implementation looks like the following::
33+
34+
// src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php
35+
namespace Acme\TestBundle\Tests\Form\Type;
36+
37+
use Acme\TestBundle\Form\Type\TestedType;
38+
use Acme\TestBundle\Model\TestObject;
39+
use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase;
40+
41+
class TestedTypeTest extends TypeTestCase
42+
{
43+
public function testBindValidData()
44+
{
45+
$formData = array(
46+
'test' => 'test',
47+
'test2' => 'test2',
48+
);
49+
50+
$type = new TestedType();
51+
$form = $this->factory->create($type);
52+
53+
$object = new TestObject();
54+
$object->fromArray($formData);
55+
56+
$form->bind($formData);
57+
58+
$this->assertTrue($form->isSynchronized());
59+
$this->assertEquals($object, $form->getData());
60+
61+
$view = $form->createView();
62+
$children = $view->children;
63+
64+
foreach (array_keys($formData) as $key) {
65+
$this->assertArrayHasKey($key, $children);
66+
}
67+
}
68+
}
69+
70+
So, what does it test? Let's explain it line by line.
71+
72+
First you verify if the ``FormType`` compiles. This includes basic class
73+
inheritance, the ``buildForm`` function and options resolution. This should
74+
be the first test you write::
75+
76+
$type = new TestedType();
77+
$form = $this->factory->create($type);
78+
79+
This test checks that none of your data transformers used by the form
80+
failed. The :method:`Symfony\\Component\\Form\\FormInterface::isSynchronized``
81+
method is only set to ``false`` if a data transformer throws an exception::
82+
83+
$form->bind($formData);
84+
$this->assertTrue($form->isSynchronized());
85+
86+
.. note::
87+
88+
Don't test the validation: it is applied by a listener that is not
89+
active in the test case and it relies on validation configuration.
90+
Instead, unit test your custom constraints directly.
91+
92+
Next, verify the binding and mapping of the form. The test below
93+
checks if all the fields are correctly specified::
94+
95+
$this->assertEquals($object, $form->getData());
96+
97+
Finally, check the creation of the ``FormView``. You should check if all
98+
widgets you want to display are available in the children property::
99+
100+
$view = $form->createView();
101+
$children = $view->children;
102+
103+
foreach (array_keys($formData) as $key) {
104+
$this->assertArrayHasKey($key, $children);
105+
}
106+
107+
Adding a Type your Form depends on
108+
----------------------------------
109+
110+
Your form may depend on other types that are defined as services. It
111+
might look like this::
112+
113+
// src/Acme/TestBundle/Form/Type/TestedType.php
114+
115+
// ... the buildForm method
116+
$builder->add('acme_test_child_type');
117+
118+
To create your form correctly, you need to make the type available to the
119+
form factory in your test. The easiest way is to register it manually
120+
before creating the parent form::
121+
122+
// src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php
123+
namespace Acme\TestBundle\Tests\Form\Type;
124+
125+
use Acme\TestBundle\Form\Type\TestedType;
126+
use Acme\TestBundle\Model\TestObject;
127+
use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase;
128+
129+
class TestedTypeTest extends TypeTestCase
130+
{
131+
public function testBindValidData()
132+
{
133+
$this->factory->addType(new TestChildType());
134+
135+
$type = new TestedType();
136+
$form = $this->factory->create($type);
137+
138+
// ... your test
139+
}
140+
}
141+
142+
.. caution::
143+
144+
Make sure the child type you add is well tested. Otherwise you may
145+
be getting errors that are not related to the form you are currently
146+
testing but to its children.
147+
148+
Adding custom Extensions
149+
------------------------
150+
151+
It often happens that you use some options that are added by
152+
:doc:`form extensions<cookbook/form/create_form_type_extension>`. One of the
153+
cases may be the ``ValidatorExtension`` with its ``invalid_message`` option.
154+
The ``TypeTestCase`` loads only the core form extension so an "Invalid option"
155+
exception will be raised if you try to use it for testing a class that depends
156+
on other extensions. You need add those extensions to the factory object::
157+
158+
// src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php
159+
namespace Acme\TestBundle\Tests\Form\Type;
160+
161+
use Acme\TestBundle\Form\Type\TestedType;
162+
use Acme\TestBundle\Model\TestObject;
163+
use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase;
164+
165+
class TestedTypeTest extends TypeTestCase
166+
{
167+
protected function setUp()
168+
{
169+
parent::setUp();
170+
171+
$this->factory = Forms::createFormFactoryBuilder()
172+
->addTypeExtension(
173+
new FormTypeValidatorExtension(
174+
$this->getMock('Symfony\Component\Validator\ValidatorInterface')
175+
)
176+
)
177+
->addTypeGuesser(
178+
$this->getMockBuilder(
179+
'Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser'
180+
)
181+
->disableOriginalConstructor()
182+
->getMock()
183+
)
184+
->getFormFactory();
185+
186+
$this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
187+
$this->builder = new FormBuilder(null, null, $this->dispatcher, $this->factory);
188+
}
189+
190+
// ... your tests
191+
}
192+
193+
Testing against different Sets of Data
194+
--------------------------------------
195+
196+
If you are not familiar yet with PHPUnit's `data providers`_, this might be
197+
a good opportunity to use them::
198+
199+
// src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php
200+
namespace Acme\TestBundle\Tests\Form\Type;
201+
202+
use Acme\TestBundle\Form\Type\TestedType;
203+
use Acme\TestBundle\Model\TestObject;
204+
use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase;
205+
206+
class TestedTypeTest extends TypeTestCase
207+
{
208+
209+
/**
210+
* @dataProvider getValidTestData
211+
*/
212+
public function testForm($data)
213+
{
214+
// ... your test
215+
}
216+
217+
public function getValidTestData()
218+
{
219+
return array(
220+
array(
221+
'data' => array(
222+
'test' => 'test',
223+
'test2' => 'test2',
224+
),
225+
),
226+
array(
227+
'data' => array(),
228+
),
229+
array(
230+
'data' => array(
231+
'test' => null,
232+
'test2' => null,
233+
),
234+
),
235+
);
236+
}
237+
}
238+
239+
The code above will run your test three times with 3 different sets of
240+
data. This allows for decoupling the test fixtures from the tests and
241+
easily testing against multiple sets of data.
242+
243+
You can also pass another argument, such as a boolean if the form has to
244+
be synchronized with the given set of data or not etc.
245+
246+
.. _`data providers`: http://www.phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers

cookbook/map.rst.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
* :doc:`/cookbook/form/create_custom_field_type`
8282
* :doc:`/cookbook/form/create_form_type_extension`
8383
* :doc:`/cookbook/form/use_virtuals_forms`
84+
* :doc:`/cookbook/form/unit_testing`
8485
* (validation) :doc:`/cookbook/validation/custom_constraint`
8586
* (doctrine) :doc:`/cookbook/doctrine/file_uploads`
8687

0 commit comments

Comments
 (0)