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!
Discussion
Thanks for this article Matt! Just what I needed right now.