• Reading time ~ 6 min
  • 30.07.2023

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: Pest CLI output

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

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

ABOUT

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

About author CrazyBoy49z
WORK EXPERIENCE
Contact
Ukraine, Lutsk
+380979856297