a black and white photo of a njoguamos in a suit and with a serious look on his face

Testing Laravel Validation Rules with Pest PHP

Created 25 Jul 2023 | Updated 30 Jul 2023

LaravelTestingPestPHP

Prerequisites

In this guide, we will test the validation rules for user registration. However, the concept applies to other scenarios. I assumes that:

Why test validation rules?

  • To ensure that your validation rules are working correctly. Testing ensures that your application is not accepting invalid data, which could have security or performance implications.
  • To detect bugs in your validation rules. With tests, you can catch bugs early enough and prevent them from problems in production.
  • To improve the quality of your code. When you test your validation rules, you are forced to think about how they work and how they should be tested.

Testing user registration

Update your PHPUnit

I prefer using the sqlite :memory: database while testing my application for simplicity. Not only does it make tests run faster, but also it does not require an additional database setup. Update the phpunit.xml file as follows.

 <!-- <env name="DB_CONNECTION" value="sqlite"/> -->
 <!-- <env name="DB_DATABASE" value=":memory:"/> -->
 <env name="DB_CONNECTION" value="sqlite"/>
 <env name="DB_DATABASE" value=":memory:"/>

To ensure that your database is automatically migrated during tests, update Pest.php file

uses(
    Tests\TestCase::class,
 // Illuminate\Foundation\Testing\RefreshDatabase::class,
 Illuminate\Foundation\Testing\LazilyRefreshDatabase::class,
)->in('Feature');
 

Registration logic

Suppose you have a registration route,

use App\Http\Controllers\RegistrationController;
 
Route::view('/','welcome')->name('home');
Route::post('/register', RegistrationController::class)->name('register');

The route point to your controller which validate the request, create a new user and redirect to home page:

<?php
 
namespace App\Http\Controllers;
 
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
 
class RegistrationController
{
    public function __invoke(Request $request)
    {
        $validated = $request->validate([
            'name' => ['required', 'string', 'min:4', 'max:80'],
            'email' => ['required', 'email', 'max:100', 'unique:users'],
            'password' => ['required', Password::min(size: 8)->uncompromised(), 'max:64'],
        ]);
 
        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => Hash::make($validated['password']),
        ]);
 
        session()->flash(
            key: 'status',
            value: __(
                key: 'messages.user.created',
                replace: ['name' => $user->name]
            )
        );
 
        return to_route(route: 'home');
    }
}
 

Where the content of lang/en/messages.php files is

<?php
 
declare(strict_types=1);
 
return [
    'user' => [
        'created' => "Welcome :name! You're now a member of our community."
    ]
];

Testing with pest php datasets

Start by creating a new feature test using the command below. A new file should be created under tests/Feature/Feature/RegistrationControllerTest.php

php artisan make:test Feature/RegistrationControllerTest --pest

The easiest way to test multiple validations is to use pest php data sets. The code for RegistrationControllerTest.php file would look like this.

<?php
 
use App\Models\User;
use Illuminate\Support\Str;
 
function getLongName(): string
{
    return Str::repeat(string: 'name', times: rand(min: 30, max: 50));
}
 
function getATakenEmail(): string
{
    $takenEmail = '[email protected]';
    User::factory()->create(['email' => $takenEmail]);
    return $takenEmail;
}
 
dataset(name: 'validation-rules', dataset: [
    'name is required' => ['name', '', fn() => __(key: 'validation.custom.name.required')],
    'name be a string' => ['name', ['array'], fn() => __(key: 'validation.custom.name.string')],
    'name not too short' => ['name', 'ams', fn() => __(key: 'validation.custom.name.min')],
    'name not too long' => ['name', getLongName(), fn() => __(key: 'validation.custom.name.max')],
 
    'email is required' => ['email', '', fn() => __(key: 'validation.custom.email.required')],
    'email be valid' => ['email', 'esthernjerigmail.com', fn() => __(key: 'validation.custom.email.email')],
    'email not too long' => ['email', fn() => getLongName() . '@gmail.com', fn() => __(key: 'validation.custom.email.max')],
    'email be unique' => ['email', fn() => getATakenEmail(), fn() => __(key: 'validation.custom.email.unique')],
 
    'password is required' => ['password', '', fn() => __(key: 'validation.custom.password.required')],
    'password be >=8 chars' => ['password', 'Hf^gsg8', fn() => __(key: 'validation.custom.password.min')],
    'password be uncompromised' => ['password', 'password', 'The given password has appeared in a data leak. Please choose a different password.'],
    'password not too long' => ['password', fn() => getLongName(), fn() => __(key: 'validation.custom.password.max')],
]);
 
 
it(
    description: 'can validate user inputs',
    closure: function (string $field, string|array $value, string $message) {
 
    $data = [
        'name' => fake()->name(),
        'email' => fake()->unique()->email(),
        'password' => fake()->password(minLength: 8),
    ];
 
    $response = $this->post(
        uri: route(name: 'register'),
        data: [...$data, $field => $value]
    );
 
    $response->assertSessionHasErrors(keys: [$field => $message]);
 
    $this->assertGuest();
})->with('validation-rules');

Note: The order of the validation rules matched the order in the RegistrationController.php

I like to customise the validation message by creating lang/en/validation.php file and adding the following content:

<?php
 
declare(strict_types=1);
 
return [
 
    'custom' => [
        'name' => [
            'required' => 'Please enter your name.',
            'string' => 'Your name is missing.',
            'min' => 'Name is too short. Try your first and last name.',
            'max' => 'Name is too long. Please shorten your name and try again.',
        ],
        'email' => [
            'required' => 'Email address is required.',
            'email' => 'Enter a valid email e.g [email protected].',
            'max' => 'Email is too long. Please shorten your email and try again.',
            'unique' => 'Email is already registered. Try another one or reset password.',
        ],
        'password' => [
            'required' => 'Enter a password.',
            'min'      => 'Password should be at least 8 characters. Add a word or two.',
            'max'      => 'Password needs to be less than 128 characters. Please enter a short one.'
        ],
    ],
];

Testing output

Run the test by running

vendor/bin/pest --filter="RegistrationControllerTest"

The output should look like this:

a screenshot of a terminal screen with text descriptions

Code explained

About getLongName() and getATakenEmail()

These are custom helper function i created to simplify the code. They are re-usable and convenient. You can learn more about helper functions from pest documentation

Datasets structure

Consider the following dataset subset

dataset(name: 'validation-rules', dataset: [
    'name not too long' => ['name', fn() => getLongName(), fn() => __(key: 'validation.custom.name.max')],
]);
  • name not too long is an optional human friendly name of the dataset. I use it because to improve readability of the cli output.
  • validation-rules is the name of the dataset.
  • fn() => getLongName() it is recommended to use closure function when you get data that involves computation or database.
  • __(key: 'validation.custom.name.max') a short Laravel function for retrieving translation. This ensure that you test that the correct message is returned to the user.

Customising the validation messages

While it is not mandatory to customise the error messages, as UI/UX designer, I understand the need for clear, concise, and helpful error messages.

Conclusion

Pest datasets are an excellent tool for testing Laravel validation rules. I hope that you have learned a thing or two about testing validation rules. You can get the source code of this guide from my GitHub repository

Reach out

Let’s talk about working together.

Available for new opportunities