Testing Laravel form requests
Tags: webdev, laravel, php, testing
Published: 2019-07-05 08:47:28

---
title: Testing Laravel form requests
published: true
description: Testing Laravel form requests
tags: webdev, laravel, php, testing
---

I was struggling to decide how I should test form requests in Laravel. Specifically, I wanted to test validation. I did a quick google search and it looks like there are multiple ways to do it. One blog post stood out of all, which is [A guide to unit testing Laravel Form Requests in a different way](https://medium.com/%40daaaan/a-guide-to-unit-testing-laravel-form-requests-in-a-different-way-f1bdb6d86053) by [@daaaan](https://medium.com/@daaaan). He suggests instead of writing integration tests, and having multiple methods such as `it_fails_validation_without_title` we should use unit tests with the help of PHPUnit's @dataProviders. 

This was definitely something I wanted to try, because in the past I had form requests with tens of test methods, each testing individual request attribute, and there was a lot of code duplication.

Daan suggests the following:

```
<?php

namespace Tests\Feature\App\Http\Requests;

use App\Http\Requests\SaveProductRequest;
use Faker\Factory;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class SaveProductRequestTest extends TestCase
{
    use RefreshDatabase;

    /** @var \App\Http\Requests\SaveProductRequest */
    private $rules;

    /** @var \Illuminate\Validation\Validator */
    private $validator;

    public function setUp(): void
    {
        parent::setUp();

        $this->validator = app()->get('validator');

        $this->rules = (new SaveProductRequest())->rules();
    }

    public function validationProvider()
    {
        /* WithFaker trait doesn't work in the dataProvider */
        $faker = Factory::create( Factory::DEFAULT_LOCALE);

        return [
            'request_should_fail_when_no_title_is_provided' => [
                'passed' => false,
                'data' => [
                    'price' => $faker->numberBetween(1, 50)
                ]
            ],
            'request_should_fail_when_no_price_is_provided' => [
                'passed' => false,
                'data' => [
                    'title' => $faker->word()
                ]
            ],
            'request_should_fail_when_title_has_more_than_50_characters' => [
                'passed' => false,
                'data' => [
                    'title' => $faker->paragraph()
                ]
            ],
            'request_should_pass_when_data_is_provided' => [
                'passed' => true,
                'data' => [
                    'title' => $faker->word(),
                    'price' => $faker->numberBetween(1, 50)
                ]
            ]
        ];
    }

    /**
     * @test
     * @dataProvider validationProvider
     * @param bool $shouldPass
     * @param array $mockedRequestData
     */
    public function validation_results_as_expected($shouldPass, $mockedRequestData)
    {
        $this->assertEquals(
            $shouldPass, 
            $this->validate($mockedRequestData)
        );
    }

    protected function validate($mockedRequestData)
    {
        return $this->validator
            ->make($mockedRequestData, $this->rules)
            ->passes();
    }
}
```

#How does this work?

Looks like we just provide an array with a test method, whether we expect it to pass, and some request attributes. We can have as many test methods as we want this way, and PHPunit will take care of the rest. We add `@dataProvider `annotation to `validation_results_as_expected` method, which is where the assertion actually happens. Looping through each item, it then calls `validate` method in our test case. This method, in turn calls `validate` on `$this->validator` which we define in `setUp`. 


#The problem
Now, I really like this approach, but I felt like I am not actually testing my form request. In the `setUp` method, we instantiate the form request, but we only get the validation rules from it and pass it down to Laravel's validator. This would probably work most of the time with simple data, but in my case, I was doing some data manipulation in `prepareForValidation` form request method. I was also doing some other checks in `withValidator` method. With this testing approach, those methods are not called, and I don't really know whether my form request works as expected.

#The solution?

I thought instead of setting up the validator, we should setup the actual form request, so I quickly changed the `validate` method in our test case to something like this:

```php
protected function validate($mockedRequestData)
{
    return (new CreateRedirectRequest())->...;
}
```
Hang on a minute... What method do I call? How do I pass the data to it? I then inspected the `Illuminate\Foundation\Http\FormRequest` and it looks like it extends `Illuminate\Http\Request`, which in turn extends Symfony `Symfony\Component\HttpFoundation\Request`. Okay, it looks like the form requests are actually extending the request, and, we do not want to be setting it up ourselves. 

So I thought I would resolve it out of the container. I changed the validate method to something like this: 

```php
protected function validate($mockedRequestData)
{
    app(CreateRedirectRequest::class);
}
```

I run the test and I was immediately greeted with an error. In my `withValidator` method I was doing something like this:

```php
public function withValidator($validator)
{
    $validator->after(function ($validator) {
        if (! $this->parse($this->source)) {
            $validator->errors()->add('source', 'Invalid source');
        }
    });
}
```

The error was `Argument 1 passed to parse() must be of the type string, null given`. How does this work, I thought. We are not calling the validation yet, but `withValidator` method is called, and I didn't even pass data to it yet. 

I inspected the `FormRequest` class again, and it looks like it is using  `ValidatesWhenResolved` interface and `ValidatesWhenResolvedTrait` trait. Which, well, does what it says. When the object using this trait is resolved out of the container, it will call `prepareForValidation`, it will check authorization, and finally will validate the request properly.

In the `FormRequestServiceProvider` we can see this code:

```php
 /**
 * Bootstrap the application services.
 *
 * @return void
 */
public function boot()
{
    $this->app->afterResolving(ValidatesWhenResolved::class, function ($resolved) {
        $resolved->validateResolved();
    });

    $this->app->resolving(FormRequest::class, function ($request, $app) {
        $request = FormRequest::createFrom($app['request'], $request);

        $request->setContainer($app)->setRedirector($app->make(Redirector::class));
    });
}
```

Okay, so this happens afterResolving, I know laravel also has a `resolving` callback, so could we pass our data before it is resolved? I changed `validate` method once again to something like this:

```php
protected function validate($mockedRequestData)
{
    $this->app->resolving(CreateRedirectRequest::class, function ($resolved) use ($mockedRequestData){
        $resolved->merge($mockedRequestData);
    });

    try {
        app(CreateRedirectRequest::class);

        return true;
    } catch (ValidationException $e) {
        return false;
    }
}

```

I run the tests and they all pass. Now when I want to add a new test to this test case, I only need to add a new array item with the data and whether it should pass.

##So why does it work?

It works because when Laravel's container is resolving the form request 
object, it will merge the `$mockedRequestData` array, which holds the data from our test array, to the request object. 

If it resolves successfully, we know all the data is valid, otherwise it will throw `Illuminate\Validation\ValidationException`, which we try to catch. We return either true or false, because our `validation_results_as_expected` method then compares it to whether the test case should pass or not. We also do not need 'setUp' anymore. 

## Final words

If you are writing multiple test methods testing each request attribute individually, I would say give this a try. Maybe you will like it. I certainly prefer this approach. 


#### P.S 
Although I am not new to web development, this is my first blog post. If you would like to read more blog posts like this in the future, feel free to follow me or leave a comment, I welcome any feedback or questions.