Tag: testing

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

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 it 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. We have required name, required email, email must be a valid email, email must be unique, and so on.

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
}

What we are doing in every test is sending 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.

And this is how most people I know test validation – some optimize this further by extracting methods to reduce duplication, but the general idea is to write one test per validation rule.

The thing is, 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.

We pay for confidence with tests

We write tests so we can be confident that our changes don’t break the app. We pay for confidence with tests. The more tests we write, the more confident we are things are working.

But sometimes, we happen to overpay for that confidence. Sometimes we write more tests than we actually 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 a cost of about 6s of waiting time and 160 tests – assuming each request has about 8 validation rules. This is the price we pay for the confidence that our requests are validated. 6s and 160 tests.

But there’s a cheaper way to do it. It doesn’t yield as much confidence as what we are doing now, but it is close enough, and it’s 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; it will let me know if the field is missing. I don’t need to test that.

What I do need to test is that my request is validated with whatever rules I set in place. But this approach comes with some costs.

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

The second thing is, with this approach, you can no longer use inline validation – it only works with form request objects.

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 rules we want it to have.

Before, we had to write 8 tests to ensure our register action is validated; now, we only need 2.

Before, our tests needed 300ms to run; now, they only take 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
}

Now, these tests only take 9ms to run. That’s over 30 times faster than what we had before.

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

That’s how I test validation in Laravel. 2 tests. And they are fast tests.

If you liked this article, consider subscribing to my youtube channel.