EliasSoares's avatar

TDD - How to test Controller Validation?

Hi,

We're developing a new system trying to use TDD everywhere, and I'm stuck at Validation tests.

I use the Request classes to validate my API Requests. The problem is how to test it?

I've seen two options:

  1. Manually test all possible combinations for all fields and make many assertions for it.
  2. Just check if my Request rules() method have my expected validations (relying on Laravel Validator) and just check if my Request are being called on my acceptance tests.

I'm tending to use the 2nd approach, but I'm suck on how can I assert that my controller is using the Request class for that call?

My current acceptance test for a update method is:

    /** @test */
    public function test_update_method()
    {
        $this->authenticate();

        $originalClient = factory(Client::class)->create();
        $modifiedClient = factory(Client::class)->make()->toArray();
    
        $this->json('put', '/client/' . $originalClient->id, $modifiedClient);
        // ASSERT HERE IF ClientRequest was used on this request
        $this->seeStatusCode(200);
        $this->seeJson();
        $this->checkJsonStructure();

        $this->seeInDatabase('clients', $modifiedClient);
    }
0 likes
4 replies
Kandown's avatar

I'm having the same issue. Hope someone more skilled will help to solve this. I would also appreciate to be able to test if the given validation rule was applied on the request.

EliasSoares's avatar

@Kandown

I didn't solved this problem, not in a "beauty way".

I just check if my ClientRequest has the right (expected) validation rules, and trust that Laravel Validation is working as expected.

I've also created an abstract test to do this tests, where the rules order isn't important.

Also you can create a test for each validation on your controller, but this will be very exhaustive, since some models have 20~30 fields, each with 1~3 validation rules, and some rules are complicated to test...

Kandown's avatar

@EliasSoares I've solved this in a dirty way. Wondering about a pull request after a cleanup.. Thank you very much for your response. Below is my working example..

Usage:

$this->post(route('profile.change-password'), [
    'old_password' => 'password',
    'password' => $password,
    'password_confirmation' => $password
]);

$this->hasValidator('old_password', 'required');
$this->hasValidator('password', ['required', 'min:6', 'confirmed']);

Implementation

protected function hasValidator($field, $rule)
{
    $controllerWithMethod = explode('@', app('router')->current()->getActionName());
    $controller = $controllerWithMethod[0];
    $method = $controllerWithMethod[1];

    $rules = [];
    $refleciton = new ReflectionMethod($controller, $method);

    // foreach method parameters
    foreach ($refleciton->getParameters() as $parameter) {
        // is instance of form request
        if ($parameter->getClass()->isSubclassOf(\Illuminate\Foundation\Http\FormRequest::class)) {
            // has method rules
            if ($parameter->getClass()->hasMethod('rules')) {
                try {
                    $request = app($parameter->getClass()->getName());
                    foreach ($request->rules() as $f => $r) {
                        $rules[$f] = explode("|", $r);
                    }
                } catch (\Illuminate\Validation\ValidationException $e) {
                    $rules = $e->validator->getRules();
                }
            }
        }
    }

    self::assertArrayHasKey(
        $field,
        $rules,
        "\e[0;33mField [{$field}] has no validation rules applied.\e[m"
    );

    if (is_array($rule)) {
        foreach ($rule as $r) {
            self::assertTrue(
                in_array($r, $rules[$field]),
                "\e[0;33mField [{$field}] does not have a validation rule [{$r}] applied.\e[m"
            );
        }
    } else {
        self::assertTrue(
            in_array($rule, $rules[$field]),
            "\e[0;33mField [{$field}] does not have a validation rule [{$rule}] applied.\e[m"
        );
    }

    return $this;
}
ifpingram's avatar

@Kandown You have massively overcomplicated things. Testing is essentially about taking an input, passing it through some code, then verifying the outcomes. Instead you are introspecting into the code itself, and this will only lead to pain later on.

To begin with the test code is very difficult to read at a glance. I have looked over it twice and have no idea what is going on. In six months time, when you go back to look at it, nor will you!

Secondly, you are essentially trying to test Laravel's internal features. These are tested within the framework itself. There is no need for you to do it again.

Thirdly, and going back to my original point, unless you are leaving out bits of test code in your "Usage" section, you are not verifying outcomes but looking at system configuration directly. System configuration will have an effect on outcomes, but it is the outcomes you should be asserting against.

What you should be doing is setting up the test to drive out the outcomes you are looking to get when the wrong input is received. Then the assertions should be against the response you receive. If the validation rules are broken, then your response should indicate which rules are broken and you should then assert accordingly...

5 likes

Please or to participate in this conversation.