In a previous post, we looked at SparkPost (as an alternative to Mandrill), and explored a bit of the official PHP client. The official client handles a decent amount of work, but I got to thinking about what it would take to build a new client.
The more I thought about it, the more it made sense. I could learn about the SparkPost API, and practice Test Driven Development at the same time. So, in this post we’ll look to do just that!
You can find the code for this post on Github.
Key Takeaways
- Building a SparkPost client involves learning about the SparkPost API and practicing Test Driven Development (TDD) using PHPUnit and Mockery.
- The process begins with installing Guzzle to make requests to the SparkPost API, and installing PHPUnit and Mockery for writing tests. A configuration file for PHPUnit is also required.
- Test Driven Development encourages creating interfaces that are minimalistic and user-friendly. The first test involves sending an email through the SparkPost API using a POST request.
- The creation of the client interface involves defining requirements for the public interface, such as not repeating the API URL for every request and not knowing the specific request methods or endpoints.
- The final step involves implementing the interface and running PHPUnit to check for errors. Once the client class is created and the requests are forwarded to SparkPost, the tests should pass.
Getting Started
To begin, we’re going to need Guzzle to make requests to the SparkPost API. We can install it with:
composer require guzzlehttp/guzzle
In addition, we’re going to be writing tests early, so we should also install PHPUnit and Mockery:
composer require --dev phpunit/phpunit mockery/mockery
Before we can run PHPUnit, we need to create a configuration file:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="false"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false">
<testsuites>
<testsuite>
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
</phpunit>
This configuration file handles a number of things:
- Many of the root node attributes are sensible, intuitive defaults. The one I want to draw particular attention to is
bootstrap
: which tells PHPUnit to load Composer’s autoload code. - We tell PHPUnit to load all files ending in
Test.php
, in thetests
folder. It will treat all files with this suffix as though they are class files with a single class each. If it can’t instantiate any of the classes it finds (like abstract classes) then it will just ignore those. - We tell PHPUnit to add all PHP files (from the
src
folder) to code coverage reporting. If you’re unsure what that is, don’t worry. We’ll look at it in a bit…
We can now run:
vendor/bin/phpunit
… and we should see:
Designing The Interface
One of the things I love most about Test Driven Development is how it pushes me towards interfaces that are minimalistic and friendly. I start as the consumer, and end up with an implementation that is the easiest for me to use.
Let’s make our first test. The simplest thing we can do is send an email through the SparkPost API. If you check out the docs, you’ll find that this happens through a POST
request to https://api.sparkpost.com/api/v1/transmissions
, with a JSON body and some key headers. We can simulate this with the following code:
require "vendor/autoload.php";
$client = new GuzzleHttp\Client();
$method = "POST";
$endpoint = "https://api.sparkpost.com/api/v1/transmissions";
$config = require "config.php";
$response = $client->request($method, $endpoint, [
"headers" => [
"Content-type" => "application/json",
"Authorization" => $config["key"],
],
"body" => json_encode([
"recipients" => [
[
"address" => [
"name" => "Christopher",
"email" => "cgpitt@gmail.com",
],
],
],
"content" => [
"from" => "sandbox@sparkpostbox.com",
"subject" => "The email subject",
"html" => "The email <strong>content</strong>",
],
]),
]);
You can use the same from/recipients details as in the previous post.
This assumes you have a config.php
file, which stores your SparkPost API key:
return [
"key" => "[your SparkPost API key here]",
];
Be sure to add this file to .gitignore
so you don’t accidentally commit your SparkPost API key to Github.
Given the rough details of implementation, we can start to make some assumptions (or define some requirements) for the public interface:
- We don’t want to repeat
https://api.sparkpost.com/api/v1
for every request. We could accept it as a constructor argument. - We don’t want to know the specific request methods or endpoints. The client should work that stuff out and adjust itself for new versions of the API.
- We may want to define some or all of the endpoint parameters, but there should be some sensible defaults and a way for the client to rearrange them.
Starting With Tests
Since we want to use Mockery, it would be a good idea for us to make a central, base test class, to close the mocks:
namespace SparkPost\Api\Test;
use Mockery;
use PHPUnit_Framework_TestCase;
abstract class AbstractTest extends PHPUnit_Framework_TestCase
{
/**
* @inheritdoc
*/
public function tearDown()
{
Mockery::close();
}
}
We can use this as the base class for our other tests, and PHPUnit won’t try to instantiate it since it is abstract. The underlying reasons for closing Mockery are a bit out of scope for this post. It’s just something you need to know to do with Mockery (and similar libraries).
Now we can begin to define what we want the client interface to look like:
namespace SparkPost\Api\Test;
use Mockery;
use Mockery\MockInterface;
use GuzzleHttp\Client as GuzzleClient;
class ClientTest extends AbstractTest
{
/**
* @test
*/
public function itCreatesTransmissions()
{
/** @var MockInterface|GuzzleClient $mock */
$mock = Mockery::mock(GuzzleClient::class);
// ...what do we want to test here?
}
}
The idea behind Mockery is that it allows us to simulate classes we don’t want to test, that are required by those we do want to test. In this case, we don’t want to write tests for Guzzle (or even make real HTTP requests with it) so we create a mock of the Guzzle\Client class. We can tell the mock to expect a call to the request
method, with a request method string, an endpoint string and an array of options:
$mock
->shouldReceive("request")
->with(
Mockery::type("string"),
Mockery::type("string"),
$sendThroughGuzzle
)
->andReturn($mock);
We don’t really care which request method is sent to Guzzle, or which endpoint. Those might change as the SparkPost API changes, but the important thing is that strings are sent for these arguments. What we are really interested in is seeing is whether the parameters we send to the client are formatted correctly.
Let’s say we wanted to test the following data:
$sendThroughGuzzle = [
"headers" => [
"Content-type" => "application/json",
"Authorization" => "[fake SparkPost API key here]",
],
"body" => json_encode([
"recipients" => [
[
"address" => [
"name" => "Christopher",
"email" => "cgpitt@gmail.com",
],
],
],
"content" => [
"from" => "sandbox@sparkpostbox.com",
"subject" => "The email subject",
"html" => "The email <strong>content</strong>",
],
]),
];
…but we only wanted that data to go through if we called the following method:
$client = new SparkPost\Api\Client(
$mock, "[fake SparkPost API key here]"
);
$client->createTransmission([
"recipients" => [
[
"address" => [
"name" => "Christopher",
"email" => "cgpitt@gmail.com",
],
],
],
"subject" => "The email subject",
"html" => "The email <strong>content</strong>",
]);
Then we would probably need to make the rest of the test work with this structure in mind. Let’s assume we’re only interested in the json_decode
data returned from SparkPost…
We could imagine using the client like:
- We call
$client->createTransmission(...)
. - The client formats those parameters according to how SparkPost wants them.
- The client sends an authenticated request to SparkPost.
- The response is sent through
json_decode
, and just gets an array of response data.
We can build these steps into the test:
public function itCreatesTransmissions()
{
/** @var MockInterface|GuzzleClient $mock */
$mock = Mockery::mock(GuzzleClient::class);
$sendThroughGuzzle = [
"headers" => [
"Content-type" => "application/json",
"Authorization" => "[fake SparkPost API key here]",
],
"body" => json_encode([
"recipients" => [
[
"address" => [
"name" => "Christopher",
"email" => "cgpitt@gmail.com",
],
],
],
"content" => [
"from" => "sandbox@sparkpostbox.com",
"subject" => "The email subject",
"html" => "The email <strong>content</strong>",
],
]),
];
$mock
->shouldReceive("request")
->with(
Mockery::type("string"),
Mockery::type("string"),
$sendThroughGuzzle
)
->andReturn($mock);
$mock
->shouldReceive("getBody")
->andReturn(
json_encode(["foo" => "bar"])
);
$client = new Client(
$mock, "[fake SparkPost API key here]"
);
$this->assertEquals(
["foo" => "bar"],
$client->createTransmission([
"recipients" => [
[
"address" => [
"name" => "Christopher",
"email" => "cgpitt@gmail.com",
],
],
],
"subject" => "The email subject",
"html" => "The email <strong>content</strong>",
])
);
}
Before we can run PHPUnit to see this test in action, we need to add the autoload directives for our library, to composer.json
:
"autoload": {
"psr-4": {
"SparkPost\\Api\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"SparkPost\\Api\\Test\\": "tests"
}
}
You’ll probably also have to run composer du
to get these to work…
Implementing The Interface
The moment we run PHPUnit, we’ll see all sorts of errors. We’ve not yet made the classes, let alone implemented their behavior. Let’s make the Client
class:
<?php
namespace SparkPost\Api;
use GuzzleHttp\Client as GuzzleClient;
class Client
{
/**
* @var string
*/
private $base = "https://api.sparkpost.com/api/v1";
/**
* @var GuzzleClient
*/
private $client;
/**
* @var string
*/
private $key;
/**
* @param GuzzleClient $client
* @param string $key
*/
public function __construct(GuzzleClient $client, $key)
{
$this->client = $client;
$this->key = $key;
}
/**
* @param array $options
*
* @return array
*/
public function createTransmission(array $options = [])
{
$options = array_replace_recursive([
"from" => "sandbox@sparkpostbox.com",
], $options);
$send = [
"recipients" => $options["recipients"],
"content" => [
"from" => $options["from"],
"subject" => $options["subject"],
"html" => $options["html"],
]
];
return $this->request("POST", "transmissions", $send);
}
/**
* @param string $method
* @param string $endpoint
* @param array $options
*
* @return array
*/
private function request(
$method, $endpoint, array $options = []
)
{
// ...time to forward the request
}
}
We’re making progress! When we run PHPUnit we should see something like:
Let’s finish up by forwarding the requests to SparkPost:
private function request(
$method, $endpoint, array $options = []
)
{
$endpoint = $this->base . "/" . $endpoint;
$response = $this->client->request($method, endpoint, [
"headers" => [
"Content-type" => "application/json",
"Authorization" => $this->key,
],
"body" => json_encode($options),
]);
return json_decode($response->getBody(), true);
}
With this, the tests will be passing.
It’s perhaps also helpful for us to see how this can be consumed without any mocking:
require "vendor/autoload.php";
$config = require "config.php";
$client = new SparkPost\Api\Client(
new GuzzleHttp\Client(), $config["key"]
);
$reponse = $client->createTransmission([
"recipients" => [
[
"address" => [
"name" => "Christopher",
"email" => "cgpitt@gmail.com",
],
],
],
"subject" => "The email subject",
"html" => "The email <strong>content</strong>",
]);
Coverage
Remember I mentioned code coverage reporting? We can run PHPUnit in the following way:
vendor/bin/phpunit --coverage-html coverage
This will generate a folder of HTML files. You can open coverage/index.html
to get an idea of how much of your library is covered by your tests. You’ll need to have the XDebug extension installed, and running larger tests suites with this parameter may be slower than without.
Things To Consider
We didn’t validate the parameters for
createTransmission
. That would be a good thing to do.We’re very tightly coupled to Guzzle. I think that’s ok, but if you have reservations, rather create a “client” interface and a Guzzle adapter, or even better, take the Httplug approach.
There is a lot more to the SparkPost API, and there are some interesting paths you can take when designing such an interface. Here’s a recent screencast I did which explores an expressive syntax on top of the client we’ve built today…
How are you finding SparkPost? Do you like this sort of development flow? Let us know in the comments below.
Frequently Asked Questions (FAQs) about Building a SparkPost Client with PHPUnit and Mockery
What is the significance of Test-Driven Development (TDD) in building a SparkPost client?
Test-Driven Development (TDD) is a software development approach where tests are written before the actual code. In the context of building a SparkPost client, TDD ensures that the code functions as expected. It allows developers to consider all possible scenarios and edge cases, leading to robust and error-free software. TDD also makes the code more maintainable and easier to refactor, as the tests provide a safety net against potential bugs introduced during changes.
How does PHPUnit aid in TDD for building a SparkPost client?
PHPUnit is a testing framework for PHP that provides a structured way to write and run tests. When building a SparkPost client, PHPUnit can be used to write unit tests for individual components of the client. These tests ensure that each component, or “unit,” of the client functions correctly in isolation. PHPUnit also provides various assertions to verify the behavior of the code, making it an essential tool for TDD.
What role does Mockery play in testing a SparkPost client?
Mockery is a PHP mock object framework used in unit testing. When testing a SparkPost client, Mockery can be used to create “mock” versions of external dependencies, such as the SparkPost API. This allows developers to isolate the code they are testing and control the behavior of external dependencies. By using Mockery, developers can simulate various scenarios and edge cases that might be difficult to reproduce with real dependencies.
How can I handle errors and exceptions when building a SparkPost client?
Error handling is a crucial aspect of building a SparkPost client. PHP provides several mechanisms for error handling, including error reporting, exception handling, and custom error handlers. When interacting with the SparkPost API, it’s important to handle potential errors such as network failures, API rate limits, and invalid responses. This can be done by wrapping API calls in try-catch blocks and handling exceptions appropriately.
How can I ensure that my SparkPost client is secure?
Security is a critical concern when building a SparkPost client, as it often involves handling sensitive data such as API keys. To ensure security, it’s important to follow best practices such as storing sensitive data securely, validating and sanitizing input data, and using secure communication protocols. Additionally, regular code reviews and security audits can help identify and fix potential security vulnerabilities.
How can I optimize the performance of my SparkPost client?
Performance optimization is an important aspect of building a SparkPost client. This can involve various strategies, such as minimizing the number of API calls, using efficient data structures and algorithms, and leveraging caching where appropriate. Additionally, performance profiling tools can be used to identify bottlenecks and optimize the code accordingly.
How can I make my SparkPost client scalable?
Scalability is a key consideration when building a SparkPost client, especially for applications with a large number of users or high email volumes. Scalability can be achieved by designing the client to handle increasing workloads efficiently, for example by using asynchronous processing, load balancing, and horizontal scaling strategies.
How can I integrate my SparkPost client with other systems?
Integration with other systems is often a requirement when building a SparkPost client. This can be achieved through various methods, such as APIs, webhooks, and middleware. It’s important to design the client with flexibility and extensibility in mind, to accommodate different integration requirements.
How can I maintain and update my SparkPost client?
Maintenance and updates are an ongoing part of the lifecycle of a SparkPost client. This involves fixing bugs, adding new features, and keeping up with changes in the SparkPost API. Good practices such as version control, automated testing, and continuous integration can help streamline the maintenance and update process.
How can I get support and community help for building a SparkPost client?
There are various resources available for getting support and community help when building a SparkPost client. These include the official SparkPost documentation, online forums and communities, and open-source projects on platforms like GitHub. Additionally, SparkPost offers support plans for customers who need dedicated assistance.
Christopher is a writer and coder, working at Over. He usually works on application architecture, though sometimes you'll find him building compilers or robots.