Chris White Web Developer

Using PHPUnit Data Providers to Test Validation Rules

8 May 2022 ~1 minute read

Testing the validation rules attached to your forms is important as they are both the first line of defence against bad form data entering your application's database, and also directly impact a user's experience on your website. There's nothing more frustrating than getting to the end of a long form and a bad validation rule rejects the entire thing because your phone number has a + sign in it.

The problem with these tests is that, as they're typically written, they are extremely repetitive and hard to refactor. Often a developer will write out a series of tests such as:

1public function it_requires_an_email_address()
3 //
6public function email_address_must_be_valid()
8 //
11public function email_address_cannot_be_greater_than_255_characters()
13 //

While these tests are highly descriptive they quickly lead to test files that are thousands of lines long for large forms, and end up looking like an exercise in copying and pasting rather than a well designed test suite.

What I prefer to do instead is to isolate the various validation rules under test into a PHPUnit data provider, and then have a single test method that can test each rule. The following example is a Laravel Dusk test, but the same principle can be applied to any kind of test that validates form validation.

3class SignupTest extends DuskTestCase
5 /**
6 * @test
7 * @dataProvider validationErrors
8 */
9 public function it_validates_against_bad_input(string $field, $data, string $message)
10 {
11 $this->browse(function (Browser $browser) use ($field, $data, $message) {
12 $browser->visit('/signup')
13 ->type($field, $data)
14 ->press('Sign up')
15 ->assertUrlIs('http://laravel.test/signup')
16 ->assertSee($message);
17 });
18 }
20 public function validationErrors(): array
21 {
22 return [
23 ['name', '', 'The name field is required.'],
24 ['name', str_repeat('x', 256), 'The name must not be greater than 255 characters.'],
25 ['email', '', 'The email field is required.'],
26 ['email', 'abcdefg', 'The email must be a valid email address.'],
27 ['email', '', 'The email has already been taken.'],
28 ['email', str_repeat('x', 246).'', 'The email must not be greater than 255 characters.'],
29 ['password', '', 'The password field is required.'],
30 ['password', 'abc', 'The password must be at least 6 characters.'],
31 ];
32 }

The data provider returns an array where each element is a property to pass into the test method. Let's take one array element to explain:

1['name', str_repeat('x', 256), 'The name must not be greater than 255 characters.'],
  • The first element of this array is name, which identifies the name of the input field to validate.
  • The second element of this array is the value being entered into the input element, in this case a string that's over the maximum allowed length.
  • The third element of this array is the validation error message that we expect to see on the page after submitting the form.

Those three array elements get passed into the it_validates_against_bad_input test method by way of properties called $field, $data and $message.

If we run this single test via PHPUnit you'll see we actually run 8 tests, one for each validation rule:

1PHPUnit 9.5.13 by Sebastian Bergmann and contributors.
3........ 8 / 8 (100%)
5Time: 00:09.589, Memory: 36.00 MB
7OK (8 tests, 16 assertions)

And if one of the tests fails because we messed up a validation rule we get told which data provider entry caused the failure:

1There was 1 failure:
31) Tests\Browser\SignupTest::it_validates_against_bad_input with data set #3 ('email', 'abcdefg', 'The email must be a valid ema...dress.')
Made with Jigsaw and Torchlight. Hosted on Netlify.