Link Search Menu Expand Document

Best Practices

We have already covered how to author good scenarios. Let us walk through the aspects we should consider when stringing scenarios together in contract files.

Design Patterns

All API endpoints in one .qontract file

When all the endpoints are simple, data structures are not too deep, or the list of APIs is small, all the scenarios related to these end points can go into a single file.

  • Advantages
    • One single place to look at for all the APIs.
    • Common structures and datatypes that are declared in the Background section can be re-used across many scenarios.
  • Disadvantages / Smells
    • The .qontract file can become large and becomes a scroll hell.
    • All the developers update the same file, resulting in a larger number of merge conflics.

One API endpoint per .qontract file

High complexity of API endpoints may result in too much information stuffed into a single contract file. This often happens when the data structures in the request or response payloads are very deep. When faced with such an API end point, put the definition of the API into a file by itself.

  • Advantages
    • Easier to comprehend .qontract files
  • Disadvantages / Smells
    • Common structures / datatypes across endpoints have to be duplicated (because we cannot import one .qontract file inside another)

Multiple scenarios per endpoint

A large number of optional keys is usually a design smell. The concerned API payload usually contains multiple groups of keys, where all the keys in a group are either present or missing together. If they are all simply marked optional, the contract will let you stub out incorrect combinations of optional keys any which way, without giving you feedback.

Here’s a concrete example.

Let’s describe an API that returns the dimensions of a shape. If the requested shape is a rectangle, the response JSON contains two keys, length and breadth, if a circle, circumference, and if a triangle, side1, side2 and side3.

Here’s what a single-scenario contract might look like:

Feature: Shape API
  Scenario: Dimensions
    When GET /dimensions/(shape_id:number)
    Then status 200
    And response-body
    | type           | (string) |
    | length?        | (number) |
    | breadth?       | (number) |
    | side1?         | (number) |
    | side2?         | (number) |
    | side3?         | (number) |
    | circumference? | (number) |

Now the contract shows almost all the keys as optional. This lets us stub out a response containing only length and side1, which is unrealistic. The real API would never return such a response, so we should not be able to stub out such a response either.

But the contract in this form allows it. So if we in error stub out a response with length and side1, and write our consumer against this stub, we’d get no negative feedback. Then, when we actually integrate our consumer with the real shape API instance, the consumer will expect length and side1, but it will get length and breadth, and it will break.

If the contract were designed better, the incorrect stub of length and side1 itself would have been rejected, saving us the integration error.

Take a look at the contract below:

Feature: Shape API
  Scenario: Dimensions of a rectangle
    When GET /dimensions/(id:number)
    Then status 200
    And response-body
    | type    | (string) |
    | length  | (number) |
    | breadth | (number) |

  Scenario: Dimensions of a triangle
    When GET /dimensions/(id:number)
    Then status 200
    And response-body
    | type  | (string) |
    | side1 | (number) |
    | side2 | (number) |
    | side3 | (number) |

  Scenario: Dimensions of a circle
    When GET /dimensions/(id:number)
    Then status 200
    And response-body
    | type          | (string) |
    | circumference | (number) |

This contract will not let us stub out a response with length and side1. None of the scenarios support that combination.

But now there’s duplication in all the scenarios. This too can be fixed:

Feature: Shape API
  Background:
    When GET /dimensions/(id:number)
    Then status 200

  Scenario: Dimensions of a rectangle
    * response-body
    | type    | (string) |
    | length  | (number) |
    | breadth | (number) |

  Scenario: Dimensions of a triangle
    * response-body
    | type  | (string) |
    | side1 | (number) |
    | side2 | (number) |
    | side3 | (number) |

  Scenario: Dimensions of a circle
    * response-body
    | type          | (string) |
    | circumference | (number) |

Note how the background contains everything that’s common to the scenarios.

Principles Of Design

  • Be specific with datatypes.
    • This improves ability spot issues when running the contract as a test and in generating meaningful dummy values in stub mode.
    • Example: Prefer date/url over string where possible
  • When your Scenario has several nullable values, use Scenario Outline. In Scenario Outline provide multiple example rows when there are nullable values.
    • Qontract runs two tests per nullable value, one for null and one for non-null value. When there are several nullable values Qontract will attempt running tests for all permutations.
    • The example rows in Scenario Outline can help Qontract in determining which are the plausible combinations in the context of your application.
  • Reduce Duplication. Extract common structures and datatypes to background.
    • Helps reduce verbosity in scenarios
    • Reduces human error that can happen when there is duplication of structure or datatype definition across scenarios
  • Single Responsibility Principle
    • We recommend adhering to SRP in order to achieve well authored contracts.
    • Each .qontract should have scenarios (regardless of whether they belong one endpoint or several endpoints) that are related to a single axis of change.
    • In other words, a contract file must change for one and only reason (entity, purpose or action).
    • This promotes healthy re-usability of structures and datatypes and helps in reducing merge conflicts
    • It may take time to triangulate on the right set of scenarios per file. In the long term this approach is best suited or applications of any size.

Namespacing

It is a good idea to maintain contract files under folder structures that represent namespaces just like you would maintain code.

For example:

/com
    /shop
        /orders
            returns.qontract

While this is not mandated, it is highly recommended as a way to improve maintainablity.

Static stubs

Static stub files should be co-located with their respective contracts. Example:

/com
    /shop
        /orders
            returns.qontract
            /returns_data
                returns.json

CI and Pull Request Builders

  • In repositories that store contracts, we recommend running the backward-compatibility check as a bare minimum
  • It is also helpful to have a Pull Request builder that can run the backward-compatibility check and annotate PRs where a contract change is involved