Testing Contracts for Services

There is a growing use of services and micro-services to develop applications. I’ve had numerous questions about how to test the services and who should be responsible for what testing. I wrote a book called Interface-Oriented Design which covered many of these issues. Here’s a summary and example adapted from that book.

Introduction

Let’s start with terminology. There is a producer who supplies the service and a consumer who uses the service. Often there are multiple consumers. There is an interface to the service which gives the details of the operations the service performs along with the inputs and outputs (including potential error outputs).

Let’s suppose the service returns the account balance for a particular customer. Its overall contract could be expressed in Given/When/Then form as examples:

Scenario: Account found
Given input is:
Account ID
123
When service called Then output is:
Account IDBalance
123$1000
Scenario: Account not found Given input is:
Account ID
567
When service called and account is not found Then output is:
Account IDError
567Account Not Found
Scenario: Service not available Given input is:
Account ID
789
When service called and it cannot perform due to missing dependencies Then output is:
Account IDError
789Service Not Available

Note that the actual format of the service (REST, web, etc.) is not exposed in these scenarios.

Acceptance Tests

With business facing requirements, the triad (customer, tester, and developer) collaborate on developing acceptance tests. With a service, the consumer is the customer and the producer is the developer. This triad should create acceptance tests for the service. If there are multiple consumers, they work as the customer unit. A “product manager” might be appointed who represents all the consumers and balances any conflicting requirements.

The acceptance tests might appear very similar to the example given above. They would be implemented using whichever service format was agreed upon by the consumer and producer. Since services tend have a scope greater than a single consumer, the format will probably be dictated by an enterprise architect. That way service tools and testing will be consistent among producers.

The producer uses these acceptance tests to drive the development of the service. The producer ensures that the actual services performs properly with any other services that it depends upon, such as an enterprise database or external third party service.

Test Double (Mock)

The consumer of the service needs to be able to run automated tests against their application. In order to do so, they need to have test doubles for services that return values that are either constant or can be controlled by the consumer’s test.

The responsibility for creating the test double should be the producer’s. When other consumers want to utilize that service, the test double is easily available. The setup of the test double is the responsibility of the consumer. This setup can occur via configuration files or programmatic calls to the test double. Here’s an example of a setup that corresponds the previous example:

Test Double Setup 
InputOutputOutput
Account IDBalanceError
123$1000
567 Account Not Found
789 Service Not Available

The consumer tests use these different values to test the application’s response to return values and errors.

More details on how to create a mock/test double are at https://kenpugh.com/blog/?p=33

Encapsulating the Interface

Consumer applications should not usually change if the format of the service changes, only the semantics of the interface. To accomplish this, one typically creates a programmatic interface that encapsulates the service format. This is should be the responsibility of the producer, as the same interface will be used by multiple consumers.

For the example, the programmatic interface might look like:

class AccountData {
    AccountID accountID;
    Dollar balance; 
    }

 enum AccountServiceExceptionValues {
     AccountNotFound, ServiceNotAvailable};

 class AccountServiceException extends Exception
    {
    AccountServiceExeceptionValues value; 
    } 

 public interface GetAccountDataService {
    public AccountData getDataforID(AccountID id) 
        throws AccountServiceExecption; 
    ]

Changing the Interface

Interfaces are not always static. They often need to change to accommodate new requirements. If a new requirement requires additional data, that usually can be handled easily. Older applications which do not require that old data simply ignore it. However, changes to an interface which changes its paradigm will break existing consumers.

The impact to the consumers of an interface change can be relatively measured by whether an existing acceptance test has to change. If it does, then there will be an impact. Producers should be running consumer acceptance tests every time the service is changed. A broken acceptance test is a time to meet back as a triad and discuss the issues prior to deploying the changed service.