Skip to content

A cookbook article on how to test forms #2012

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 24, 2013
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions cookbook/form/unit_testing.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
.. index::
single: Form; Form testing

Testing forms
=============

The Form Component consists of 3 core objects: a FormType (implementing
:class:`Symfony\\Component\\Form\\FormTypeInterface`), the
:class:`Symfony\\Component\\Form\\Form` and the
:class:`Symfony\\Component\\Form\\FormView`.

The only class that is usually manipulated by programmers is the FormType class
which serves as a form blueprint. It is used to generate the Form and the
FormView. You could test it directly by mocking its interactions with the
factory but it would be complex. It is better to pass it to FormFactory like it
is done in a real application. It is simple to bootstrap and we trust Symfony
components enough to use them as a testing base.

There is already a class that you can benefit from for simple FormTypes
testing, the
:class:`Symfony\\Component\\Form\\Tests\\Extension\\Core\\Type\\TypeTestCase`.
It is used to test the core types and you can use it to test yours too.

.. note::

Depending on the way you installed your Symfony or Symfony Form Component
the tests may not be downloaded. Use the --prefer-source option with
composer if this is the case.

The Basics
----------

The simplest TypeTestCase implementation looks like the following::

// src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php
namespace Acme\TestBundle\Tests\Form\Type;

use Acme\TestBundle\Form\Type\TestedType;
use Acme\TestBundle\Model\TestObject;
use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase;

class TestedTypeTest extends TypeTestCase
{
public function testBindValidData()
{
$formData = array(
'test' => 'test',
'test2' => 'test2',
);

$type = new TestedType();
$form = $this->factory->create($type);

$object = new TestObject();
$object->fromArray($formData);

$form->bind($formData);

$this->assertTrue($form->isSynchronized());
$this->assertEquals($object, $form->getData());

$view = $form->createView();
$children = $view->children;

foreach (array_keys($formData) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
}

So, what does it test? Let's explain it line by line.

First we verify if the FormType compiles. This includes basic class
inheritance, the buildForm function and options resolution. This should
be the first test you write::

$type = new TestedType();
$form = $this->factory->create($type);


This test checks if none of your DataTransformers used by the form
failed. The isSynchronized is only set to false if a DataTransformer
throws an exception::

$form->bind($formData);
$this->assertTrue($form->isSynchronized());

.. note::

We don't check the validation – it is done by a listener that is not
active in the test case and it relies on validation configuration.
You would need to bootstrap the whole kernel to do it. Write
separate tests to check your validators.

Next we verify the binding and mapping of the form. The test below
checks if all the fields are correctly specified::

$this->assertEquals($object, $form->getData());

At last we check the creation of the FormView. You should check if all
widgets you want to display are available in the children property::

$view = $form->createView();
$children = $view->children;

foreach (array_keys($formData) as $key) {
$this->assertArrayHasKey($key, $children);
}

Adding a Type your form depends on
----------------------------------

Your form may depend on other types that are defined as services. It
would be defined like this::

// src/Acme/TestBundle/Form/Type/TestedType.php

// ... the buildForm method
$builder->add('acme_test_child_type');

To create your form correctly you need to make the type available to the
form factory in your test. The easiest way is to register it manually
before creating the parent form::

// src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php
namespace Acme\TestBundle\Tests\Form\Type;

use Acme\TestBundle\Form\Type\TestedType;
use Acme\TestBundle\Model\TestObject;
use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase;

class TestedTypeTest extends TypeTestCase
{
public function testBindValidData()
{
$this->factory->addType(new TestChildType());

$type = new TestedType();
$form = $this->factory->create($type);

// ... your test
}
}

.. caution::

Make sure the child type you add is well tested. Otherwise you may
be getting errors that are not related to the form you are currently
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should be a way maybe to isolate this

also on the tests we should put comments via the assert arguments as to give an idea of what went wrong

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A way to isolate it is not implementing the test the way I showe but by mocking the interactions with FormFactory. Can not do.

testing but to its children.

Adding custom extensions
------------------------

It often happens that you use some options that are added by form
extensions. One of the cases may be the ValidatorExtension with its
invalid_message option. The TypeTestCase loads only the core Form
Extension so an “Invalid option” exception will be raised if you try to
use it for testing a class that depends on other extensions. You need
add the dependencies to the Factory object::

// src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php
namespace Acme\TestBundle\Tests\Form\Type;

use Acme\TestBundle\Form\Type\TestedType;
use Acme\TestBundle\Model\TestObject;
use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase;

class TestedTypeTest extends TypeTestCase
{
protected function setUp()
{
parent::setUp();

$this->factory = Forms::createFormFactoryBuilder()
->addTypeExtension(
new FormTypeValidatorExtension(
$this->getMock('Symfony\Component\Validator\ValidatorInterface')
)
)
->addTypeGuesser(
$this->getMockBuilder(
'Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser'
)
->disableOriginalConstructor()
->getMock()
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be easier to add the ValidatorExtension here instead of adding the different type extensions and type guessers manually

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof - It's just an example, I'd rather expose the interface than look for simplicity.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for letting marek teach us things for the form newbies

->getFormFactory();

$this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->builder = new FormBuilder(null, null, $this->dispatcher, $this->factory);
}

// ... your tests
}

Testing against different sets of data
--------------------------------------

If you are not familiar yet with PHPUnit's `data providers`_ it would be
a good opportunity to use them::

// src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php
namespace Acme\TestBundle\Tests\Form\Type;

use Acme\TestBundle\Form\Type\TestedType;
use Acme\TestBundle\Model\TestObject;
use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase;

class TestedTypeTest extends TypeTestCase
{

/**
* @dataProvider getValidTestData
*/
public function testForm($data)
{
// ... your test
}

public function getValidTestData()
{
return array(
array(
'data' => array(
'test' => 'test',
'test2' => 'test2',
),
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although this comma will pass, it was lazy-left

array(
'data' => array(),
),
array(
'data' => array(
'test' => null,
'test2' => null,
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove those commas where it is not needed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cordoval those commas are part of the Symfony Coding Standard and should be kept in all the examples.

),
);
}
}

The code above will run your test three times with 3 different sets of
data. This allows for decoupling the test fixtures from the tests and
easily testing against multiple sets of data.

You can also pass another argument, such as a boolean if the form has to
be synchronized with the given set of data or not etc.

.. _`data providers`: http://www.phpunit.de/manual/3.7/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers