#GivenWhenThenWithStyle

How to test APIs with Gherkin?

The challenge 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?

Here is the challenge

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             |

 

Solving: How to test APIs with Gherkin?

The challenge for this week was describing API interactions with Gherkin.

Gaspar Nagy, one of the creators of SpecFlow and the co-author of BDD books, is our guest expert this week. Gaspar provided a detailed answer on his blog – check it out there.

Solving: How to test APIs with Gherkin? (#GivenWhenThenWithStyle – Challenge 21)