Unit Test Laravel's FormRequest Unit Test Laravel's FormRequest laravel laravel

Unit Test Laravel's FormRequest


I found a good solution on Laracast and added some customization to the mix.

The Code

public function setUp(){    parent::setUp();    $this->rules     = (new UserStoreRequest())->rules();    $this->validator = $this->app['validator'];}/** @test */public function valid_first_name(){    $this->assertTrue($this->validateField('first_name', 'jon'));    $this->assertTrue($this->validateField('first_name', 'jo'));    $this->assertFalse($this->validateField('first_name', 'j'));    $this->assertFalse($this->validateField('first_name', ''));    $this->assertFalse($this->validateField('first_name', '1'));    $this->assertFalse($this->validateField('first_name', 'jon1'));}protected function getFieldValidator($field, $value){    return $this->validator->make(        [$field => $value],         [$field => $this->rules[$field]]    );}protected function validateField($field, $value){    return $this->getFieldValidator($field, $value)->passes();}

Update

There is an e2e approach to the same problem. You can POST the data to be checked to the route in question and then see if the response contains session errors.

$response = $this->json('POST',     '/route_in_question',     ['first_name' => 'S']);$response->assertSessionHasErrors(['first_name']);


Friends, please, make the unit-test properly, after all, it is not only rules you are testing here, the validationData and withValidator functions may be there too.

This is how it should be done:

<?phpnamespace Tests\Unit;use App\Http\Requests\AddressesRequest;use App\Models\Country;use Faker\Factory as FakerFactory;use Illuminate\Routing\Redirector;use Illuminate\Validation\ValidationException;use Tests\TestCase;use function app;use function str_random;class AddressesRequestTest extends TestCase{    public function test_AddressesRequest_empty()    {        try {            //app(AddressesRequest::class);            $request = new AddressesRequest([]);            $request                ->setContainer(app())                ->setRedirector(app(Redirector::class))                ->validateResolved();        } catch (ValidationException $ex) {        }        //\Log::debug(print_r($ex->errors(), true));        $this->assertTrue(isset($ex));        $this->assertTrue(array_key_exists('the_address', $ex->errors()));        $this->assertTrue(array_key_exists('the_address.billing', $ex->errors()));    }    public function test_AddressesRequest_success_billing_only()    {        $faker = FakerFactory::create();        $param = [            'the_address' => [                'billing' => [                    'zip'        => $faker->postcode,                    'phone'      => $faker->phoneNumber,                    'country_id' => $faker->numberBetween(1, Country::count()),                    'state'      => $faker->state,                    'state_code' => str_random(2),                    'city'       => $faker->city,                    'address'    => $faker->buildingNumber . ' ' . $faker->streetName,                    'suite'      => $faker->secondaryAddress,                ]            ]        ];        try {            //app(AddressesRequest::class);            $request = new AddressesRequest($param);            $request                ->setContainer(app())                ->setRedirector(app(Redirector::class))                ->validateResolved();        } catch (ValidationException $ex) {        }        $this->assertFalse(isset($ex));    }}


I see this question has a lot of views and misconceptions, so I will add my grain of sand to help anyone who still has doubts.

First of all, remember to never test the framework, if you end up doing something similar to the other answers (building or binding a framework core's mock (disregard Facades), then you are doing something wrong related to testing).

So, if you want to test a controller, the always way to go is: Feature test it. NEVER unit test it, not only is cumbersome to unit test it (create a request with data, maybe special requirements) but also instantiate the controller (sometimes it is not new HomeController and done...).

They way to solve the author's problem is to feature test like this (remember, is an example, there are plenty of ways):

Let's say we have this rules:

public function rules(){    return [        'name' => ['required', 'min:3'],        'username' => ['required', 'min:3', 'unique:users'],    ];}
namespace Tests\Feature;use App\Models\User;use Illuminate\Foundation\Testing\RefreshDatabase;use Tests\TestCase;class HomeControllerTest extends TestCase{    use RefreshDatabase;    /*     * @dataProvider invalid_fields     */    public function test_fields_rules($field, $value, $error)    {        // Create fake user already existing for 'unique' rule        User::factory()->create(['username' => 'known_username']);        $response = $this->post('/test', [$field => $value]);        $response->assertSessionHasErrors([$field => $error]);    }    public function invalid_fields()    {        return [            'Null name' => ['name', null, 'The name field is required.'],            'Empty name' => ['name', '', 'The name field is required.'],            'Short name' => ['name', 'ab', 'The name must be at least 3 characters.'],            'Null username' => ['username', null, 'The username field is required.'],            'Empty username' => ['username', '', 'The username field is required.'],            'Short username' => ['username', 'ab', 'The username must be at least 3 characters.'],            'Unique username' => ['username', 'known_username', 'The username has already been taken.'],        ];    }}

And that's it... that is the way of doing this sort of tests... No need to instantiate/mock and bind any framework (Illuminate namespace) class.

I am taking advantage of PHPUnit too, I am using data providers so I don't need to copy paste a test or create a protected/private method that a test will call to "setup" anything... I reuse the test, I just change the input (field, value and expected error).

If you need to test if a view is being displayed, just do $response->assertViewIs('whatever.your.view');, you can also pass a second attribute (but use assertViewHas) to test if the view has a variable in it (and a desired value). Again, no need to instantiate/mock any core class...

Have in consideration this is just a simple example, it can be done a little better (avoid copy pasting some errors messages).


One last important thing: If you unit test this type of things, then, if you change how this is done in the back, you will have to change your unit test (if you have mocked/instantiated core classes). For example, maybe you are now using a FormRequest, but later you switch to other validation method, like a Validator directly, or an API call to other service, so you are not even validating directly in your code. If you do a Feature Test, you will not have to change your unit test code, as it will still receive the same input and give the same output, but if it is a Unit Test, then you are going to change how it works... That is the NO-NO part I am saying about this...

Always look at test as:

  1. Setup minimum stuff (context) for it to begin with:
    • What is your context to begin with so it has logic ?
    • Should a user with X username already exist ?
    • Should I have 3 models created ?
    • Etc.
  2. Call/execute your desired code:
    • Send data to your URL (POST/PUT/PATCH/DELETE)
    • Access a URL (GET)
    • Execute your Artisan Command
    • If it is a Unit Test, instantiate your class, and call the desired method.
  3. Assert the result:
    • Assert the database for changes if you expected them
    • Assert if the returned value matches what you expected/wanted
    • Assert if a file changed in any desired way (deletion, update, etc)
    • Assert whatever you expected to happen

So, you should see tests as a black box. Input -> Output, no need to replicate the middle of it... You could setup some fakes, but not fake everything or the core of it... You could mock it, but I hope you understood what I meant to say, at this point...