layout | title | nav_order |
---|---|---|
default |
Getting started (in 5 min) |
4 |
{% include setup_command_line.md %}
PetStore application has a backend API (Provider) and a front-end client application (Consumer).
Here is a sequence diagram representing the getPetById
operation.
UI (Consumer) API (Provider)
| --- getPetById ---> |
| <-- {Pet JSON} ---- |
Before we get started, here is a quick refresher on the terminology used in the documentation.
- Consumer - The application requesting the data (in this case UI)
- Provider - The application responding with the data (in this case API)
Below is the OpenAPI specification that represents the communication between UI and Backend in the above example application. Please save this to a file called service.yaml
.
openapi: 3.0.1
info:
title: Contract for the petstore service
version: '1'
paths:
/pets/{petid}:
get:
summary: Should be able to get a pet by petId
parameters:
- name: petid
in: path
required: true
schema:
type: number
examples:
SCOOBY_200_OK:
value: 1
responses:
'200':
description: Should be able to get a pet by petId
content:
application/json:
schema:
required:
- id
- name
- status
- type
properties:
id:
type: number
name:
type: string
type:
type: string
status:
type: string
examples:
SCOOBY_200_OK:
value:
id: 1
name: Scooby
type: Golden Retriever
status: Adopted
We have a sample implementation of the PetStore API running which you can access through curl or any other tool of your choice.
curl https://my-json-server.typicode.com/znsio/specmatic-documentation/pets/1
Now lets use Specmatic to run the above API specification as a contract test against the Provider / API to see if it is adhering the OpenAPI Specification. {% tabs test %} {% tab test java %}
specmatic test service.yaml --testBaseURL=https://my-json-server.typicode.com/znsio/specmatic-documentation
{% endtab %} {% tab test npm %}
npx specmatic test service.yaml --testBaseURL=https://my-json-server.typicode.com/znsio/specmatic-documentation
{% endtab %} {% tab test docker %}
docker run -v "/local-directory/service.yaml:/service.yaml" znsio/specmatic test "/service.yaml" --testBaseURL=https://my-json-server.typicode.com/znsio/specmatic-documentation
{% endtab %} {% endtabs %}
Your output will look as shown in below (a few lines have been deleted in the interest of brevity).
API Specification Summary: service.yaml
OpenAPI Version: 3.0.1
API Paths: 1, API Operations: 1
Executing 1 tests
--------------------
Request to https://my-json-server.typicode.com/znsio/specmatic-documentation at 2024-2-12 1:54:58.570
GET /znsio/specmatic-documentation/pets/1
Response at 2024-2-12 1:54:58.572
200 OK
Content-Type: application/json; charset=utf-8
{
"id": 1,
"name": "Scooby",
"type": "Golden Retriever",
"status": "Adopted"
}
Tests run: 1/1 (100%)
Scenario: GET /pets/(petid:number) -> 200 | EX:SCOOBY_200_OK has SUCCEEDED
Could not load report configuration, coverage will be calculated but no coverage threshold will be enforced
|----------------------------------------------------------------------|
| API COVERAGE SUMMARY |
|----------------------------------------------------------------------|
| coverage | path | method | response | # exercised | remarks |
|----------|---------------|--------|----------|-------------|---------|
| 100% | /pets/{petid} | GET | 200 | 1 | covered |
|----------------------------------------------------------------------|
| 100% API Coverage reported from 1 path |
|----------------------------------------------------------------------|
Saving Open API Coverage Report json to ./build/reports/specmatic ...
Tests run: 1, Successes: 1, Failures: 0, Errors: 0
- Specmatic parsed your API specification and printed a brief
API Specification Summary
- Then it generated and started
Executing 1 tests
because our API specification contains only one endpoint with a single GET operation - Specmatic then logged the
HTTP Request
that it generated and theHTTP response
it received from the API implementation - And finally it prints out the test results along with an API Coverage Report (Read our detailed post on API Converage Report to know more.)
How did Specmatic know to make the exact request to GET /znsio/specmatic-documentation/pets/1
with petId as "1"? And not just any other number?
In the OpenAPI spec you may have noticed that there is an examples section for petid
with a named example called SCOOBY_200_OK
.
- name: "petid"
in: "path"
required: true
schema:
type: "number"
examples:
SCOOBY_200_OK:
value: 1
Remove the examples section such that the petid
param look as shown below.
- name: "petid"
in: "path"
required: true
schema:
type: "number"
And try running the specmatic test command again.
{% tabs test3 %} {% tab test3 java %}
specmatic test service.yaml --testBaseURL=https://my-json-server.typicode.com/znsio/specmatic-documentation
{% endtab %} {% tab test3 npm %}
npx specmatic test service.yaml --testBaseURL=https://my-json-server.typicode.com/znsio/specmatic-documentation
{% endtab %} {% tab test3 docker %}
docker run -v "/local-directory/service.yaml:/service.yaml" znsio/specmatic test "/service.yaml" --testBaseURL=https://my-json-server.typicode.com/znsio/specmatic-documentation
{% endtab %} {% endtabs %}
This will result in a test failure because the sample application returns a 404
.
Unsuccessful Scenarios:
" Scenario: GET /pets/(petid:number) -> 200 FAILED"
Reason: Testing scenario "Should be able to get a pet by petId. Response: Should be able to get a pet by petId"
API: GET /pets/(petid:number) -> 200
>> RESPONSE.STATUS
Expected status 200, actual was status 404
Tests run: 1, Successes: 0, Failures: 1, Errors: 0
This is because we removed the named example SCOOBY_200_OK
, Specmatic generated a random petId based on the datatype of the petId path parameter. And since test data does not exist for this petId in the sample application, we get a 404.
--------------------
Request to https://my-json-server.typicode.com/znsio/specmatic-documentation at 2024-2-11 5:44:5.791
GET /znsio/specmatic-documentation/pets/318
Once you restore the OpenAPI file to its original state (add back the example petId value) the tests should start passing again.
- Specmatic is able to tie the named example
SCOOBY_200_OK
listed under the request parameters and the response sections of the OpenAPI spec to create a test. - This is also reflected in the name of the test where Specmatic displays the
SCOOBY_200_OK
in the test logs - Here's a detailed breakdown of the contract test:
- Request: Specmatic uses the value defined for the petId request parameter from the
SCOOBY_200_OK
request example to make a HTTP request. - Response: In order to tie the above request with a HTTP response code in the spec, Specmatic looks for an example with same name:
SCOOBY_200_OK
under responses. In this case the response code happens to be 200. This request/response pair now forms a test case. - Response Validation: Note that we are running the specification as a contract test here, in which we are interested in validating only the API signature and not the API logic. Hence, Specmatic does not validate the actual response values defined in the
SCOOBY_200_OK
example against the values returned by the application. It only validates the response code. However, if you do wish to validate response values, you can find more details in our discussion here.
- Request: Specmatic uses the value defined for the petId request parameter from the
Scenario: GET /pets/(petid:number) -> 200 | EX:SCOOBY_200_OK has SUCCEEDED
Now lets try something more interesting and change the datatype of the "status" field of response in OpenAPI file to "number" and save it.
properties:
status:
type: "number"
Let us run the specmatic test command again. {% tabs test2 %} {% tab test2 java %}
specmatic test service.yaml --testBaseURL=https://my-json-server.typicode.com/znsio/specmatic-documentation
{% endtab %} {% tab test2 npm %}
npx specmatic test service.yaml --testBaseURL=https://my-json-server.typicode.com/znsio/specmatic-documentation
{% endtab %} {% tab test2 docker %}
docker run -v "/local-directory/service.yaml:/service.yaml" znsio/specmatic test "/service.yaml" --testBaseURL=https://my-json-server.typicode.com/znsio/specmatic-documentation
{% endtab %} {% endtabs %} This time around the test fails because the response from our sample app is not in line with the OpenAPI Specification.
Unsuccessful Scenarios:
" Scenario: GET /pets/(petid:number) -> 200 | EX:SCOOBY_200_OK FAILED"
Reason: Testing scenario "Should be able to get a pet by petId. Response: Should be able to get a pet by petId"
API: GET /pets/(petid:number) -> 200
>> RESPONSE.BODY.status
Contract expected number but response contained "Adopted"
Tests run: 1, Successes: 0, Failures: 1, Errors: 0
This is how Specmatic is able to make sure that your API never deviates from the Specification.
Please refer to below videos for extensive demos on Contract as Test.
- Video: Boundary Condition Testing - Verifying edge cases
- Video: Tracer Bullet Approach - Leveraging Contract as Test to Test Drive your Code
Learn more about Contract Tests here.
We have so far established that Specmatic will keep OpenAPI spec and the API implementation in sync. This gives us the confidence to use the same OpenAPI spec service.yaml
on the Consumer side for Intelligent Service Virtualisation with Specmatic. This will help us isolate our UI development and make progress independent of the Provider / API. Here is a sequence diagram illustrating the same where UI no longer has to interact with the real backend for testing purposes. UI can instead rely on Specmatic Stub which is emulating the Provider / API.
UI (Consumer) Specmatic Stub <- service.yaml
| --- getPetById ---> |
| <-- {Pet JSON} ---- |
Before we begin, please make sure that your service.yaml
file is restored to its original state.
To spin up a stub server with the service.yaml we authored earlier, run below command. {% tabs stub %} {% tab stub java %}
specmatic stub service.yaml
{% endtab %} {% tab stub npm %}
npx specmatic stub service.yaml
{% endtab %} {% tab stub docker %}
docker run -v "/local-directory/service.yaml:/service.yaml" -p 9000:9000 znsio/specmatic stub "/service.yaml"
{% endtab %} {% endtabs %}
This should start your stub server on port 9000 by default as below.
Loading service.yaml
API Specification Summary: service.yaml
OpenAPI Version: 3.0.1
API Paths: 1, API Operations: 1
Stub server is running on http://0.0.0.0:9000. Ctrl + C to stop.
Tip: You can switch the port number by adding --port <port of your choice>
in the command.
{% tabs stub-custom-port %} {% tab stub-custom-port java %}
specmatic stub service.yaml --port 9002
{% endtab %} {% tab stub-custom-port npm %}
npx specmatic stub service.yaml --port 9002
{% endtab %} {% tab stub-custom-port docker %}
# Note that --port 9002 at the end of the command is crucial. It ensures that the stub
# server inside the Docker container is listening on the same port (9002) that's being
# mapped to port 9000 on your machine. If these ports don't match, you won't be able to
# access the stub server from your machine.
docker run -v "/local-directory/service.yaml:/service.yaml" -p 9000:9002 znsio/specmatic stub "/service.yaml" --port 9002
{% endtab %} {% endtabs %}
Once the stub server is running you can verify the API by accessing it through Postman, Chrome, Curl etc.
curl http://localhost:9000/pets/123
You should now be able to see the response that matches the schema defined in your OpenAPI spec.
{
"id": 864,
"name": "VRIQA",
"type": "KPNDQ",
"status": 990
}
The response contains auto-generated values that adhere to the data type defined in the contract. In above output petid "864" is generated by specmatic and will vary with every execution.
However for petId 1, it will always return below values.
{
"id": 1,
"name": "Scooby",
"type": "Golden Retriever",
"status": "Adopted"
}
This is because the example SCOOBY_200_OK
in the service.yaml
spec file, which we earlier saw being used while running contract test, also serves a stub data when we run Specmatic stub.
With this we have effectively achived three goals in one go.
- Examples serve as sample data for people referring to the API specification as documentation
- The same examples are used in contract tests to create the HTTP request
- And these examples also serve as stub data when we run Spemcatic stub command
Let us try a few experiments. Remove the status
field in the 200_OKAY
response example in service.yaml
(the very last line in that file) and run the stub command again.
examples:
200_OKAY:
value:
id: 1
type: "Golden Retriever"
name: "Scooby"
status: "Adopted" # Remove this line
The stub server will auto reload your service.yaml
file as soon as you save it. And you should see an output as shown below.
Loading service.yaml
API Specification Summary: service.yaml
OpenAPI Version: 3.0.1
API Paths: 1, API Operations: 1
[Example SCOOBY_200_OK]: Error from contract service.yaml
In scenario "Should be able to get a pet by petId. Response: Should be able to get a pet by petId"
API: GET /pets/(petid:number) -> 200
>> RESPONSE.BODY.status
key named status in the spec was not found in the "SCOOBY_200_OK" example
Specmatic rejects the expectation / canned response since it is not in line with the OpenAPI Specification.
Please restore service.yaml
to its original state(by adding back the status
field in the SCOOBY_200_OK
example) before proceeding with this section.
If you would like to add more stub responses, however you do not wish to bloat your specification with a lot of examples, we can also externalise the stub / canned responses to json files also.
- Create a folder named
service_examples
in the same folder as yourservice.yaml
file (_examples
suffix is a naming convention that tell Specmatic to look for canned responses in that directory) - Create a json file with the name
togo.json
and add below contents to it
{
"http-request": {
"path": "/pets/2",
"method": "GET"
},
"http-response": {
"status": 200,
"body": {
"id": 2,
"name": "Togo",
"type": "Siberian Husky",
"status": "Adopted"
},
"status-text": "OK"
}
}
Now let us run the stub command again. {% tabs stub2 %} {% tab stub2 java %}
specmatic stub service.yaml
{% endtab %} {% tab stub2 npm %}
npx specmatic stub service.yaml
{% endtab %} {% tab stub2 docker %}
# Please note docker command here has to volume map the directory containing service.yaml
# to a directory within the container so that both service.yaml and folder service_examples along
# with togo.json are available to Specmatic docker container
docker run -v "/local-directory/:/specs" -p 9000:9000 znsio/specmatic stub "/specs/service.yaml"
{% endtab %} {% endtabs %}
This time you should see Specmatic load your canned response file also.
Loading service.yaml
API Specification Summary: service.yaml
OpenAPI Version: 3.0.1
API Paths: 1, API Operations: 1
Loading stub expectations from /Users/harikrishnan/projects/agilefaqs/ContractTesting/ExamplesAsTestAndStub/service_examples
Reading the following stub files:
/Users/harikrishnan/projects/agilefaqs/ContractTesting/ExamplesAsTestAndStub/service_examples/togo.json
Stub server is running on http://0.0.0.0:9000. Ctrl + C to stop.
Once the stub server is running you can verify the API by accessing it through Postman, Chrome, Curl etc.
curl http://localhost:9000/pets/2
You should now be able to see the data pertaining to the togo.json
file that you added.
{
"id": 2,
"name": "Togo",
"type": "Siberian Husky",
"status": "Adopted"
}
Specmatic validates this externalised stub JSON file togo.json
against the service.yaml
. Let us try this by removing the status
field within http-response body in togo.json
and run the stub command again.
{% tabs stub3 %} {% tab stub3 java %}
specmatic stub service.yaml
{% endtab %} {% tab stub3 npm %}
npx specmatic stub service.yaml
{% endtab %} {% tab stub3 docker %}
# Please note docker command here has to volume map the directory containing service.yaml
# to a directory within the container so that both service.yaml and folder service_examples along
# with togo.json are available to Specmatic docker container
docker run -v "/local-directory/:/specs" -p 9000:9000 znsio/specmatic stub "/specs/service.yaml"
{% endtab %} {% endtabs %}
You should see an output like below.
Loading stub expectations from /Users/harikrishnan/projects/agilefaqs/ContractTesting/ExamplesAsTestAndStub/service_examples
Reading the following stub files:
/Users/harikrishnan/projects/agilefaqs/ContractTesting/ExamplesAsTestAndStub/service_examples/togo.json
/Users/harikrishnan/projects/agilefaqs/ContractTesting/ExamplesAsTestAndStub/service_examples/togo.json didn't match service.yaml
Error from contract service.yaml
In scenario "Should be able to get a pet by petId. Response: Should be able to get a pet by petId"
API: GET /pets/(petid:number) -> 200
>> RESPONSE.BODY.status
Key named status in the contract was not found in the stub
Stub server is running on http://0.0.0.0:9000. Ctrl + C to stop.
Specmatic again rejects the expectation / canned response since it is not in line with the OpenAPI Specification.
We can now start consumer development against this stub without any dependency on the real API.
To know more about Intelligent Service Virtualisation please refer to below video demos