36

I am trying to unit test various custom FormRequest inputs. I found solutions that:

  1. Suggest using the $this->call(…) method and assert the response with the expected value (link to answer). This is overkill, because it creates a direct dependency on Routing and Controllers.

  2. Taylor’s test, from the Laravel Framework found in tests/Foundation/FoundationFormRequestTest.php. There is a lot of mocking and overhead done there.

I am looking for a solution where I can unit test individual field inputs against the rules (independent of other fields in the same request).

Sample FormRequest:

public function rules()
{
    return [
        'first_name' => 'required|between:2,50|alpha',
        'last_name'  => 'required|between:2,50|alpha',
        'email'      => 'required|email|unique:users,email',
        'username'   => 'required|between:6,50|alpha_num|unique:users,username',
        'password'   => 'required|between:8,50|alpha_num|confirmed',
    ];
}

Desired Test:

public function testFirstNameField()
{
   // assertFalse, required
   // ...

   // assertTrue, required
   // ...

   // assertFalse, between
   // ...
}

public function testLastNameField()
{
    // ...
}

How can I unit test (assert) each validation rule of every field in isolation and individually?

Dov Benyomin Sohacheski
  • 7,133
  • 7
  • 38
  • 64
  • Well, as you said, FormRequests are tested inside Laravel so you don't have to test them again, Validation are also tested in Laravel. I don't really get what you want test exactly... – Elie Faës May 04 '16 at 20:31
  • It's a very simple thing, I am looking to test the validation rules that I set for my form requests. – Dov Benyomin Sohacheski May 04 '16 at 20:33
  • So you want to test if `XformRequest::rules` have let's say `$first_name === 'required'`? – Elie Faës May 04 '16 at 20:36
  • No. More like: assertTrue('jon', FormRequest::rules()['first_name']) – Dov Benyomin Sohacheski May 04 '16 at 20:39
  • 5
    So yeah that's what I wanted to say in my first comment, Validation rules are already tested in Laravel. In your example you just want to test if an input is valid against your set of rules but the rules work, that's what the Laravel tests tell us. I'm not really sure it's necessary to test it again. IMHO it's the same as writing `$this->assertEquals([ 'first_name' => 'required|between:2,50|alpha'....], (new FormRequest)->rules())` – Elie Faës May 04 '16 at 20:50
  • 1
    I am looking to test the inputs against the rules that are set both valid and invalid inputs – Dov Benyomin Sohacheski May 04 '16 at 20:52
  • Well, good luck ;) – Elie Faës May 04 '16 at 20:55
  • 2
    I voted up this question and redirect here all the other similar questions. I hope it will help many people. – Yevgeniy Afanasyev Mar 29 '19 at 00:50
  • 1
    @PeterPan666 Sure, that works for simple rules. But if you combine rules and use regular expressions and such, then things become more complicated. I trust that Laravel will test the input against the regular expression, but I don't trust myself fully understanding the rules API and always writing correct regular expressions. It would be nice to have a way to test this. Someone actually wrote a a package for this*, but I think this should work out-of-the-box. * https://github.com/mohammedmanssour/form-request-tester – Luc Jul 14 '19 at 18:25

3 Answers3

39

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

The Code

/**
 * Test first_name validation rules
 * 
 * @return void
 */
public function test_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'));
}

/**
 * Check a field and value against validation rule
 * 
 * @param string $field
 * @param mixed $value
 * @return bool
 */
protected function validateField(string $field, $value): bool
{
    return $this->validator->make(
        [$field => $value],
        [$field => $this->rules[$field]]
    )->passes();
}

/**
 * Set up operations
 * 
 * @return void
 */
public function setUp(): void
{
    parent::setUp();

    $this->rules     = (new UserStoreRequest())->rules();
    $this->validator = $this->app['validator'];
}

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']);
Josh Bonnick
  • 2,281
  • 1
  • 10
  • 21
