cdruc

The only reasonable, cost-effective way to test validation in Laravel applications

Every time I tell someone how I test validation in Laravel their reaction is somewhere in the lines of "wait, what? this is so much better. I wish I knew this existed!".

So, yeah, here's how I test validation in Laravel.

Bellow we have a bunch of tests asserting that different validation rules are in place when registering a new user.

namespace Tests\Feature\Auth;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class RegistrationTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function name_is_required()
    {
        $response = $this->postJson(route('register'), [
            'name' => '',

            'email' => 'druc@pinsmile.com',
            'password' => 'password',
            'password_confirmation' => 'password',
            'device_name' => 'iphone'
        ]);

        $response->assertStatus(422);
        $response->assertJsonValidationErrors('name');
    }

    /** @test */
    public function email_is_required()
    {
        $response = $this->postJson(route('register'), [
            'email' => '',

            'name' => 'Constantin Druc',
            'password' => 'password',
            'password_confirmation' => 'password',
            'device_name' => 'iphone'
        ]);

        $response->assertStatus(422);
        $response->assertJsonValidationErrors('email');
    }

    /** @test */
    public function email_must_be_a_valid_email()
    {
        // code
    }

    /** @test */
    public function email_must_unique()
    {
        // code
    }

    // more similar tests
}

In every test, we send the correct values except for the field we are testing the validation. This way, we can assert that we receive a 422 response and validation errors for that specific field. This is how most people test validation – some optimize it further by extracting methods to reduce duplication, but the general idea is to write one test per validation rule.

That is a painful and slow way of doing things.

Just as production code, test code requires maintenance, and the more tests you have, the slower your test suite becomes.

The slower your test suite becomes, the less often you'll run it, and the less often you'll be refactoring and improving code. Not only that, but you will also end up avoiding writing more tests knowing it will slow you down even more.

So while writing tests is crucial, having too many of them can also become a problem. Ideally, you want to have as few tests as possible that run as fast as possible, while still being confident enough that everything works.

Mo' tests, mo' confidence

We write tests so we can be confident that future changes won't break the app. Confidence costs though, and the currency is having more tests. The more tests we write, the more confident we are things are not breaking.

But sometimes we happen to overpay. Sometimes we write more tests than we need to.

In our example, those tests take about 300ms to run. Let's say we have an app where 20 requests require validation – that will add up to about 6s of waiting time and 160 tests (assuming each request has about 8 validation rules). 6s wait time and 160 tests is a hefty price.

But there's a cheaper way to do it!

It doesn't yield as much confidence as one test per validation rule, but it is close enough, and much, much cheaper.

Laravel has thorough and exhaustive tests for every validation rule. If I set required as a validation rule, I'm confident that it will work; I don't need to test that.

What I do need to test is that my request is validated with the rules I set in place.

To help us do that, we need to install an additional package: jasonmccreary/laravel-test-assertions – this will provide us the necessary assertions to test that our controller action is validated using the correct form request object.

Here's how it looks:

namespace Tests\Feature\Auth;

use App\Http\Controllers\Auth\RegistrationController;
use App\Http\Requests\RegistrationRequest;
use JMac\Testing\Traits\AdditionalAssertions;
use Tests\TestCase;

class RegistrationRequestTest extends TestCase
{
    use AdditionalAssertions;

    /** @test */
    public function registration_uses_the_correct_form_request()
    {
        $this->assertActionUsesFormRequest(
            RegistrationController::class,
            'register',
            RegistrationRequest::class
        );
    }

    /** @test */
    public function registration_request_has_the_correct_rules()
    {
        $this->assertValidationRules([
            'name' => ['required'],
            'email' => ['required', 'email', 'unique:users,email'],
            'password' => ['required', 'min:8', 'confirmed'],
            'device_name' => ['required']
        ], (new RegistrationRequest())->rules());
    }
}

The first test asserts that the RegistrationController@register action uses the RegistrationRequest form request object.

The second test asserts that the RegistrationRequest has the intended rules.

Previously, we had to write 8 tests to ensure our register action is validated. Now, we only need 2.

Previously, our tests needed 300ms to run. Now, they only need 80ms. And we can speed this up even more by replacing Laravel's TestCase with the PHPUnit/Framework/TestCase class. The first one loads the entire Laravel application, and we don't need that to run these two tests.

namespace Tests\Feature\Auth;

use App\Http\Controllers\Auth\RegistrationController;
use App\Http\Requests\RegistrationRequest;
use JMac\Testing\Traits\AdditionalAssertions;
use PHPUnit\Framework\TestCase; // previously: use Tests\TestCase;

class RegistrationRequestTest extends TestCase
{
    use AdditionalAssertions;

    /** @test */
    public function registration_uses_the_correct_form_request()
    {
		// code
    }

	  // second test
}

These tests only take 9ms to run. That's over 30 times faster.

So while we need to install an additional package, and we are limited to only using form request objects, this second approach is much, much faster. On top of that, it only requires 2 tests instead of one for each validation rule.

That's how I test validations in Laravel. 2 tests. And they are fast tests!