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.

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

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

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
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant 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
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
Rector logo

Rector

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

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Asymmetric Property Visibility in PHP 8.4 image

Asymmetric Property Visibility in PHP 8.4

Read article
Access Laravel Pulse Data as a JSON API image

Access Laravel Pulse Data as a JSON API

Read article
Laravel Forge adds Statamic Integration image

Laravel Forge adds Statamic Integration

Read article
Transform Data into Type-safe DTOs with this PHP Package image

Transform Data into Type-safe DTOs with this PHP Package

Read article
PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell image

PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell

Read article
Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3 image

Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3

Read article