1
votes

Background: In my Laravel project, I am implementing the user creation function where an admin can create users. During user creation I'm setting a random password (not an input) and sending a password reset email to user. On my schema password field is not nullable (I don't want any users without passwords in the database).

I'm using Form Request to implement the controller validation and prepareForValidation to set the random password on POST but sets the password to null on PATCH as I don't want the password to be updated on user update event. (Admins can update user profiles but not the passwords.) And validation rules 'sometimes' handles the two scenarios.

Below is from my Request class (rules and prepareForValidation methods).

class AdminUserRequest extends FormRequest
{
    public function rules()
    {
        return [
            ...
            'password' => [
                'sometimes',
                'required',
                'min:8',
            ],
            ...
        ];
    }

    /**
     * Prepare the data for validation.
     *
     * @return void
     */
    protected function prepareForValidation()
    {
        if ($this->getMethod() == 'POST') {

            $this->merge([
                'password' => Hash::make(Str::random(10)),
            ]);
        } else {
            $this->offsetUnset('password');
        }
    }
}

Question: My question is what's the best way to unit test functionality of prepareForValidation in Form Request.

It should test that the request data set has a password on POST and and not set on other methods.

Of course I can do functional testing to cover the cases

  • POST to route creates a user in database and has no errors in session
  • PATCH to route doesn't change the password even if it's passed in the dataset.

But I'd like to know if there's a way to unit test this. And how to do it.

Thanks in advance

1
Why not feature test it with a http test ? laravel.com/docs/7.x/http-tests - frogeyedman
there's no reason not to feature test it. But I wanted to know if there's a way to unit test just the prepareForValidation() and may be the 'sometimes' logic in rules. I have unit tests for other rules. Thought it would be neat to cover this in unit test as well. - Jude F'do
Yes, there is. Trigger whatever entry point that method has in your test - Nico Haase
@NicoHaase - That's what I'm not not sure how to do. How do I make a request instance and set the method to a post and verify the request dataset? - Jude F'do

1 Answers

3
votes

It could be something like this (I didn't mock the request and arrange expectations on it - instead kept it simple). The last method is for private/protected method invocation of the classes. This can be moved to TestCase class for future usages.

class AdminUserRequestTest extends TestCase
{
    /**
     * @test
     * @covers ::prepareForValidation
     */
    function it_should_add_password_when_it_is_post_method()
    {
        $request = new AdminUserRequest();
        $request->setMethod('POST');
        $hashedPassword = 'a1b2c3d4e5';

        Hash::shouldReceive('make')->andReturn($hashedPassword);
        $this->invokeMethod($request, 'prepareForValidation');

        $this->assertSame($request->get('password'), $hashedPassword);
    }

    /**
     * @test
     * @covers ::prepareForValidation
     */
    function it_should_unset_password_when_it_is_not_post_method()
    {
        $request = new AdminUserRequest();
        $request->setMethod('PUT'); // it could be something else besides POST
        $this->invokeMethod($request, 'prepareForValidation');

        $this->assertFalse($request->has('password'));
    }

    protected function invokeMethod(&$object, $methodName, array $parameters = [])
    {
        $reflection = new ReflectionClass(get_class($object));
        $method = $reflection->getMethod($methodName);
        $method->setAccessible(true);

        return $method->invokeArgs($object, $parameters);
    }
}