Unit testing your symfony forms

Tired of slow test suites? Not enough RAM to satisfy both tests and your browser? Have you ever committed without seeing tests results because “It 18.30!”?

Good, this one is totally for you.

The problem

You all know how symfony1 test suites become a problem as you have lots of tests: when I say lots, I mean 30, 40 tests, which should not be that huge amount of tests.

A possible solution for this problem is not to test forms with functional tests, when you have tons of:

1
2
3
4
5
6
7
->click('Submit', array('form' => array('name' => 'Alessandro') ... ))

...

with('form')->begin()->
  hasError(true)->
  isError(...)

but to use unit tests.

How could this be possible? Well, thanks to the form framework, all forms, widgets and validators are, obviously, objects, so they are easy to test as a unit.

Let’s imagine this scenario: I want to test that a form has one and one widget only, in which the user can type multiple email addresses, separated by comma.

Testing the form

Create the empty form under lib/form directory:

1
2
3
4
5
<?php

class MultipleMailForm extends BaseForm
{
}

Let’s write the test, which checks that the form has 1 widget and 1 validator:

1
2
3
4
5
6
7
8
9
10
11
12
<?php

include(dirname(__FILE__).'/../../bootstrap/unit.php');
$t = new lime_test();
$f = new MultipleMailForm();
$ws = $f->getWidgetSchema();
$vs = $f->getValidatorSchema();

$t->isa_ok($ws['email_addresses'], 'sfWidgetFormInput');
$t->isa_ok($vs['email_addresses'], 'MultipleInlineEmailAddressesValidator');
$t->is(1, count($ws));
$t->is(1, count($vs));

Run the test ( php symfony test:unit MultipleMailForm ): you will se that it’s gonna fail.

Now implement your feature:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

class MultipleMailForm extends BaseForm
{
  public function configure()
  {
    parent::configure();

    $ws = $this->getWidgetSchema();
    $ws['email_addresses'] = new sfWidgetFormInput();

    $vs = $this->getValidatorSchema();
    $vs['email_addresses'] = new MultipleInlineEmailAddressesValidator();
  }
}

Create the validator class under lib/validator:

1
2
3
4
5
<?php

class MultipleInlineEmailAddressesValidator extends sfValidatorEmail
{
}

If you re-launch the test, a green bar will appear :)

As you might notice, now the test isn’t meaningful: we know how many widgets/validators we have but we didn’t tested in which conditions the form is valid, which kind of data it accepts and so on.

To do so, you have 2 things to do: first, bind the form in the test with an array of values and then check the isValid() method; second, test your custom validator.

Testing the validator

Create a test for the validator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php

include(dirname(__FILE__).'/../../bootstrap/unit.php');

$t = new lime_test();
$v = new MultipleInlineEmailAddressesValidator();

try
{
  $t->is($v->clean('[email protected]'), true);
}
catch (Exception $e)
{
  $t->fail('The validator accepts a single email address');
}

try
{
  $t->is($v->clean('[email protected], [email protected]'), true);
}
catch (Exception $e)
{
  $t->fail('The validator accepts multiple email addresses');
}

try
{
  $t->is($v->clean('[email protected],[email protected]'), true);
}
catch (Exception $e)
{
  $t->fail('The validator accepts multiple email addresses, without spaces');
}

try
{
  $t->is($v->clean(' [email protected] ,    [email protected]  '), true);
}
catch (Exception $e)
{
  $t->fail('The validator accepts multiple email addresses, without caring about spaces');
}

try
{
  $t->is($v->clean('alessandro.nadalinmymail.com'), true);
  $t->fail('Exception should be raised');
}
catch (Exception $e)
{
  $t->pass('The validator fails if an address is wrong');
}

try
{
  $t->is($v->clean('[email protected], [email protected]'), true);
  $t->fail('Exception should be raised');
}
catch (Exception $e)
{
  $t->pass('The validator fails if a multiple address is wrong');
}

which checks the validator under lots of circumstances ( single email, multiple emails, multiple emails some good some wrong, etcetera ): launching it, you should get a red bar.

Now you can implement the validator, overriding the doClean() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

protected function doClean($value)
{
  $addresses = explode(',', $value);
  $emails    = array();

  foreach ($addresses as $address)
  {
    $emails[] = parent::doClean(trim($address));
  }

  return $emails;
}

Green bar all the way.


In the mood for some more reading?

...or check the archives.