Defense Programming: Anticipating Failures with Tests

Published on by

Defense Programming: Anticipating Failures with Tests image

When you start working on a new feature, it is wise to plan out not only how it is expected to work, but what happens if something fails. Taking the time up front to anticipate failure is a quality of a great developer.

As a simple example, consider a blog that is populated by data from a third-party service. For instance, the home page of Laravel News pulls in jobs from LaraJobs. What happens if LaraJobs goes down? Or stops working?

Since we don’t know when a dependency might fail, it’s best to plan for failure by having tests so we can be more confident in failed states.

Laravel can help us write tests that plan for failure using real-time facades, but before we jump in, let’s create a fictional implementation for pulling in a list of articles to our site.

Let’s start with the following example of an ArticleRepository class with a Guzzle HTTP client as a dependency:

<?php
 
namespace AppRepositories;
 
use GuzzleHttpClient;
 
 
class ApiArticleRepository implements ArticleRepository
{
public function __construct(Client $client)
{
$this->client = $client;
}
 
public function get($id)
{
return $this->client->get('posts', ['query' => ['id' => $id]]);
}
}

If you’re not familiar with Guzzle:

Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and trivial to integrate with web services.

One approach we can take in our ApiArticleRepository class is to hide this implementation behind an interface that you can use to instantiate this class using Laravel’s service container:

<?php
 
namespace AppContracts;
 
interface ArticleRepository
{
public function get();
}

Now, let’s bind the implementation to the interface so the container can resolve this class every time we try to instantiate the interface using dependency injection in our controllers.

Binding an interface to our concrete implementation can be done in the AppProvidersAppServiceProvider class:

<?php
 
use GuzzleHttp/Client;
 
class RepositoryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('PostRepository', function () {
return new ApiPostRepository(new GuzzleClient ([
'base_uri' => config('api.url')
]);
});
}

And here’s an example of how you implement the repository in a controller:

<?php
 
namespace AppHttpControllers;
 
use AppRepositoriesApiArticleRepository as Repository;
 
class RegisterController extends Controller
{
protected $repository;
 
public function __construct(Repository $repository)
{
$this->repository = $repository;
}
 
public function view($id)
{
return view('artice.view', ['article' => $this->repository->get($id)]);
}
}

Testing Failure

With our ApiArticleRepository class in mind, we can start by thinking what would happen if the API responded with an exception instead.

What will happen when Guzzle tries to make a request and it fails?

Laravel has a concept called real-time facades that we can use to mock an exception and test how our code responds.

Using Real-time Facades

If you are unfamiliar with real-time facades, they can be defined as the following:

Facades provide a “static” interface to classes that are available in the application’s service container.

One of the advantages of working with facades is the fact you have access to a couple of methods that help you to create mocks of any class within your test environment.

How to instantiate a class using real-time facades?

The only thing you need to do is add the Facades prefix to the use statement like so:

<?php
 
use FacadesGuzzleHttpClient as GuzzleClient;

In this case, instead of mocking our ApiArticleRepository::class, we can go one layer back, and mock the GuzzleHttpClient::class instead. We can force each method of this class to return the desired response, and by doing so, we don’t need to change any implementation of the repository class.

The first step would be updating the ApiArticleRepository class to use a Facade instead dependency injection.

<?php
 
namespace AppRepositories;
 
use FacadesGuzzleHttpClient;
 
 
class ApiArticleRepository implements ArticleRepository
{
 
public function get($id)
{
return Client::get('posts', ['query' => ['id' => $id]]);
}
}

Mock a Guzzle Response

<?php
 
class ClientTest extends TestCase
/**
* @test
*/
public function testing_guzzle_exception()
{
FacadesGuzzleHttpClient::shouldReceive('get')->andThrow(
new GuzzleHttpExceptionRequestException(
"Error Communicating with Server",
new GuzzleHttpPsr7Request('GET', 'test')
)
);
 
$this->expectException(GuzzleHttpExceptionRequestException::class);
 
$repository = resolve('PostRepository');
$response = $repository->where(['limit' => 1]);
}
}

The results of this test:

$ phpunit --filter=testing_guzzle_exception
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.
 
. 1 / 1 (100%)
 
Time: 131 ms, Memory: 12.00MB
 
OK (1 test, 2 assertions)

Using the shouldReceive() method returns an instance of MockeryExpectation::class, so we can chain the method andThrown() to specify which exception is thrown each time the app tries to run the method get() on the GuzzleHttpClient instance.

The following line is an assertion itself, and it would return an error in our tests if the expected exception never gets fired

$this->expectException(GuzzleHttpExceptionRequestException::class);

When the ApiArticleRepository::get() try to access to the GuzzleHttpClient::get() method, instead of a successful response, the specified exception is going to be thrown.

With this approach, you’ll be able to do higher level tests like:

<?php
 
class ClientTest extends TestCase
/**
* @test
*/
public function testing_guzzle_exception()
{
FacadesGuzzleHttpClient::shouldReceive('get')->andThrow(
new GuzzleHttpExceptionRequestException(
"Error Communicating with Server",
new GuzzleHttpPsr7Request('GET', 'test')
)
);
 
$this->expectException(GuzzleHttpExceptionRequestException::class);
 
$response = $this->get('/');
$response->assertStatus(500);
}
}

In this case, our tests are going to pass as well.

$ phpunit --filter=testing_guzzle_exception
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.
 
. 1 / 1 (100%)
 
Time: 131 ms, Memory: 12.00MB
 
OK (1 test, 2 assertions)

Final thoughts

I think this is a relatively easy approach to test your application and anticipate failure when you need to use external services and third-party APIs with guzzle.

Also, if you are just starting to work with tests, believe me, you are going to find this so much easy rather than trying to create mocks, stubs, using doubles, etc.

That’s all, now you are ready to write an HTTP client implementation backed by tests that responds to external API failure.

Jeff photo

I'm a full-stack web developer and a part-time writer.

You can find more of my writing on https://medium.com/@jeffochoa.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Visit Laravel Forge
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
All Green logo

All Green

All Green is a SaaS test runner that can execute your whole Laravel test suite in mere seconds so that you don't get blocked – you get feedback almost instantly and you can deploy to production very quickly.

All Green
Larafast: Laravel SaaS Starter Kit logo

Larafast: Laravel SaaS Starter Kit

Larafast is a Laravel SaaS Starter Kit with ready-to-go features for Payments, Auth, Admin, Blog, SEO, and beautiful themes. Available with VILT and TALL stacks.

Larafast: Laravel SaaS Starter Kit
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector

The latest

View all →
Property Hooks Get Closer to Becoming a Reality in PHP 8.4 image

Property Hooks Get Closer to Becoming a Reality in PHP 8.4

Read article
Asserting Exceptions in Laravel Tests image

Asserting Exceptions in Laravel Tests

Read article
Reversible Form Prompts and a New Exceptions Facade in Laravel 11.4 image

Reversible Form Prompts and a New Exceptions Facade in Laravel 11.4

Read article
Basset is an alternative way to load CSS & JS assets image

Basset is an alternative way to load CSS & JS assets

Read article
Integrate Laravel with Stripe Connect Using This Package image

Integrate Laravel with Stripe Connect Using This Package

Read article
The Random package generates cryptographically secure random values image

The Random package generates cryptographically secure random values

Read article