- The Contract File
- Basic structure
- JSON Syntax
- XML Syntax
- HTTP Protocol
- Kafka Messages
- The Background section
Specmatic extends the Gherkin syntax by adding some more keywords.
If you are familiar with Gherkin you should feel right at home. If not, don’t worry, Gherkin is easy to pick up.
The Contract File
A contract is an API specification. It describes the provider endpoints, the requests it accepts, the responses it returns and the data types it support etc.
Contracts are text files which you can create with any text editor. Just make sure that the extension is .spec
.
Visual Studio Code and IntelliJ Idea support syntax highlighting. We recommend setting up an editor if you want to write these contracts on a regular basis.
Basic structure
Here’s a small contract.
Feature: Contract for the petstore service
Scenario: Should be able to get a pet by petId
When GET /pets/(petid:number)
Then status 200
And response-body {petid: "(number)"}
Each Feature can have several scenarios.
Each scenario describes a single interaction with the Provider, using the Given, When and Then Format
- Given - Pre-conditions, data structure definitions, etc.
- When - This represents the request
- Then - Assertions
We’ll see more examples of how Given
is used below.
Next, let us focus on this line:
When GET /pets/(petid:number)
The When
keyword is followed by the Http Method and the URL.
- List of supported Http Methods - GET, POST, PUT, DELETE
- The URL can have multiple path parameters and query parameters. Examples:
/pets/(petid:number)?name=(string)
/pets/(petid:number)/owners/(ownerid:number)?name=(string)&type=(string)
URL Parameters Definition
- Path Parameters - (<parameter-name>:<data-type>)
- Query Parameters - <parameter-name>=(<data-type>)
And finally, Then
. This section describes the response.
Then status 200
And response-body {petid: "(number)"}
Note that we added a response body with the And
clause.
Slightly More Complex Example
Feature: Contract for the petstore service
Scenario: Should be able to get a pet by petId
When POST /pets/
And request-body {petid: "(number)"}
And request-header auth-token (string)
And request-header traceid (number)
Then status 200
And response-header token (string)
And response-header CONTENT-TYPE application/json
Note how we add more information to the request and response with the And
clause.
In above example, there are two more keywords.
- request-body - You can only have one request-body in a scenario
- request-header - You can add as many request headers as you like
The response-body and response-header keywords are similar to request-body and request-header respectively.
As a whole, the contract should read a little like English.
Built-in Data Types
- number
- string
- boolean
- null
- url
- datetime
So for example:
Feature: String API
Scenario: Upper case of a string
When POST /uppercase
And request-body (string)
Then status 200
And response-body (string)
number, string, boolean and null are all used the same way.
Note: The number type matches decimals numbers too.
Length Restrictions
String and Number support minLength and maxLength when they are declared as a type.
Given type <Type Name> <(string)/(number)> minLength <min length> maxLength <max length>
Once declared they can be referred in any part of the specification.
Example:
Feature: Contract for /employees API
Scenario: api call
Given type EmployeeName (string) minLength 6 maxLength 12
And type EmployeeId (number) minLength 8 maxLength 11
And type Employee
| name | (EmployeeName) |
| id | (EmployeeId) |
When GET /employees
Then status 200
And response-body (Employee*)
datetime type
(datetime)
matches and generates ISO standard dates within strings. No other primitive data type will be accepted.
url type
(url)
matches and generates valid urls within strings. No other primitive data type will be accepted.
For example, (url)
will match “http://somedomain.xyz?key=value”
You can also specifically define the following url types:
(url-http)
declares an http url(url-https)
declares an https url(url-path)
declares a url path without the scheme prefix
Pattern In String
We can explicitly describe types in strings. If for example we know that the value is a number, but it will be inside a string:
Given type Id (number in string)
Explicit Reference To Examples In Type Declarations
We can explicitly refer to the example column from within a type declaration.
When POST /
And request-body (orderid:number)
Then status 200
Examples:
| orderid |
| 10 |
In this example, the request-body looks up the orderid from the examples. When the test request is generated, it contain the value 10
in the request body.
Scenario Outline For Contract Testing
Let us assume that we need to run below Scenario as a test with two petIds, 2 and 3, instead of autogenerated values.
Read more about contract tests here.
Scenario: Should be able to get a pet by petId
When GET /pets/(petid:number)
Then status 200
And response-body {petid: "(number)"}
A way to achieve this is by declaring the same scenario twice.
Scenario: Should be able to get a pet by petId 2
When GET /pets/2
Then status 200
And response-body {petid: "(number)"}
Scenario: Should be able to get a pet by petId 3
When GET /pets/3
Then status 200
And response-body {petid: "(number)"}
This can get repetitive. We can leverage “Scenario Outline” to run this “Scenario” as a test twice with different values.
Scenario Outline: Should be able to get a pet by petId
When GET /pets/(petid:number)
Then status 200
And response-body {petid: "(number)"}
Examples:
| petid |
| 2 |
| 3 |
NOTE: Scenario Outline is effective only in the context of request params when running contract as a test. To return specific values when running the contract leverage Stub Mode.
JSON Syntax
Arrays
We can describe an array containing multiple values.
Feature: Arithmetic API
Scenario: Add 2 numbers
When POST /add
And request-body ["(number)", "(number)"]
Then status 200
And response-body (number)
Since we are leveraging native JSON syntax, the type must be placed within a string.
[“(number)”, “(number)”] would match [1, 2], but not [1], nor [1, 2, 3], and not [1, “2”]
Variable Length Arrays
We can describe arrays where the type of each element is known, but the length of the array is not fixed.
Feature: Arithmetic API
Scenario: Add all the numbers
When POST /add
And request-body ["(number*)"]
Then status 200
And response-body (number)
Here, [“(number*)”] would match [1], [1, 2], and even the empty array [].
Note that for now, the array operator works only on type names. [“(number)”]* is not valid syntax. See Defining Types for more.
Objects
We can describe JSON objects, and provide type specifiers for their values.
Feature: Arithmetic API
Scenario: Perform an arithmetic operation
When POST /operate
And request-body {"val1": "(number)", "val2": "(number)", "operation": "(string)"}
Then status 200
And response-body (number)
In {"val1": "(number)", "val2": "(number)", "operation": "(string)"}
you can see that the keys are fixed.
This would match {"val1": 10, "val2": 20, "operation": "+"}
.
Defining Types
Sometimes the data structure is too complex to view in place. It helps to be able to pull it out, so that the semantics of the request are not obscured.
Feature: Arithmetic API
Scenario: Perform 2 nested operations
When POST /operate
And request-body {"op1": {"val1": "(number)", "val2": "(number)", "operation": "(string)"}, "op2": {"val1": "(number)", "val2": "(number)", "operation": "(string)"}, "operation": "(string)"}
Then status 200
And response-body (number)
This request body is way too complex. Instead:
Feature: Arithmetic API
Scenario: Perform 2 nested operations
Given type Operation {"val1": "(number)", "val2": "(number)", "operation": "(string)"}
And type ContainerOperation {"op1": "(Operation)", "op2": "(Operation)", "operation": "(string)"}
When POST /operate
And request-body (ContainerOperation)
Then status 200
And response-body (number)
This expresses the intent of the structures much more easily.
Defining Enums
Declaration Syntax
Given enum <EnumName> <DataType - String Or Number Only> values <Comma separated values>
Usage Patterns
Enums can appear as part of url parameters, headers and body. Below is an example showing various usage patterns.
Feature: Contract for employees API
Scenario: api call
Given enum EmployeeType (string) values contract,permanent,trainee
And enum Rating (number) values 1,2,3
And enum Organisation (string) values hr,tech,admin
And enum ClientType (string) values mobile,web
And type Employee
| name | (string) |
| id | (number) |
| type | (EmployeeType) |
| rating | (Rating?) |
When GET /(organisation:Organisation)/employees/?empType=(EmployeeType)
And request-header client (ClientType)
Then status 200
And response-body (Employee*)
Nullable Enum
To mark an Enum as Nullable, suffix it with a question mark. Example: See “(Rating?)” in above spec. Note: Marking the Data Type (number and string) as nullable is not allowed within the context of Enum declaration.
Defining Objects As Tables
We can take readability one step further by using tables.
Feature: Arithmetic API
Scenario: Perform 2 nested operations
Given json Operation
| val2 | (number) |
| val1 | (number) |
| operation | (string) |
And json ContainerOperation
| op1 | (Operation) |
| op2 | (Operation) |
| operation | (string) |
When POST /operate
And request-body (ContainerOperation)
Then status 200
And response-body (number)
And this way each part of the structure is easy to see.
The pipes should be aligned for better readability. Fortunately, modern editors or IDEs like Visual Studio Code and IntelliJ Idea take care of this for you.
Putting Value In Objects
You can specify values in objects instead of types.
Feature: Arithmetic API
Scenario: Perform 2 nested operations
Given json Operation
| val1 | (number) |
| val2 | (number) |
| option | "first" |
This matches {"val1": 10, "val2": 20, "option": "first" }
but not {"val1": 10, "val2": 20, "option": "something else" }
List Operator
We can describe a list of items of a particular type.
Feature: Arithmetic API
Scenario: Add all numbers
Given type Numbers (number*)
When POST /sum
And request-body (Numbers)
Then status 200
And response-body (number)
(Numbers) is defined as (number*), and this matches [4, 7, 3, 85, 0]
Optional Operator
We can describe an object in which a key is optional.
Feature: Pet Store API
Scenario: Get pet details
Given type Pet
| id | (number) |
| name | (string) |
| description? | (string) |
When GET /pet/(id:number)
Then status 200
And response-body (Pet)
Note the suffix ?
attached to the description key, which declares the keyoptional.
The response can now be {"id": 10, "name": "Socks", "description": "Brown and white"}
, or just {"id": 10, "name": "Socks"}
without the description field.
However if Pet is in the request, such as:
Feature: Pet Store API
Scenario: Create pet
Given type Pet
| name | (string) |
| description? | (string) |
When POST /pets
Then status 200
And response-body (number)
Cyclomatic complexity here is 2. There are 2 types of structures that could be sent in the request, one with description and one without.
So when running in test mode, specmatic will run 2 tests for this, one with the description key, and one without.
Nullable Operator
We can represent values that may be null.
Scenario: Create pet
Given type Pet
| name | (string) |
| description | (string?) |
When POST /pets
Then status 200
And response-body (number)
The Pet type will permit both {"name": "Daisy", "description": "Beautiful dog!"}
and {"name": "Daisy", "description": null}
to be passed as a request to /pets.
Note that while the value of description can be null, the key itself is compulsory, and must be passed.
Cyclomatic complexity is 2, so when running in test mode, Specmatic will generate two requests, one with description set to a random string, and one with it set to null.
Rest Operator
Feature: Arithmetic API
Scenario: Arithmetic operation
Given type Operation ["(string)", "(number...)"]
When POST /operate
And request-body (Operation)
Then status 200
And response-body (number)
Operation would match [”+”, 1, 2, 3], [”-“, 2, 5, 3, 6], etc.
It can only be used in an array.
Combining *, ? and …
These operators can be combined. The operators must be interpreted in reverse order.
So:
(number*)
means an array of numbers, such as [1, 2, 3](numbers*?)
reading *? backwards, it means a nullable array of numbers, so it matches either [1, 2, 3] or null. For example:Given type Data | numbers | (numbers*?) |
…would match both {“numbers”: [1, 2, 3]} and {“numbers”: null}
(numbers?*)
is an array of nullable numbers, so it matches [1, 2, null, 4, null]. For example:Given type Data | numbers | (numbers?*) |
…would match both {“numbers”: [1, 2, 3]} and {“numbers”: [1, null, 3, 4, null, null]}
["(string)", "(number?...)"]
matches [”+”, 1, 2, null, 3, 4]. The first is a string. The rest are all nullable numbres. For example:Given type Operation | numbers | (numbers?*) |
…would match both {“numbers”: [1, 2, 3]} and {“numbers”: [1, null, 3, 4, null, null]}
Dictionary
Sometimes we don’t know the exact keys and values, but we know what their types will be.
For example:
{
"cart_id": 25,
"order": {
"10": {
"product_id": 174673,
"quantity": 5
},
"20": {
"product_id": 89374543,
"quantity": 1
},
}
}
This is a json object containing a cart. The order
value is a JSON value, the structure of which is an order id
and the details of the product
being ordered.
The order id
s will always be numbers. But they will change from cart to cart, so we cannot hard code them in the contract. The product details data structure for each order id
will always be a json object containing product_id and quantity.
We can express the whole thing like this:
Given type ProductDetails
| product_id | (number) |
| quantity | (number) |
And type Order (dictionary number ProductDetails)
And type Cart
| cart_id | (number) |
| order | (Order) |
There are 3 parts to this:
- We first declare the
ProductDetails
entry. - Next we declare a dictionary named
Order
, in which all the keys are numbers, and the values areProductDetails
. - We declare the enclosing
Cart
type, which contains the thecart id
and theorder
.
XML Syntax
XML Tags
We can describe XML tags like this:
When POST /customer
And request-body <customer><name>(string)</name></customer>
Then status 200
And this matches the following xml document:
<customer><name>John Doe</name></customer>
Multiline XML Definitions
When POST /customer
And request-body <customer><name>(string)</name><id>(number)</id></customer>
Then status 200
The above contract can also be declared like this:
When POST /customer
And request-body
"""
<customer>
<name>(string)</name>
<id>(number)</id>
</customer>
"""
Then status 200
XML Data Types
You can use all the scalar data types described earlier in this document except for null.
XML Attributes
Similarly, we can describe XML tags:
When POST /customer
And request-body
"""
<customer enabled="(boolean)">
<name>(string)</name>
</customer>
"""
Then status 200
This matches the following XML document:
<customer enabled="true"><name>John Doe</name></customer>
Optional Values In XML Tags
When POST /customer
And request-body
"""
<customer>
<name>(string?)</name>
</customer>
"""
Then status 200
And this matches an xml documents containing a name:
<customer><name>John Doe</name></customer>
and one with an empty name tag:
<customer><name></name></customer>
or just:
<customer><name/></customer>
Optional Values In XML Attributes
When POST /customer
And request-body
"""
<customer enabled="(boolean?)">
<name>(string)</name>
</customer>
"""
Then status 200
This matches a document with a value in enabled tag:
<customer enabled="true"><name>John Doe</name></customer>
and one with an empty enabled tag:
<customer enabled="true"><name>John Doe</name></customer>
Note here that only the value is optional. The tag is not. So the contract does NOT match a document without an enabled tag.
Specifically, the above contract will NOT match the following:
<customer><name>John Doe</name></customer>
Optional XML Nodes
Consider this XML node for a customer named Jane Holmes:
<customer>
<name>Jane Holmes</name>
<address>Baker Street</address>
</customer>
And this one for Sherlock Holmes:
<customer>
<name>Sherlock Holmes</name>
</customer>
The address
node is optional. Specmatic can express this like so:
<customer>
<name>(string)</name>
<address qontract_occurs="optional">(string)</address>
</customer>
This will match the customer xml data for both Jane and Sherlock Holmes.
Multiple Consecutive XML Nodes
Consider the following information from a shopping cart:
<cart>
<id>10</id>
<productid>10</productid>
<productid>20</productid>
<productid>30</productid>
<customerid>100</customerid>
</cart>
There could be any number of product ids.
The following is how we can put it in Specmatic:
<cart>
<id>(number)</id>
<productid qontract_occurs="multiple">(number)</productid>
<customerid>(number)</customerid>
</cart>
The productid
can now occur 0 or more times.
XML Types
Consider the following employee record:
<employee>
<name>(string)</name>
<address>(string)</address>
</employee>
And the following manager record:
<manager>
<name>(string)</name>
<address>(string)</address>
</manager>
Different node names, but the same tags inside.
We can pull the type out like this:
Given type Person
"""
<SPECMATIC_TYPE>
<name>(string)</name>
<address>(string)</address>
</SPECMATIC_TYPE>
"""
And type Manager <manager qontract_type="Person"/>
And type Employee <employee qontract_type="Person"/>
The Manager
type will match the manager
node, and the Employee
type will match the employee
node.
Reusing XML Nodes
Given type Customer
"""
<customer>
<name>(string)</name>
</customer>
"""
When POST /customer
And request-body (Customer)
Then status 200
This matches the following XML contract:
<customer><name>John Doe</name></customer>
XML Child Nodes
Given type Name <name>(string)</name>
And type Customer <customer>(Name)</customer>
When POST /customer
And request-body (Customer)
Then status 200
This matches the following XML contract:
<customer><name>John Doe</name></customer>
HTTP Protocol
Headers
We can describe headers in an HTTP request like this:
When POST /orders
And header Authentication: (string)
This matches an HTTP request with a header named Authentication
.
Specmatic ignores all headers in the HTTP request that are not defined in the contract.
Form Fields
We can describe form fields in the request like this:
When POST /orders
And form-field name (string)
And form-field address (string)
This corresponds Postman’s x-www-form-urlencoded in the request body.
Multipart Form Data
We can describe multipart form data in the request like this:
When POST /orders
And request-part name (string)
And request-part address (string)
This corresponds Postman’s form-data in the request body.
If the request part contains a file, use the @ symbol to denote the file name, like so:
When POST /orders
And request-part customers @customers.csv
When running the test, @customers.csv must actually exist in the working directory.
Kafka Messages
We can describe Kafka messages.
The syntax:
* kafka-message <topic> <key type> <value type>
OR
* kafka-message <topic> <value type>
For example:
Given json Customer
| name | (string) |
| phone | (string) |
Then kafka-message customerdata (String) (Customer*)
Here the message is expected to be on the customerdata topic, the key should be a string, and the value should be a json string.
The Background section
If there are multiple APIs using common types, it helps to put them in the Background. All scenarios inherit definitions in the background.
Feature: Arithmetic API
Background:
Given type Numbers (number*)
Scenario: Add numbers
When POST /sum
And request-body (Numbers)
Then status 200
And response-body (number)
Scenario: multiply numbers
When POST /product
And request-body (Numbers)
Then status 200
And response-body (number)
Common Definitions in the Background
All Scenarios and Scenario Outlines inherit the Background. So put the parts common to all scenarios into the Background.
For example:
Feature: Customer API
Scenario: Get customer details
Given type Customer
| name | (string) |
| address | (string) |
When GET /customer/(id:number)
Then status 200
And response-body (Customer)
Scenario: Get customer list
Given type Customer
| name | (string) |
| address | (string) |
When GET /customers
Then status 200
And response-body (Customer*)
Let’s remove the duplication, by putting the Customer type into the Background.
Feature: Customer API
Background:
Given type Customer
| name | (string) |
| address | (string) |
Scenario: Get customer details
When GET /customer/(id:number)
Then status 200
And response-body (Customer)
Scenario: Get customer list
When GET /customers
Then status 200
And response-body (Customer*)
Overriding The Background
A scenario can override a type defined in the background. Useful when one scenarios needs a slightly different data structure from the rest.
Feature: Pet Store API
Background:
Given type Pet
| id | (number) |
| name | (string) |
| type | (string) |
Scenario: Get details of a pet
When GET /pet/(id:number)
Then status 200
And response-body (Pet)
Scenario: Create pet
Given type Pet
| name | (string) |
| type | (string) |
When POST /pet
And request-body (Pet)
Then status 200
And response-body (number)
Compare the Pet definition in the background, and in the second scenario. The second one lacks an id, since the id is assigned by the application, and the caller will not have the id when invoking the API