The test client pattern
Testing is a central activity in software development: due to its “softness”, software changes very frequently and, particularly in large systems, this rate of change can cause previously correct behavior to change; this type of bug is commonly called a regression.
As code complexity rises quite quickly with the size of the code base, it becomes impossible to check for regressions using manual testing procedures and, in practice, any professional software development effort will employ automated testing; automating tests is of such importance that, in many projects, the ratio of production to test code is almost 1:1.
This makes the use of patterns for managing test code important, and one such pattern that we’ve found useful for testing API (Application Programming Interface) based components is implementing a test client.
What is a test client?
A test client is an entire client for your API that is written for the purpose of implementing tests.
In this context, an API is an interface that is accessed over a network (using a protocol such as HTTP); one of the goals of the test client is to abstract that protocol’s details in the same way they would be if you were writing an entire client application.
What are the benefits of writing a test client?
Improved DX (Developer Experience)
The most common complaint heard by someone consuming an API is that it is difficult to use.
This usually happens because:
Operations/resources have a plethora of arguments/options;
There are many seemingly identical operations;
The sequence of operations to perform a higher level action is complex.
By creating a separate code base, you are, in effect, replicating the DX of whoever needs to integrate with your API; this means that any awkwardness in the API design becomes much more obvious/painful to the team designing and implementing the API.
More readable test code
Unfortunately, while most engineers are motivated to produce Clean Code (meaning readable, intent-revealing code) for production, test code is often exempted from this attention to detail, often resulting in tangled, brittle tests.
The following is a too common example of implementing a test:
By implementing a test client, you would instead write something like:
This makes the code much easier to understand and maintain.
What are the disadvantages of implementing a test client?
Duplicated effort
By design, the test client is an abstraction which requires effort to build and maintain; while this effort will usually be small, it needs to be taken into account when planning.
On the other hand, if the effort required to build the test client is significant, this could indicate a complex API design that should be given more attention.
How should I implement my test client?
Avoid code sharing
Ideally, a test client implementation will use a different programming language than the one used to implement the API; for example, if the API is implemented using Java, the client could be implemented in JavaScript or Python.
The benefit of this is that it avoids too much coupling of the API design to the affordances of a particular programming language or stack.
In particular, it makes it almost impossible to share production code with the test client, forcing API designers to face their design decisions; we’ve seen extreme cases in which, by sharing Data Transfer Objects between the application and test code, developers were oblivious to what the JSON payload really looked at the wire level (and yes, it looked nasty).
Use a code generator, if possible
While we find the usefulness of tools like OpenAPI debatable when used strictly for specification or documentation purposes, their ability to generate client stubs from the spec can yield significant productivity gains.
Since the biggest con of implementing a test client is increased development effort, using a code generator greatly alleviates this and makes the pattern much more worthwhile.
Publish the client
If the API is public (i.e. meant to be consumed by external parties), you can publish the test client as an artifact, to make it easier for others to integrate with your API; some examples of this are the API clients for cloud services like Google Cloud Platform or Amazon Web Services.
Even if the API isn’t public and is only consumed by a single application (usually a Web Frontend), it’s still possible (and beneficial) to publish the client, so it can be used not only by the tests but also the actual client.
Summary
In this article, we’ve described the test client pattern; this pattern makes the friction in DX more evident to API designers, thus incentivizing them to improve the design.
Is this pattern one that would make sense for you? Let us know in the comments!



