Introduction
Laravel Form Requests in my opinion are one of the most powerful components of the framework. They can handle request authorization and validation ahead of controllers, keeping them more clean and concise. They have been an important part of the framework since version 5.0 and dispite a few minor differences between versions, they have not really changed much.
In this post we are going to briefly introduce them, for the few of you who are not familiar with the topic, and then I will try to cover the strategies that you can apply to test them.
Get Started
First of all let’s see how validation can be performed directly in the controller without using Form Requests. Let’s use the usual example of a BlogPostController
that handles the submission of a new blog post. The following is the most basic approach and involves the creation of a new validator instance using the Validator
facade:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
class BlogPostController extends Controller
{
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'title' => 'required|string|max:50',
'body' => 'required|string'
]);
if ($validator->fails()) {
Session::flash('error', $validator->messages()->first());
return redirect()->back()->withInput();
}
// store the blog post ...
}
}
Alternatively, to speed things up you can use the validate
method, provided by the ValidatesRequests
trait, in the base App\Http\Controllers\Controller
class
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class BlogPostController extends Controller
{
public function store(Request $request)
{
$this->validate($request, [
'title' => 'required|string|max:50',
'body' => 'required|string'
]);
// ...
}
}
or use the validate
method provided by the Illuminate\Http\Request
object injected into the controller function
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class BlogPostController extends Controller
{
public function store(Request $request)
{
$validatedData = $request->validate($request, [
'title' => 'required|string|max:50',
'body' => 'required|string'
]);
// ...
}
}
In these latest two examples Laravel will take care to redirect the user back to the previous page and flash the validation errors to the session.
This approach in most of the cases is acceptable, but it’s not the best way to handle validation because it clutters the controllers and delegates them too many responsibilities, making very difficult to mange them as the code evolves.
Notice: the single responsibility principle, one of the five SOLID principles of object-oriented programming, states that “A class should have only one reason to change”.
Create a Form Request
To create a new Form Request you can run this Artisan CLI command
php artisan make:request BlogPostCreateRequest
The generated class will be stored in in the App/Http/Requests
directory. Let’s see how we can adjust the basic skeleton to our needs:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Session;
class BlogPostCreateRequest extends FormRequest
{
// store the length of the title in a constant
// to make it available outside the class
const TITLE_MAX_LENGTH = 50;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'title' => 'required|string|max:' . self::TITLE_MAX_LENGTH,
'body' => 'required|string'
];
}
}
As you can see, a Form Request class comes with two default methods:
authorize()
handles the authorization of the request. Here you can check if the current user is allowed to perform the request or not. You have to return aboolean
. Thecheck()
method of theAuth
facade returnstrue
if a user is currently logged in.rules()
defines all your validation rules as an array. The string and array notations of expressing rules are equivalent in most of the cases.
There are two additional methods that are often used in Form Requests:
messages()
to customize the error messages used by the form requestattributes()
to get custom attribute names for validator errors
Notice:
since Laravel 5.6 you can type-hint any dependencies you need whithin the autorize()
and rules()
methods. They will automatically be resolved via the Laravel service container.
Now we can change our BlogPostController
to use our new shiny BlogPostCreateRequest
. We can type-hint our request class instead of the generic Illuminate\Http\Request
and Laravel will automatically resolve it, performing authorization and validation before even touching our controller function.
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\BlogPostCreateRequest;
class BlogPostController extends Controller
{
public function store(BlogPostCreateRequest $request)
{
// Here we already know that
// the request is valid
$validated = $request->validated();
}
}
If authorization fails (the function returns false
), a HTTP response with a 403 status code will automatically be returned.
If validation fails instead, Laravel will automatically redirect the user back to their previous location. In addition, all of the validation errors will automatically be flashed to the session. In case of an AJAX request Laravel will return all of the validation errors in JSON format, as part of a response with 422 HTTP status code.
Notice: if you want to handle authorization and validation failures by yourself, you can extend the corresponding Form Request methods.
Testing
Now let’s dive deep into testing. Form Requests can be both unit-tested and feature-tested (or integration), but there are pros and cons in both cases.
We are going to talk about feature testing first, because it gives us a high-level overview of the functionality, which later can be elaborated more in details with unit testing.
Feature Testing
In this case we can use Laravel HTTP Tests to cover the entire lifecycle of the request-response cycle.
Authorization
Let’s start by checking that it is not possible to create a new blog post without being authenticated first:
<?php
/** @test */
public function it_should_forbid_an_unauthenticated_user_to_create_a_blog_post()
{
$response = $this->json('POST', '/posts', [
'title' => 'test',
'body' => 'this is a test'
]);
// or $this->postJson in Laravel 6.x
$response->assertForbidden();
// or $response->assertStatus(403);
// for Laravel pre-5.6
}
We can make this test pass by using the actingAs
helper method and a model factory to authenticate a fake user before making the request:
<?php
/** @test */
public function it_should_allow_an_authenticated_user_to_create_a_blog_post()
{
$user = factory(User::class)->create();
$response = $this->actingAs($user)
->json('POST', '/posts', [
'title' => 'test',
'body' => 'this is a test'
]);
$response->assertOk();
}
Validation
Now we can start testing the validation rules:
<?php
/** @test */
public function it_should_fail_validation_when_creating_a_blog_post_without_title()
{
$user = factory(User::class)->create();
$response = $this->actingAs($user)
->json('POST', '/posts', [
'body' => 'this is a test'
]);
$response->assertStatus(422);
$response->assertValidationErrors(['title']);
// or $response->assertJson for Laravel pre-5.5
}
You can already see a potential downside of this approach. We are testing only one (when the title
is not provided in the request) of the many possible failure paths. However to reach complete coverage of our code we should write a test for every validation rule:
- testing
title
required - testing
title
is a string - testing
title
maximum length - testing
body
required - testing
body
is a string
and finally testing that validation passes when all rules are met.
6 tests for just two fields. You can imagine the pain when you have large forms with many fields and many different validation rules. The number of tests required is going to increase exponentially.
Notice: in the end it’s all about tradeoffs. If you have the time and you’re comfortable writing tests this way, good for you. Do not change your habits just because a random guy on the Internet tells you to do differently. As long as you’re aware of the alternatives it’s completely fine.
Let’s conclude this section by looking at the test that checks the successful path:
<?php
/** @test */
public function it_should_create_a_blog_post_successfully()
{
$user = factory(User::class)->create();
$response = $this->actingAs($user)
->json('POST', '/posts', [
'title' => 'test',
'body' => 'this is a test'
]);
$response->assertOk();
$this->assertDatabaseHas('posts', [
'title' => 'test',
'body' => 'this is a test'
]);
}
Remember to use the RefreshDatabase
or DatabaseTransaction
traits in your test classes to keep the database state consistent.
Unit Testing
Unit testing Form Requests can be really hard because you have to know a little bit more of how Laravel works under the hood. Futhermore you have to be careful in your approach because subtle bugs can slip through anyway.
One benefit of these types of tests it that they are usually faster and more flexible, but let’s go with order.
Form Requests are just plain PHP classes so you can create new instances of it wherever you want. For now let’s stick with the new
keyword, but later in the post we will examine a different way for creating a new instance of the request.
Authorization
First of all let’s test authorization. Our class has an external dependency (the Auth
facade) which is not injected into the code. Therefore we have to rely on mocking. Laravel can help us with the handy Facade mocking. This is how it’s done:
<?php
/** @test */
public function it_should_not_authorize_the_request_if_the_user_is_not_logged_in()
{
Auth::shouldReceive('check')
->once()
->andReturn(false);
$request = new BlogPostCreateRequest();
$this->assertFalse($request->authorize());
}
We can test the successful path as well by changing the return value of our mock object
<?php
/** @test */
public function it_should_authorize_the_request_if_the_user_is_logged_in()
{
Auth::shouldReceive('check')
->once()
->andReturn(true);
$request = new BlogPostCreateRequest();
$this->assertTrue($request->authorize());
}
Warning:
remember that since Laravel 5.6 you can type-hint dependencies in the signature of authorize
and rules
methods. Therefore, if you’re using external dependencies, but you still want to use this approach you have to inject them by yourself.
Keep in mind that if in your Form Request you’re retrieving the authenticated user directly via the parent Illuminate\Http\Request
class, those tests are not going to work unless you’re able to inject a user instance programmatically.
<?php
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return ! is_null($this->user());
}
Validation
To test validation, the first approach that comes in mind is checking that all the expected rules are defined in the Form Request:
<?php
/** @test */
public function it_should_contain_all_the_expected_validation_rules()
{
$request = new BlogPostCreateRequest();
$this->assertEquals([
'title' => 'required|string|max:' . BlogPostCreateRequest::TITLE_MAX_LENGTH,
'body' => 'required|string'
], $request->rules());
}
I’m not a big fan of such tests because it’s just a duplication of code.
The second approach involves creating manually a Validator instance, to test your input against your validation rules. Let’s see an example:
<?php
/** @test */
public function it_should_fail_validation_if_the_title_is_not_provided()
{
$request = new BlogPostCreateRequest();
$validator = Validator::make([
'body' => 'this is a test',
], $request->rules());
$this->assertFalse($validator->passes());
$this->assertContains('title', $validator->errors()->keys());
}
This approach is valid, but it suffers from the same problem as before: you still have to write a test for each field and each validation rule.
However in this case, you can speed things up by using PHPUnit data providers, as explained in this blog post.
For example let’s test first valid data:
<?php
/**
* @dataProvider provideValidData
*/
public function testValidData(array $data)
{
$request = new BlogPostCreateRequest();
$validator = Validator::make($data, $request->rules());
$this->assertTrue($validator->passes());
}
public function provideValidData()
{
$faker = \Faker\Factory::create(Factory::DEFAULT_LOCALE);
return [
[[
'title' => 'test',
'body' => 'this is a test'
]],
[[
'title' => $faker->word(),
'body' => $faker->paragraph()
]],
// ...
];
}
You can clearly see the benefits of this approach. You have one single test, but you can provide as many entries as you want in the data provider.
Let’s look now at the test that checks bad data:
<?php
/**
* @dataProvider provideInvalidData
*/
public function testInvalidData(array $data)
{
$request = new BlogPostCreateRequest();
$validator = Validator::make($data, $request->rules());
$this->assertFalse($validator->passes());
}
public function provideInvalidData()
{
return [
[[]], // missing fields
[[
'title' => 'test' // missing body
]],
[[
'body' => 'this is a test' // missing title
]],
[[
'title' => str_repeat('a', BlogPostCreateRequest::TITLE_MAX_LENGTH + 1), // title too long
'body' => 'this is a test'
]],
// ...
];
}
You can cover pretty much all cases with the combination of valid/invalid data providers.
Some developers may argue that you’re basically testing Laravel’s validation rules. They would be right, but I still think that this approach can be valuable, especially when you discover that in your production code, a particular set of bad data slipped through your validation rules. You just have to take that bad data, add it to the provider and make the test fail and finally fix the bug to get the test passing.
I honestly can't say enough about this.
— Caleb Porzio (@calebporzio) January 16, 2020
Just get in the habit. Treat a new bug as a trigger to write a test, then fix the bug to get the test passing.
Your code-base will be SO much better off.
Nevertheless the biggest downside of this approach is that it makes really hard to test validation from methods that are related to your Form Request, such as prepareForValidation
or extensions to the Validator instance.
prepareForValidation
is a function that has been introduced in Laravel 5.4 and that can be used inside a Form Request to manipulate data before the validation:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
class BlogPostCreateRequest extends FormRequest
{
/**
* Sanitize request data before validation.
*/
protected function prepareForValidation()
{
// Warning: this is a bad example made on purpose!
$this->replace([
'title' => null,
'body' => $this->input('body')
]);
}
// ...
}
As you can see here we are replacing the input of the request by setting the title
to null
. All unit tests set up before will still pass because the validation rules are left untouched, but in reality this code will never work in production. However if you think about it, this makes perfectly sense because in the previous examples we were actually unit testing only the rules
method and not the entire validation functionality.
The same goes for the extensions of the Validator. Inside a Form Request you can for example configure the Validator instance by adding an after hook or conditional rules to it.
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator
class BlogPostCreateRequest extends FormRequest
{
/**
* Configure the validator instance.
*
* @param Validator $validator
* @return void
*/
public function withValidator($validator)
{
// callback to be run after validation is completed
$validator->after(function ($validator) {
if ($this->somethingElseIsInvalid()) {
$validator->errors()->add('field', 'Something is wrong with this field!');
}
});
// rules that are added based on a condition
$v->sometimes('summary', 'required|string|max:200', function ($input) {
return strlen($input->body) >= 1000;
});
}
// ...
}
Unit tests that check the rules
will still pass, but you have to be aware that they will not cover all the potential validation paths for your Form Request.
Unit Testing Complex Form Requests
Now let’s see if we are able to unit test more complex Form Requests.
For example suppose that we want to add a route to update blog posts in our application. We can create a new Form Request and keep the same requirements of the BlogPostCreateRequest
, in terms of validation. The substantial difference however is in the authorize
method because we want to restrict the update action only to the author that created the post in the first place.
After adding the route (PUT /posts/:id
) to our routes file and the update
method in the controller, we can write the new BlogPostUpdateRequest
class:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Session;
class BlogPostUpdateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$id = $this->route('id');
$post = Post::findOrFail($id)
return Auth::check() and $post->user_id === Auth::user()->id;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'title' => 'required|string|max:' . BlogPostCreateRequest::TITLE_MAX_LENGTH,
'body' => 'required|string'
];
}
}
We could have used the rules already defined in the BlogPostCreateRequest
, for example by extending it in the new class, but I decided to copy them to keep things as simple as possible.
Let’s look at the authorize
method. We are taking the id of the post by getting the id
route parameter from the url. Then we check if the user_id
attribute in the retrieved Post
model matches with the id
of the logged user.
How can we test this class without writing a feature test for it? Well, it’s still possible, but it requires some additional work.
The fact that you’re using $this->route()
inside the authorize
method may suggest you that you need some sort of Route
object defined inside the request, just like previously you had to had a user defined in order to do $this->user()
, remember?
If you try to write the following:
<?php
/** @test */
public function it_should_return_the_id_route_parameter()
{
$request = new BlogPostUpdateRequest();
dd($request->route()->parameter('id'));
}
you will unavoidably stumble upon this error
Error: Call to a member function parameter() on null
Remember that you’re unit testing, therefore your Form Request is entirely detached from the rest of your Laravel applilcation. How can we let the request class know that there is a route for it? We need to take a look at the internals.
First of all let’s open the Illuminate\Http\Request
class, which is extended by our Form Request.
As you can see from the implementation, when you call $this->route('id')
the class tries to match the request to a route by calling the closure defined in the getRouteResolver()
method. This closure is empty therefore in our unit tests any call to $this->route(...)
will return null
. During a real HTTP request Laravel takes care of setting up the route resolver, but during a unit test we have to take care of that by ourself. Luckily for us this is not really that hard:
<?php
use Illuminate\Support\Facades\Route;
/** @test */
public function it_should_return_the_id_route_parameter()
{
$request = new BlogPostUpdateRequest();
$request->setRouteResolver(function () use ($request) {
$routes = Route::getRoutes();
return $routes->match($request);
});
dd($request->route()->parameter('id'));
}
As you can see, we are manually setting a route resolver Closure on the request. Inside this function we are using the Route
facade to get the entire list of routes from our application, and finally we try to match the request on the RouteCollection
class.
However this still doesn’t work because we didn’t specify that our Form Request handles HTTP requests issued with a very specific HTTP method (PUT
), on a very specific URL (/posts/:id
). Therefore we have to update how the request is initialized.
Up to now in our tests we have always used the new
keyword to create a new instance of the request. However, as you can see from the implementation, the Illuminate\Http\Request
doesn’t have a constructor, but it uses the parent constructor defined on the Symfony\Component\HttpFoundation\Request
class. This already looks pretty complex, but if you scroll down you will notice that there is also a static create
method, which definitely looks more readable. We can try to implement it:
<?php
use Illuminate\Support\Facades\Route;
/** @test */
public function it_should_return_the_id_route_parameter()
{
$user = factory(User::class)->create();
$post = factory(Post::class)->create([
'user_id' => $user->id
]);
$url = "/posts/{$post->id}";
$method = 'PUT';
$request = BlogPostUpdateRequest::create($url, $method, [
'title' => 'my updated title',
'body' => $post->body
]);
$request->setRouteResolver(function () use ($request) {
$routes = Route::getRoutes();
return $routes->match($request);
});
Auth::shouldReceive('check')->once()->andReturn(true);
Auth::shouldReceive('user')->once()->andReturn($user);
$this->assertTrue($request->authorize());
}
And we have green!
We can further improve this test by extracting the setRouteResolver
functionality inside a trait
<?php
trait RouteResolver
{
/**
* Set route resolver.
*
* @param Request $request
*/
protected function setRouteResolver(Request $request)
{
$request->setRouteResolver(function () use ($request)
{
$routes = Route::getRoutes();
return $routes->match($request);
});
}
}
otherwise you can use a dedicated package developed by Mohammed Manssour
The same goes for the setUserResolver
callback that you can use to inject a user instance into your request.
That’s it! Happy testing!