Contract Testing

Contract Testing

Getting things set up and rolling at my new job, the test setup and how to do some things was something I was especially interested in. Due to past experience with testing, I was asked to put thoughts, ideas and techniques together to help us kick things off 'right'. I brought up the subject of contract testing and figured I'd write up my thoughts and notes on it.

Pre-post information: this uses the term fake / double to essentially mean something that's used in tests in place of a real implementation of something - that's it in a nutshell. If you want to read more about that, the article Understanding Test Doubles (Mock vs Stub) is a good read.

Now that you know what a test double is, let's move on.

Intro

I was introduced to contract testing by Ciaran McNulty. Once he talked me through it, I really liked how it was solving the problem it set out to address.

I'll start with the problem it tries to solve:

  • Fake/double implementations are fast (yay!), but lower confidence that they actually work with the real thing (boo!)
  • Real implementations are slow (boo!) or even unreliable* (double-boo!), but have high confidence that they actually work with the real thing (yay!)

We can increase confidence in fake infrastructure by ensuring that it does what the real infrastructure does. To do this, we can test the contract (interface) that the two share!

We can then continue to use the fake infrastructure in our tests to keep them fast, but ensure the test doubles continue to match the interface and real implementations by a contract test, thus increasing confidence in them.

This type of testing is also useful if you're switching implementations over from one to another. You can write tests against the existing one that you know works, then create the other implementation that will be tested against the existing one

What we're testing

We create a contract (interface) that all implementations can adhere to.

interface BlogPostRepository
{
    public function findBySlug(Slug $slug): BlogPost;
}

In this example we take a slug, and return the blog post from that slug. (Assuming we also have Slug and BlogPost classes elsewhere)

In our tests, we have a test double that we're using. It's an in-memory implementation of the repository, and we're looking to ensure that it's working correctly and matches our real implementation that uses a database.

(remember: Fake = fast, low confidence. Real = slow, high confidence)

class InMemoryBlogPostRepository implements BlogPostRepository
{
    public function findBySlug(Slug $slug): BlogPost
    {
        // return blog post from an in-memory array
    }
}

And we also have our real repository, in this case it's talking to a database, but it could be any other external service (API etc).

class MySQLBlogPostRepository implements BlogPostRepository
{
    /** @var PDO */
    private $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function findBySlug(Slug $slug): BlogPost
    {
        // return blog post from an actual database
    }
}

The contract test

Setup an abstract base test class, this has the actual test-cases in it, testing against the contract the implementations share.

The abstract test is there to ensure the test cases are written, but cannot be run unless an implementation has a test case where the setup and teardown is handled.

abstract class BlogPostRepositoryContractTest extends TestCase
{
    /** @var BlogPostRepository */
    protected $repository;

    /** @test */
    public function it_retrieves_a_post_by_slug(): void
    {
        $post = $this->repository->findBySlug(new Slug('my-awesome-post'));

        self::assertEquals('my-awesome-post', (string) $post->slug());
    }

    abstract function setUp();

    abstract function tearDown();
}

We now introduce a test case for each of the implementations, these test cases just handle the setup of the implementation they're for and extend our base test case that has the tests within it.

class InMemoryBlogPostRepositoryContractTest extends BlogPostRepositoryContractTest
{
    public function setUp()
    {
        $this->repository = new InMemoryBlogPostRepository;
    }

    public function tearDown()
    {
        // do nothing
    }
}
class MySQLBlogPostRepositoryContractTest extends BlogPostRepositoryContractTest
{
    public function setUp()
    {
        $pdo = new PDO('dsn', 'user', 'pass');
        $this->repository = new MySQLBlogPostRepository($pdo);
    }

    public function tearDown()
    {
        // reset the DB or whatever you need to do
    }
}

For those of you that might like UML, it's setup a bit like this:

Benefits

With this, we can raise the confidence that our test doubles / fake implementations match our real ones.

You don't have to run the contract tests on every test run (that's semi-personal preference, do whatever suits you / your team / your workflow best), but run them periodically to ensure things still work.

If you do want to run them separately, consider organising them into a different suite, so you can run them whenever suits you best. Different tools have different ways of achieving this.

TL;DR

To quote my team lead on this when I told the team about it:

"So it’s a way to run the same tests on different implementations of the same interface?"

Anthony Chambers

Pretty much! πŸ‘πŸ»

Further reading

Footnotes

  • Hero image by rawpixel on Unsplash
  • * Unreliable in the sense that if it's a 3rd party API, can you trust that they're always up and online?