Kwadz
Kwadz

Reputation: 2232

How to really unit test Symfony forms?

The following example comes from the official documentation:

use AppBundle\Form\Type\TestedType;
use AppBundle\Model\TestObject;
use Symfony\Component\Form\Test\TypeTestCase;

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

        $form = $this->factory->create(TestedType::class);

        $object = TestObject::fromArray($formData);

        // submit the data to the form directly
        $form->submit($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);
        }
    }
}

However, with a real unit testing approach, the test should only contain an individual class as a SUT, everything else should be Test Doubles like stubs, mock objects...

How should we unit test a Symfony form using Test Doubles like mock objects?

We could assume a simple form class:

class TestedType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('firstname', TextType::class, [
                'label' => 'First name',
                'attr' => [
                    'placeholder' => 'John Doe',
                ],
            ])
    }
}

Upvotes: 4

Views: 2621

Answers (1)

Lex Lustor
Lex Lustor

Reputation: 1615

Your question is rather old but is unanswered and I stumbled upon it while being recently confronted with the same concern.

How should we unit test a Symfony form using Test Doubles like mock objects?

We usually test the result of a single method but in the case of buildForm method, the only test we can write with a simple form type is the interaction with the FormBuilderInterface instance passed as parameter.

// Imports and PhpDoc annotations skipped for brievety
public class TestedTypeTest  extends TestCase
{
    private $systemUnderTest;

    protected function setUp()
    {
        parent::setUp();
        $this->systemUnderTest = new TestedType();
    }

    /**
     * Tests that form is correctly build according to specs
     */
    public function testBuildForm(): void
    {
        $formBuilderMock = $this->createMock(FormBuilderInterface::class);
        $formBuilderMock->expects($this->atLeastOnce())->method('add')->willReturnSelf();

        // Passing the mock as a parameter and an empty array as options as I don't test its use
        $this->systemUnderTest->buildForm($formBuilderMock, []);
    }
}

So it's a pretty basic unit test properly speaking, testing your class in isolation.

Don't make the same mistake I did and don't forget to call willReturnSelf method as add is a chainable method (fluent interface pattern).

You can start from here and refine your test, tying it more to implementation with

// Tests number of calls to add method, in my case, 2
$formBuilderMock->expects($this->exactly(2))->method('add')->willReturnSelf();

or

// Tests number of calls AND parameters successively passed
$formBuilderMock->expects($this->exactly(2))->method('add')->withConsecutive(
    [$this->equalTo('field_1'), $this->equalTo(TextType::class)],
    [$this->equalTo('field_2'), $this->equalTo(TextType::class)]
);

I think you get the point... There, your tests are tied to implementation details, forcing you to change them as soon as you change your code : its your call to change the granularity of your tests, depending on your context.


Sidenotes

As you're not specifying any versions of Symfony and PhpUnit, know that my example use following versions:

  • Symfony 4.1
  • PhpUnit 7.5

Questions read while answering about testing behavior

Do mocks break the "test the interface, not the implementation" mantra?
What's the difference between faking, mocking, and stubbing?

Upvotes: 5

Related Questions