Posted on in #development #laravel #tdd

I recently had a tricky test that was sometimes failing, never a good sign. Sometimes it would pass, next run it would fail. Found out a nice tip you can add --repeat x when running phpunit to run it multiple times which came in handy.

phpunit --filter test_can_register_with_valid_postcode --repeat 5

After investigating it was failing because I had a custom validation rule which used the postcodes.io api to validate a postcode. I was using faker to generate these postcodes with some not being real actual postcodes so the api was reporting them as invalid.

Having tests that hit 3rd party apis could be considered bad practise so I wanted to find a way to mock that in my tests so that it wouldn't happen. One way around this is to type-hint the rule as a dependency in the rules method which is mentioned in the documentation.

namespace App\Http\Requests;

use App\Rules\Postcode;
use Illuminate\Foundation\Http\FormRequest;

class Register extends FormRequest
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @param \App\Rules\Postcode $postcode
     * @return array
     */
    --public function rules()
    ++public function rules(Postcode $postcode)
    {
        return [
            'email' => 'required|string|email|max:255|unique:users|confirmed',
            'email_confirmation' => 'required',
            --'postcode' => ['required', new Postcode],
            ++'postcode' => ['required', $postcode],
        ];
    }
}

We can then mock that class using $this->mock and say that the passes method will return true meaning that it always passes. Thus bypassing the api call. Huzah! Again check out the documentation to see what's possible when mocking.

namespace Tests\Feature;

use App\Rules\Postcode;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class RegisterTest extends TestCase
{
    use WithFaker, RefreshDatabase;

    public function test_can_register_with_valid_postcode()
    {
        $this->mock(Postcode::class, function ($mock) {
            $mock->shouldReceive('passes')->andReturn(true);
        });

        $response = $this->post(route('register'), [
            'email' => 'test@user.com',
            'email_confirmation' => 'test@user.com',
            'postcode' => $this->faker('en_GB')->postcode,
        ]);

        $response->assertStatus(301);
    }
}

One downside to that is that you're then unable to pass additional arguments when creating the instance. So instead of mocking the validation rule, which uses the Guzzle http client to make requests I'm going to mock Guzzle and return a new fake Response with exactly what we want. The Postcode rule looks like this:

<?php

namespace App\Rules;

use GuzzleHttp\Client;
use Illuminate\Contracts\Validation\Rule;

class Postcode implements Rule
{
    public $client;

    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->client = resolve(Client::class);
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $response = $this->client->get('https://api.postcodes.io/postcodes/'.$value.'/validate');

        $body = json_decode((string) $response->getBody(), true);

        return $body['result'];
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The :attribute must be a valid postcode.';
    }
}

As the Client dependency is resolved from the container I'm easily able to mock it in our test:

namespace Tests\Feature;

--use App\Rules\Postcode;
++use GuzzleHttp\Client;
++use GuzzleHttp\Psr7\Response;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class RegisterTest extends TestCase
{
    use WithFaker, RefreshDatabase;

    public function test_can_register_with_valid_postcode()
    {
        --$this->mock(Postcode::class, function ($mock) {
        --   $mock->shouldReceive('passes')->andReturn(true);
        --});
        ++$this->mock(Client::class, function ($mock) {
        ++    $mock->shouldReceive('get')->andReturn(new Response(200, [], '{"status":200,"result":true}'));
        ++});

        $response = $this->post(route('register'), [
            'email' => 'test@user.com',
            'email_confirmation' => 'test@user.com',
            'postcode' => $this->faker('en_GB')->postcode,
        ]);

        $response->assertStatus(301);
    }
}

Always amazed by what's possible with Laravel and how simple yet powerful things are. As we have full control over the fake response we can also write a test to check for failed postcode validation e.g.

public function test_cannot_register_with_invalid_postcode()
{
    $this->mock(Client::class, function ($mock) {
        $mock->shouldReceive('get')->andReturn(new Response(200, [], '{"status":200,"result":false}'));
    });

    $response = $this->post(route('register'), [
        'email' => 'test@user.com',
        'email_confirmation' => 'test@user.com',
        'postcode' => $this->faker('en_GB')->postcode,
    ]);

    $response
        ->assertStatus(302)
        ->assertSessionHasErrors('postcode');
}