I always deal with controllers as a glue for my applications and never done any logic inside them, including Validation & Authorization logic. Laravel FormRequest classes have helped me a lot with keeping my controllers as a glue, but I always found them very hard to be tested. By surfing the web I found two ways to test them and also worked to create my own way, so in this blog post, I'm gonna demonstrate three ways to test Laravel FormRequests.
Before diving in:
before doing any work we need a FormRequest as an example so let's take the following into consideration.
- we need to update a post fields : title, content
- both fields are required, content should have more that 300 characters (min:300)
posts/{post}
usingput
http method, is the route used to update the post.- and the following class is the FormRequest the we'll use to explain the three ideas.
namespace MohammedManssour\FormRequestTester\Tests\Stubs\FormRequests; use Illuminate\Foundation\Http\FormRequest; use MohammedManssour\FormRequestTester\Tests\Stubs\Models\Post; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class UpdatePost extends FormRequest { /** * post to be updated * * @var \MohammedManssour\FormRequestTester\Tests\Stubs\Models\Post */ protected $model; public function rules() { return [ 'title' => ['bail', 'required'], 'content' => ['bail', 'required', 'min:300'], ]; } public function messages() { return [ 'title' => 'title field is required', 'content.required' => 'Content Field is required', 'content.min' => 'Content length should be 300 chars at least' ]; } public function authorize() { return $this->getModel()->user_id == auth()->user()->id; } public function getModel() { if (!$this->model) { $this->model = Post::find($this->route('post')); throw_if(!$this->model, NotFoundHttpException::class); } return $this->model; } }
Idea #1: Creating your own validator:
This way is very simple and intuitive, simply, you create your own validator and inject your rules and messages inside the validator, like the following
function test_validation_fails(){ $requst = new UpdatePost(); $data = [] $validator = Validator::make($data, $request->rules(), $request->messages()); $this->assertTrue($validator->fails()); }
Pros
- It's simple and pretty much, It's the first thing you'll think of when start testing your FormRequests.
Cons
- things will get harder as you making assertions for what fields have failed and what are the messages returned.
- testing authorization is very hard, especially when you're using
$this->route('post')
method in your authorization logic. - things that are related to your FormRequest can't be tested this way like
prepareForValidation
one way to overcome the authorization problem is to add the ability to pass the model to authorize
method, butI'm not a fan of adding a piece of code to the logic just to improve testing.
/ in UpdatePost function authorize($model = null) { if(is_null($model)){ $model = $this->getModel(); } return $model->user_id == auth()->user()->id; }
Idea #2: sending real requests:
The second approach is sending a real request to the route where the FormRequest will be instantiated and then see what's the response and act accordingly
function test_validation_fails(){ $post = factory(Post::class)->create(); $this->json('put', 'posts/'.$post->id,[]) ->assertStatus(422) ->assertJsonValidationErrors(['content', 'title']) ->assertJsonMessage(['content field is required', 'title field is required']); }
Pros
- It's an integration test.
Cons:
- It's an Integration test: meaning that you have to make sure everything is working in the controller where FormRequest is instantiated because the simplest error will throw an exception.
- you have to send two real requests if you want to test another rule for the same field. ex: you need to send content with your request to make sure that min:300 rule is working.
- makes your tests slower: because you need to start your whole application with each test.
Idea #3: Using FormRequestTester package:
this package will mock the needed part of your application and reuse them without the need to start your whole application on every test.
function test_validation_fails(){ $this->formRequest(UpdatePost::class) ->put([ 'title' => 'New Title' ]) ->withRoute('posts/{post}') ->assertAuthorized() ->assertValidationFailed() ->assertValidationErrors(['content']) ->assertValidationErrorsMissing(['title']) ->assertValidationMessages(['Content field is required']) }
Pros:
- It's simple: It provides simple and intuitive methods.
- It's a unit test, not an integration test: for me, I don't use integration tests to make sure that one thing works fine, I only use it to make sure that all classes in my app work well with each other.
- It allows you to test validation, authorization, and other related topics.
Cons:
As I'm the developer of the package, I can't find a con for it, but if you have one please notify me via email and I promise to add it here.
Final Words:
I say every programmer should be pragmatic, please choose the best way to suits you and your team.