Symfony2 Testing Patterns: Forms

This article has been transformed into a Symfony Cookbook article. Please, refer to it as it is now maintained by the community and will surely be more up to date. The post below is kept for archiving purposes.

It has already been some time since I started working according to the TDD methodology.
Sometimes it is really challenging, because you need to find out how to test certain
framework components you’re extending. One of these components is the form component.
Finding how to test forms properly took me a while, so I would like to share what I learned.

Before starting to code I will explain what and why we test.

The form component itself consists of 3 classes: the FormType, the Form and the FormView.
The only class that is usually handled by programmers is the Type class which serves
as a form blueprint. It is used to generate the Form and the FormView. We
could test it directly, by mocking its interactions with the factory but it would be too
complicated with little gain. What we should do instead is to pass it to FormFactory
like it is done in real application. It is simple to bootstrap and we trust Symfony
components enough to use them as a testing base.

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

The basics

The simplest TypeTestCase implementation looks like this:

<?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? I will explain it line by line.

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


This will test if your FormType compiles to a form. This includes basic class inheritance,
the buildForm function and options resolution. This should be the first test you write.

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


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. Note that 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 TestCases to test your validators.

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


This one verifies the binding of the form. It will show you if any of the fields were
wrongly specified.

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

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


This one specifies if your views are created correctly. You should check if all widgets
you want to display are available in the children property.

The tricks

The test case above works for most of the forms, but you can actually have few issues
if you’re doing something a bit more sophisticated.

1. If your form uses a custom type defined as a service

<?php

// FormType buildForm
$builder->add('acme_test_child_type');


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:

<?php
$this->factory->addType(new TestChildType());

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

2. You use some options declared in an extension.

This happens often with ValidatorExtension (invalid_message is one of those options). 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 to
add them manually to the factory object:

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()
            )
            ->getFormFactory();

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

    /** your tests */
}

3. You want to test 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:

class TestedTypeTest extends TypeTestCase
/**
 * @dataProvider getValidTestData
 */
public function testForm($data)
{
    /**
     * Do your tests
     */
}

public function getValidTestData()
{
    return array(
                array(
                    'data' => array(
                        'test' => 'test',
                        'test2' => 'test2',
                    ),
                ),
            array(
                    'data' => array(
                    ),
                ),
                array(
                    'data' => array(
                        'test' => null,
                        'test2' => null,
                    ),
                ),
            );
}


This 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.

Hope these tips will be helpful!