Building a SparkPost Client: TDD with PhpUnit and Mockery

Share this article

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!

Home page screenshot

You can find the code for this post on Github.

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:

  1. 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.
  2. We tell PHPUnit to load all files ending in Test.php, in the tests 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.
  3. 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:

New phpunit screen

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:

  1. We don’t want to repeat https://api.sparkpost.com/api/v1 for every request. We could accept it as a constructor argument.
  2. 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.
  3. 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:

  1. We call $client->createTransmission(...).
  2. The client formats those parameters according to how SparkPost wants them.
  3. The client sends an authenticated request to SparkPost.
  4. 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:

Invalid return type error

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.

Passing tests

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.

Coverage report

Things To Consider

  1. We didn’t validate the parameters for createTransmission. That would be a good thing to do.

  2. 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.

  3. 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 PittChristopher Pitt
View Author

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.

BrunoSguzzleguzzlephpmockerymockingmocksOOPHPPHPphpunitsparkposttddTestingunit testing
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form