Adding API Versioning to your Environment-Agnostic Functional Acceptance Tests

Featured image for sharing metadata for article

API versioning is very important, because there are going to be cases where you need to change your API, but don't want to break all your consumers, of which my preference is using content negotiation.

Regardless of how you do it, you're going to have a slightly different API to connect to the service - be that through a different URL, accept header, or some other means.

In Writing Environment-Agnostic Functional Acceptance Tests, I spoke about a way to structure your Cucumber tests to treat them more like the code you'd write in your production application, and I've found it to be a really nice pattern for working with tests.

However, at the time I didn't consider what this would look like for a versioned API.

Scenarios

Let's start by thinking about our scenarios, i.e. with features/product.feature:

Feature: Happy path product data
  Given something
  When I retrieve the product information
  Then I get an OK response

This is great, and doesn't include anything related to versioning. However, given this is something quite key to the functionality of the service, we probably should have something.

We could write something like the below, which embeds that into the step definitions:

Feature: The Product Service returns some product data

  Scenario Outline: Happy path product data
    Given something
    # I don't recommend this! Not super readable for not-as-technical folks
    When I retrieve the product information, using the <version> contract
    Then I get an OK response

  Examples:

    | version   |
    | version 1 |
    | version 2 |

However, I don't like this, and feel it doesn't provide the value that someone reading the feature files needs.

I'd recommend splitting out our features into i.e. features/product/v1.feature, and add a @v1 tag to the feature file, but otherwise have nothing version related in the steps:

@v1
Feature: The Product Service returns some product data, using different representations for consumers. This tests version 1.

  Scenario: Happy path product data
    Given something
    When I retrieve the product information
    Then I get an OK response

This gives us clear language for our steps, which I find to be pretty good.

We can then extend this for any number of API versions, as well as capturing the fact that a consumer could request any version i.e. features/product/any-version.feature:

@any-version
Feature: The Product Service returns some product data, using different representations for consumers. This tests the case where consumers may not pin to a version.

  Scenario: Happy path product data
    Given something
    When I retrieve the product information
    Then I get an OK response

Cucumber Hooks

To make this work, we should have some Cucumber hooks to set the current ApiVersion in our World:

// world is an instance of state being shared across a scenario

@Before("@any-version")
public void anyVersionTag() {
  world.setVersion(ApiVersion.ANY);
}

@Before("@v1")
public void version1Tag() {
  world.setVersion(ApiVersion.ONE);
}

@Before("@v2")
public void version2Tag() {
  world.setVersion(ApiVersion.TWO);
}

The ApiVersion

This API version is then a handy enum to capture the versions in a typed manner

/**
 * A generic way of determining the version of an API's request/response contract for use with the
 * Proxy class, without relying too much on implementation details.
 *
 * <p>This enum should be left with no implementation details, allowing our Proxy class to
 * implement this. Please see below for a further explanation of why.
 */
public enum ApiVersion {
  ANY, ONE, TWO;
}

The Proxy class

And then we have our proxy class updated with the new parameter on methods that support API versioning:

public class ProductServiceProxy {

  // ...

  /**
   * Retrieve a product by its identifier.
   *
   * @param productId the product identifier
   * @param version the version of the API resource to retrieve
   * @param filters any {@link Filter}s to apply to the request
   * @return the response from the server
   */
  public Response retrieveProduct(String productId, ApiVersion version, Filter... filters) {
    String mediaType = null;
    switch(version) {
      case ANY:
        // may not work if the contract changes (i.e. new headers required)
        mediaType = "application/*.json";
      case ONE:
        if (mediaType != null) {
          mediaType = "application/vnd.me.jvt.api.v1+json";
        }
        return prepare(filters)
          .header("Accept", mediaType)
          .header("Tracking-Id", UUID.randomUUID().toString())
          .basePath("/products/" + productId)
          .get();
      case TWO:
        return prepare(filters)
          .header("Accept", "application/vnd.me.jvt.api.v2+json")
          .header("Tracking-Id", UUID.randomUUID().toString())
          .header("Another-Header", "some value")
          .basePath("/products/" + productId)
          .get();
      default:
        throw new IllegalArgumentException("Version " + version + " is not supported");
    }
  }

  // ...
}

This now gives us a way to interact with our versioned API, assert things based on that version, and generally work a bit nicer with our Cucumber tests.

ApiVersion design

Something that's come up when talking to folks about this is the ApiVersion's intent, so I thought I'd discuss the pros/cons of different alternatives, given it's not clear.

To consider this, I had the following requirements in mind:

  • How do we keep steps generic, i.e. Then the response matches the schema definition without mentioning what version is used?
  • How to support different response types (controlled through the accept header)?
  • How to support different request body types (controlled through the content-type header)?
  • API Versions don't necessarily just mean presenting a new accept / content-type - there can be new required/removed querystring parameters, headers, and even the format of the request body can change drastically, so we need to have a solution which works preferably independently to each route's different versioning

Using ApiVersion as a pure enum

This is the above solution, and means:

  • Step definitions are able to react accordingly by using a switch / case statement over the ApiVersion that's provided
  • The Proxy class can support whichever response type versions
  • The Proxy class can support whichever request type versions
  • The Proxy class can support whichever other changes are required

But it also leads to:

  • Step definitions are now a little more complex, as they need to do things based on what version is there, but it reads much better to have generic steps than to keep creating new steps / have the ApiVersion too configurable

ApiVersion storing accept and content-type headers

Because we're using server-driven content negotiation for this example, we will want to store the accept and content-type headers with the version that we're communicating with. This gives us the following:

public enum ApiVersion {
  ANY("application/*+json", "application/*+json"),
  ONE("application/vnd.me.jvt.api.v1+json", "application/vnd.me.jvt.api.v1+json"),
  TWO("application/vnd.me.jvt.api.v2+json", "application/vnd.me.jvt.api.v2+json");

  ApiVersion(String acceptHeader, String contentTypeHeader) {
    // ...
  }

  // constructor and getter omitted for brevity
}

This is good because:

  • Steps are kept generic, and can validate against the acceptHeader and contentTypeHeader from the ApiVersion
  • There's no duplication of these values across the project, so our Proxy class and our steps can refer to constant values for our accept / content-types

However:

  • This doesn't work when we have a service producing/consuming different types of content - application/*+json, application/*+html, application/*+pdf, etc, as we would then need i.e. ApiVersion.JSON_ONE
  • If an route still requires a different API contract, your Proxy class will still need to implement something differently - is it worth keeping some logic in the ApiVersion and some in the Proxy then?

Storing more in the ApiVersion

As mentioned, because there's more to the versioning of an API than just the accept / content-type, we may need other parameters for a request, which balloons the size and complexity of ApiVersion.

Unfortunately this then ties the ApiVersion to knowing how each route's HTTP logic is going to work, which strays from the Proxy class owning the contract, so I don't agree with this approach.

Providing a factory for the ApiVersion to RequestSpecification conversion

One thing we could do is to provide a factory class that could convert an ApiVersion and return i.e. a RequestSpecification for a given route.

At that point though, we're just abstracting away from our Proxy class, which again I don't agree with.

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#java #testing #testing #software-testing #cucumber #software-quality #quality-engineering.

This post was filed under articles.

This post is part of the series environment-agnostic-acceptance-tests.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.