How to test APIs with Gherkin? #GivenWhenThenWithStyle

The challenge for this week is describing API interactions with Given-When-Then. Modern software rarely operates in isolation, and talking to third-party APIs is often a challenge for testing and specifications. Gherkin might be a good choice for describing API interactions, and for providing integration tests that protect against unexpected changes, especially when the audience includes other developers.

Here’s an example, loosely based on the LinkedIn sharing API (we simplified it here to focus on important aspects). Assume you’re operating a public API with an endpoint to share social media content with a message, link and a thumbnail. Optionally, the content can be linked to an organisation. You want to document how the API works. The audience are developers and integrators who speak HTTP/REST.

How would you describe the behaviour for this API?

All the examples below include two scenarios – one that posts content without linking it to an organisation, and one that creates content linked to an organisation.

Option 1: Include full JSON/XML message

The first option is to include the full body of the request (and potentially response) of the request to document the API request formats. The usual way to include formatted content into Given-When-Then is to could use the three quotes fence (“””) above and below:

Scenario: post message without organisation links

Given the user with ID "324_kGGaLE" is authenticated for the session
When a POST request is made to https://api.com/v2/shares with
"""
{
  "content": {
    "contentEntities": [
      {
        "entityLocation": "https://www.example.com/content.html",
        "thumbnails": [
          {
            "resolvedUrl": "https://www.example.com/image.jpg"
          }
        ]
      }
    ],
    "title": "Test Share with Content"
  },
  "owner": "urn:ex:person:324_kGGaLE",
  "subject": "Test Share Subject",
  "text": {
    "messageText": "Test Share!"
  }
}
"""
Then the most recent post for user 324_kGGaLE includes:
| message      | Test Share!                          |
| image        | https://www.example.com/image.jpg    |
| title        | Test Share with Content              |
| url          | https://www.example.com/content.html |
And the most recent post for user 324_kGGaLE is not linked to an organisation

Scenario: post message with organisation links

Given the user with ID "324_kGGaLE" is authenticated for the session
When a POST request is made to https://api.com/v2/shares with
"""
{
  "content": {
    "contentEntities": [
      {
        "entityLocation": "https://www.example.com/content.html",
        "thumbnails": [
          {
            "resolvedUrl": "https://www.example.com/image.jpg"
          }
        ]
      }
    ],
    "title": "Test Share with Content"
  },
  "owner": "urn:ex:person:324_kGGaLE",
  "subject": "Test Share Subject",
  "text": {
    "messageText": "Test Share Org!",
    "annotations": [
      {
        "entity": "urn:ex:organization:1337",
        "length": 8,
        "start": 6
      }
    ]
  }
}
"""
Then the most recent post for user 324_kGGaLE includes:
| message      | Test Share Org!                      |
| image        | https://www.example.com/image.jpg    |
| title        | Test Share with Org                  |
| url          | https://www.example.com/content.html |
| organisation | urn:ex:organization:1337             |

Option 2: Show important fields only, as request body snippets in the target format

Instead of showing the full request body each time, we could remove the generic aspects, and focus on the important fields for a specific scenario. For example, we could introduce a placeholder “[…]” and use step implementations to insert default property values:

Scenario: post message without organisation links

Given the user with ID "324_kGGaLE" is authenticated for the session
When a POST request is made to https://api.com/v2/shares with
"""
{
  […]
  "text": {
    "messageText": "Test Share!"
  }
}
"""
Then the most recent post for user 324_kGGaLE includes:
| message      | Test Share!                          |
And the most recent post for user 324_kGGaLE is not linked to an organisation

Scenario: post message with organisation links

Given the user with ID "324_kGGaLE" is authenticated for the session
When a POST request is made to https://api.com/v2/shares with
"""
{
  […]
  "text": {
    "messageText": "Test Share Org!",
    "annotations": [
     {
        "entity": "urn:ex:organization:1337",
        "length": 8,
        "start": 6
     }
    ]
  }
}
"""
Then the most recent post for user 324_kGGaLE includes:
| message      | Test Share Org!                      |
| organisation | urn:ex:organization:1337             |

Option 3: Set important fields with pointers

We could also completely avoid showing the request format. Instead of a placeholder that fills in missing fields, we could start with a generic message and provide just the overrides, pointing to the fields that need to be replaced.

Scenario: post message without organisation links

Given the user with ID "324_kGGaLE" is authenticated for the session
When a post is shared for the user with
| setting          | value       |
| text/messageText | Test Share! |
Then the most recent post for user 324_kGGaLE includes:
| message      | Test Share!                          |
And the most recent post for user 324_kGGaLE is not linked to an organisation

Scenario: post message with organisation links

Given the user with ID "324_kGGaLE" is authenticated for the session
When a post is shared for the user with
| setting | value   |
| text/messageText  | Hello LinkedIn world! |
| text/annotations[0]/entity | urn:ex:organization:1337 |
| text/annotations[0]/start  | 6 |
| text/annotations[0]/length | 8 |
Then the most recent post for user 324_kGGaLE includes:
| message      | Test Share Org!                      |
| organisation | urn:ex:organization:1337             |
| message      | Test Share Org!                      |

Option 4: Describe only behaviour

Alternatively, we could avoid using pointers to fields, and use business-like names instead:

Scenario: post message without organisation links

Given the user with ID "324_kGGaLE" is authenticated for the session
When a post is shared for the user with the message "Test Share!"
Then the most recent post for user 324_kGGaLE includes:
| message      | Test Share!                          |
And the most recent post for user 324_kGGaLE is not linked to an organisation

Scenario: post message with organisation links

Given the user with ID "324_kGGaLE" is authenticated for the session
When a post is shared for the user with a message linking to organisation "urn:ex:organization:1337"
Then the most recent post for user 324_kGGaLE includes:
| organisation | urn:ex:organization:1337             |

Participate in the challenge

Choose one of the options you like the best, or propose your own solution. You can post short answers directly in the form linked below. For longer explanations, post your suggestion somewhere and send us the link.

To participate, send your answers by Monday 22nd March. We will post the results on Tuesday 23rd March.

Stay up to date with all the tips and tricks and follow SpecFlow on Twitter or LinkedIn.

PS: … and don’t forget to share the challenge with your friends and team members by clicking on one of the social icons below 👇