Prerequisites
In this guide, we will test the validation rules for user registration. However, the concept applies to other scenarios. I assumes that:
- You know how to set up a Laravel application. If not, you can refer to the official Laravel documentation
- You are familiar with Pest PHP testing framework. If not, refer to Pest PHP documentation..
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:
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
Do you need help adding test to your Laravel application? Contact me and we can work it out.