loading...

Testing external services with fake classes in Laravel

mattkingshott profile image Matt Kingshott 👨🏻‍💻 Originally published at itnext.io on ・4 min read

This article is part of a series where I document insights, changes and rethinking that I experienced while refactoring the codebase for Pulse — a painless and affordable site & server monitoring tool designed for developers.

Today, I’d like to talk about how I created fake classes to test functionality that depends on making requests over the internet. Let’s dig in…

Why create fake classes?

Generally speaking, if you can avoid creating a fake class, I would absolutely recommend that you do so. It’s the same argument that exists for being wary about using mocks and spies in your code… you’re not really testing your code, so much as testing your code syntax / interaction.

Okay, if that’s so, why would I want to use them?

The answer to that is simple. Performance. Consider the following example, you write a test that makes an HTTP request to a third party e.g. acme.com You receive the response and your code reacts accordingly.

Fine and dandy… but what if the response was not what you expected? What if the site was slow? What if the site was offline? What if your internet connection wasn’t working properly?

Even if you were able to mitigate those problems every time you ran the test, there’s still one thing you can’t address and that’s the latency involved in sending the HTTP request. Your test has to wait for the response.

In the case of a single test, it’s not a big deal, but imagine if your test suite had hundreds or even thousands of tests that involved HTTP requests. You don’t want to be in a position where it takes an hour to run your test suite.

Fake classes to the rescue

The easy way to solve this problem is to wrap the functionality that sends an HTTP request within a class. Then within your app, you resolve this class out of Laravel’s service container and call its methods.

For your tests, you create a separate class that inherits from this main class. You then override the methods to perform the same functionality but without actually sending the HTTP request.

Finally, within your tests, you instruct Laravel’s service container to swap the main class with your fake class. That way, when your application requests an instance of the main class, it gets the fake class instead.

If you’re new to this, it can be a little overwhelming, so let’s make things simpler by taking a look at an example from Pulse.

Sending an HTTP request

As part of its monitoring activities, Pulse makes an HTTP request to a user’s site and checks the HTTP status code that it receives back. In order to do this, the application calls the getStatusCode method on a class called PingSite.

Here’s a simplified example of the class:

class PingSite
{

    /\*\*
     \* Send an HTTP request to a url and retrieve its status code.
     \*
     \*/
    public function getStatusCode(string $url) : int
    {
        return $client->request($url)->statusCode;
    }

}

Then, within the application, instead of simply creating a new instance of the PingSite class, we resolve the class out of the service container:

// NO
$ping = new PingSite();

// YES
$ping = resolve(PingSite::class);

Aside from how we instantiate the PingSite class, everything remains the same. It still returns an object of type PingSite.

Creating the fake implementation

For our tests, we need to create a fake class that overrides the getStatusCode method and instead simply returns an integer. However, we need a way to set the integer that is returned so that we can test for a range of scenarios.

Let’s add a property to our fake class and set its value in the constructor. We can then modify the getStatusCode method to return the property’s value:

class PingSiteFake extends PingSite
{

    protected $code;

    public function __construct(int $code)
    {
        $this->code = $code;
    }

    /\*\*
     \* Simulate an HTTP request to a url and get its status code.
     \*
     \*/
    public function getStatusCode(string $url) : int
    {
        return $this->code;
    }

}

Finally, all we have to do within our tests, is instruct Laravel to use our fake class when resolving PingSite from the service container:

/\*\* @test \*/
public function it_can_ping_a_url_and_get_the_http_status_code()
{
    // Use the fake implementation
    $this->swap(PingSite::class, new PingSiteFake(201));

    $this->get('/ping/google.com')
         ->assertJson(['status' => 201]);
}

And that’s all there is to it. This test will now run in a couple of milliseconds, as opposed to a second or longer if we actually sent a genuine HTTP request.

Caveats to be aware of

As mentioned at the beginning, the argument about being careful with this approach still applies. As you’ve probably realised, we’re not actually testing PingSite, we’re instead testing the code that interacts with PingSite.

It’s still incredibly important to write tests that do actually utilise PingSite and not its fake implementation, in order to ensure that it functions correctly. However, instead of writing dozens, hundreds or even thousands of tests which are slow because they all use PingSite, you instead only need to write a handful that specifically test the functionality of the PingSite class.

This will ensure that your test suite runs significantly faster.

Wrapping Up

Hopefully you’ve seen how using fake classes can be significantly beneficial in improving the performance of your test suite. I have more articles to share, so if you’re interested in reading them, be sure to follow me here on Medium. You can also follow me on Twitter.

Lastly, if you’re in the market for an affordable and painless site & server monitoring tool that doesn’t require you to have a DevOps degree, please take a moment to check out Pulse. I think you’ll find it to a be a breath of fresh air!

Thanks again and happy coding!


Posted on Apr 8 '19 by:

mattkingshott profile

Matt Kingshott 👨🏻‍💻

@mattkingshott

Founder. Developer. Writer. Lunatic. Created Pulse, IodineJS, Axiom, and more. #PHP #Laravel #Vue #TailwindCSS

Discussion

markdown guide
 

Thanks for this article Matt! Just what I needed right now.