Dov Benyomin Sohacheski
  • 7,133
  • 7
  • 38
  • 64
  • What about password with confirmed rule? – ExoticSeagull Jun 30 '17 at 09:40
  • @ClaudioLudovicoPanetta: Change the `$field` in `validateField` to be an array of data to check. Example: `protected function validateField(array $data, string $rule_to_check) { return $this->validator->make($data, [$rule_to_check => $this->rules[$rule_to_check]]) ->passes(); }` Then: `$this->assertTrue($this->validateField(['password' => 'correcthorsebatterystaple', 'password_confirmation' => 'correcthorsebatterystaple'], 'password'));` – YOMorales Jun 14 '18 at 15:20
  • Thank you for answering your own question. It is very considerate of you. – Yevgeniy Afanasyev Mar 29 '19 at 00:49
  • If not in an application context, the following will work: use Illuminate\Translation\ArrayLoader; use Illuminate\Translation\Translator; use Illuminate\Validation\Factory; $validator = new Factory(new Translator(new ArrayLoader, 'en')); – Michael Cordingley Apr 22 '19 at 15:38
  • 2
    Please note that by using this approach, array validation does not work. To achieve this, you have to pass the validator a bigger array, including nested fields (like names.*) – Giraffe Jul 13 '19 at 12:12
  • Please, **NEVER EVER** test the framework, the solution is to do a feature test with `$this->post` or `->get` or whatever you need, but NEVER test the framework... – matiaslauriti Apr 06 '21 at 14:42
  • @matiaslauriti That's true, however this allows you to isolate the specific validation rule (since there may be many) and focus at the unit level. – Dov Benyomin Sohacheski Apr 06 '21 at 15:11
  • Still, you are testing the framework, you are doing bad testing. This must be a feature test, and if you still require to "test" a `FormRequest` then you are doing something is wrong. This is a no-no from all points of view. – matiaslauriti Apr 06 '21 at 15:27
  • 2
    @matiaslauriti But this is not testing the framework at all. This is testing that the FormRequest is using the right rules. There's nothing in this test that tests the implementation. It only says "for the rules applied to first_name, show that 'j' is invalid". Doing feature tests using ->get or ->post is fine, but when reusing the same FormRequests in many routes, you'll simply duplicate validation & authorization tests. Testing the way OP does allows you to test the FormRequest in isolation, and mock it in the controller tests. – Steve Chamaillard Apr 07 '21 at 10:24
  • You are still testing part of the framework, a `FormRequest` has its own separate test (look at the [framework's source code](https://github.com/laravel/framework/blob/8.x/tests/Foundation/FoundationFormRequestTest.php) it is already tested there). I did not reuse a lot parts of the rules, but if you are reusing the whole `FormRequest`, again, that is an alarm that there is something wrong. Why would you require to send exactly the same data in other point ? Still needs to be a feature test, because you can later use another form of validation and you lost all the `FormRequest` passes... – matiaslauriti Apr 07 '21 at 18:57
  • Necessary plug to Adam Wathan where this code came from https://tailwindcss.com/ http://fullstackradio.com/ – plushyObject Apr 21 '21 at 21:52
11

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...

matiaslauriti
  • 7,065
  • 4
  • 31
  • 43
5

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:

<?php

namespace 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));
    }


}
Yevgeniy Afanasyev
  • 37,872
  • 26
  • 173
  • 191
  • If you want to be very specific, then you should be testing the whole data-set as a *feature* and not as a *unit*. – Dov Benyomin Sohacheski Mar 28 '19 at 07:19
  • 1
    @DovBenyominSohacheski, NO. Unit testing is a quicker option and it allows you to get better abstraction and encapsulation. And it is a part of the question too. You can learn from my post instead of practising your revenge down-votes. Think of being better developer, not of your self esteem, we are all learning here. – Yevgeniy Afanasyev Mar 29 '19 at 00:46
  • I wouldn't use random data in a test. Other than that, this seems like a good solution. Though I feel like the FormRequest wasn't designed with testability in mind. – Luc Jul 14 '19 at 18:15
  • @Luc , I recommend to use random data in some tests, because, sometimes it gives you something that you may overlook. For example, thanks to random data I have discovered that American Zip code may consist of 2 numbers, and the other time, when I was taking a substr of a name for a short reference, I did not trim that substr, and some names was having space as a last character of the substr and it was braking some other logic. But, don't get me wrong, in some cases the predefined values are must. – Yevgeniy Afanasyev Jul 16 '19 at 00:21
  • **NEVER EVER** test the framework/core... This is really wrong. You have to unit-test your code, in this case would be doing a `feature` test and see if it fails (expected or not), so the `FormRequest` will be run anyway and fully... I cannot belive that someone recommends to do good unit tests and recommends to test the framework... – matiaslauriti Apr 06 '21 at 14:40
  • @matiaslauriti, did you see the "Request" class? Did you see the way it should be extended to be put to use? Laravel suggests to create a class that would inherit the base "Request" class with some function overwritten. This is not something you can abstract out. This is overwriting! You have no choice but testing your extended class along with all the base classes. Your "NEVER EVER" is not always applicable. – Yevgeniy Afanasyev Apr 15 '21 at 03:45
  • No sir, you still do a FEATURE TEST and avoid doing a UNIT on that specific part... but whatever, you will not understand it and I don't get paid for fighting. – matiaslauriti Apr 15 '21 at 03:53
  • @matiaslauriti, I'm sorry for being harsh. Please help. "FEATURE TEST" or "UNIT" what is the difference? I thought it did not matter, because any way you are making a test file for `phpunit` to process. – Yevgeniy Afanasyev Apr 15 '21 at 05:04
  • 1
    Now you are trolling, okay, no problem at all. +1 – matiaslauriti Apr 15 '21 at 05:19
  • 1
    This a good example for Feature testing. If you have extra logic in the FormRequest class, like `prepareForValidation` with casting logic or whatever. This will imitate initializing the request for your controller function well so you can test separately. I found this useful. – Iulian Cravet Jul 06 '23 at 22:04