Service Virtualisation
- Service Virtualisation
- Why Service Virtualisation
- Why Specmatic
- First Define The Contract
- Basic stub using just the contract
- Stubbing out specific responses to specific requests
- Stubbing requests and responses with complex data
- Errors when stubbing requests or reponses that do not match the contract
- Match not found: wrong URL or method in the request
- Stubbing out multiple contracts in one Specmatic instance
- Lenient stubbing
- Strict mode
- Matching Path and Query Parameters in stub data json
- Datatype matching in stubs
- Stub without hardcoding values in the response
- Creating dynamic stubs
- Altering the stubbed response to a request
- Introducing a delay in the Stub Response
- Forward Unstubbed Requests To An Actual Service
- Stub file format
Read here about contract testing and where Specmatic fits in.
Why Service Virtualisation
It is not easy to develop an application that depends on 3rd party APIs. These APIs typically run in a staging environment. They must be invoked over the network, during the process of coding, debugging or running component tests. But sometimes the staging environment is down. The local internet connection may be offline. Sometimes an account has to be setup, data within the account has to be created, orders placed, etc and different developers on the same project overwrite eachother’s test data.
All this is outside our control. Tests can fail for any of these reasons, over and above actual logical errors, which can make running them quite frustrating.
The solution is to simulate the 3rd party APIs, and run the simulations on the developer’s laptop. A service virtualization tool provides this capability. A quick google search will yield several such tools. You feed such a tool all the requests your application makes to the 3rd party service, and all the respective responses expected from the service. With this information, the tool will completely simulate the 3rd party service for your application to run on. Since it runs on the developer’s laptop, it will never be slow or go offline.
Why Specmatic
If the API ever adds a parameter, changes a type, etc. the simulation will be out of sync, and the consumer application will not integrate with the provider API in higher environments.
One solution is to define the API first as a specification, and then use it to simulate the provider. For this to be effective, the provider dev must run the same contract spec to test their API. This way, the provider cannot deviate from the contract, assuring consumers of the fidelity of the simulation.
Specmatic can be used to define such an API specification.
First Define The Contract
Read more about how to define a contract here.
Basic stub using just the contract
A simulation is also called a stub.
In its most basic form, you only need the contract.
# filename: random.spec
Feature: Random API
Scenario: Random number
When POST /number
And request-body
| number | (number) |
Then status 200
And response-body (number)
You can run this in stub mode by itself: java -jar /path/to/specmatic.jar stub random.spec
You now have an http server running on port 9000 that responds to requests, such as:
> curl -X POST -H 'Content-Type: application/json' -d '{"number": 10}' http://localhost:9000/number
436
Any request that matches the contract request will be accepted.
The response will be randomly generated, based on the contract. The contract defines the response as a number, so the response was a randomly generated number. In every run, you will get a different, randomly generated response that matches the contract.
The file extension is .spec
by convention, and is enforced by the stub command. When contract files with extensions other than ‘.spec’ are supplied as input the command will exit and print all the files that have erroneous extensions
> java -jar /path/to/specmatic.jar stub random.contract
The following files do not end with specmatic and cannot be used:
random.contract
Stubbing out specific responses to specific requests
Often, you’ll need the stub to return a specific response for a given request.
For example:
# filename: square.spec
Feature: Square API
Scenario: Square of a number
When POST /square
And request-body (number)
Then status 200
And response-body (number)
A useful stub might be that when we post the number 5, we get 25 back.
Follow these steps:
- Create a directory named square_examples, in the same directory as the square.spec file.
- In that directory, create a json file, name it what you like. For example, square_of_5.json
- Put in that file the following text:
{
"http-request": {
"method": "POST",
"path": "/square",
"body": 5
},
"http-response": {
"status": 200,
"body": 25
}
}
- Create a json file named square_of_6.json
- Put in that file the following text:
{
"http-request": {
"method": "POST",
"path": "/square",
"body": 6
},
"http-response": {
"status": 200,
"body": 36
}
}
The directory structure now looks like this:
|
\_ square.spec [file]
\_ square_examples [directory]
|
\_ square_of_5.json [file]
\_ square_of_6.json [file]
Try running the stub now:
> java -jar /path/to/specmatic.jar stub square.spec
Reading the stub files below:
./square_examples/square_of_5.json
./square_examples/square_of_6.json
Stub server is running on http://localhost:9000. Ctrl + C to stop.
Specmatic looks for a directory with the filename of the specmatic file, + the suffix “_examples”. It found it in this case, and loaded it.
In another tab, let’s post 5 to the stub and see what happens:
> curl -X POST -H 'Content-Type: text/plain' -d 5 http://localhost:9000/square
25
Try posting 6 to the stub:
> curl -X POST -H 'Content-Type: text/plain' -d 6 http://localhost:9000/square
36
Let’s post 10 to the stub and see:
> curl -X POST -H 'Content-Type: text/plain' -d 10 http://localhost:9000/square
384
We did not have a stub with 10 in the request, but it matched the contract. Specmatic did not have a stubbed response, so it generated one and returned it.
Stubbing requests and responses with complex data
The contract:
# filename: customer.spec
Feature: Customer API
Scenario: Add customer
Given type Customer
| name | (string) |
| address | (string) |
When POST /customers
And request-body (Customer)
Then status 200
Create a directory named customer_examples, and put this stub in it:
// filename: new_customer.json
{
"http-request": {
"method": "POST",
"path": "/customers",
"body": {
"name": "Sherlock Holmes",
"address": "22 Baker Street"
}
},
"http-response": {
"status": 200
}
}
Run the stub:
> java -jar /path/to/specmatic.jar stub customer.spec
Reading the stub files below:
/path/to/customer_examples/new_customer.json
Stub server is running on http://localhost:9000. Ctrl + C to stop.
Invoke it in another tab:
> curl -X POST -H 'Content-Type: application/json' -d '{"name": "Sherlock Holmes", "address": "22 Baker Street"}' http://localhost:9000/customers
success
Errors when stubbing requests or reponses that do not match the contract
The contract:
# filename: customer.spec
Feature: Customer API
Scenario: Add customer
Given type Customer
| name | (string) |
| address | (string) |
When POST /customers
And request-body (Customer)
Then status 200
And response-body (string)
Create a directory named customer_examples, and put this stub in it:
// filename: new_customer.json
{
"http-request": {
"method": "POST",
"path": "/customers",
"body": {
"name": "Sherlock Holmes",
"address": 10
}
},
"http-response": {
"status": 200,
"body": "success"
}
}
Run the stub:
> java -jar /path/to/specmatic.jar stub customer.spec
Reading the stub files below:
/path/to/customer_examples/new_customer.json
/path/to/customer_examples/new_customer.json didn't match customer.spec
In scenario "Add customer"
>> REQUEST.BODY.address
Expected string, actual was number: 10
Stub server is running on http://localhost:9000. Ctrl + C to stop.
You can read more about Specmatic reports here.
You are warned here that the address was supposed to be a string, but was a number, 10.
Specmatic will not load this stub. But it will still load and stub out customer.spec. So you will get random responses matching the contract back for any request that matches the contract.
Try invoking the stub now:
> curl -X POST -H 'Content-Type: application/json' -d '{"name": "Sherlock Holmes", "address": "22 Baker Street"}' http://localhost:9000/customers
YHTHY
Match not found: wrong URL or method in the request
When the stub gives you this error, it means that the url in your request does not match the contract or any of the stubs that you have setup.
Let’s try this with an example.
File: square.spec
Feature: Square API
Scenario: Square number
When POST /square
And request-body (number)
Then status 200
And response-body (number)
Create a stub for it:
File: square_examples/stub.json
{
"http-request": {
"method": "POST",
"path": "/square",
"body": 10
},
"http-response": {
"status": 200,
"body": 100
}
}
Run the stub:
> specmatic stub square.spec
Loading square.spec
Loading stub expectations from /Users/joelrosario/tmp/square_examples
Reading the following stub files:
/Users/joelrosario/tmp/square_examples/stub.json
Stub server is running on http://0.0.0.0:9000. Ctrl + C to stop.
And in a new tab, let’s make an API call with the number we stubbed out:
> curl -X POST -H 'Content-Type: text/plain' -d 10 http://localhost:9000/square
100
Let’s pass a number we did not stub out, say 20:
> curl -X POST -H 'Content-Type: text/plain' -d 20 http://localhost:9000/square
263
Let’s even try with the the wrong method. The API expects POST, let’s try with GET:
curl -X GET -H 'Content-Type: text/plain' -d 20 http://localhost:9000/square
In scenario "Square number"
>> REQUEST.METHOD
Expected POST, actual was GET
But now, let’s try with a URL that does not feature in the contract at all:
curl -X POST -H 'Content-Type: text/plain' -d 20 http://localhost:9000/number
Match not found
Without the URL, Specmatic has no clue how to correlate the request with either the contract or its stubs, and simply responds saying “Match not found”.
Stubbing out multiple contracts in one Specmatic instance
If you want to stub out multiple contracts together:
> java -jar /path/to/specmatic.jar stub customer.spec order.spec
If the customer_examples and order_examples directories exist, stub data will be loaded from them automatically.
Lenient stubbing
So far we have been using examples of lenient stubbing. To reiterate, by default in lenient stubbing, if your request matches the contract, but none of the stubs, contract generates a response using the format defined in the contract. The values in the response are randomised.
Consider this contract:
# filename: customer.spec
Feature: Customer API
Scenario: Add customer
When GET /customers?name=(string)
Then status 200
And response-body (string)
Create a directory named customer_examples, and inside it a file named stub.json, with the following contents:
// filename: customer_examples/stub.json
{
"http-request": {
"method": "GET",
"path": "/customers",
"query": {
"name": "Sherlock"
}
},
"http-response": {
"status": 200,
"body": "22 Baker Street"
}
}
Now start the stub, and in a new tab or window, invoke the API passing Sherlock:
> curl http://localhost:9000/customers\?name\=Sherlock
22 Baker Street%
Which is what we expect.
But let’s try the wrong name now:
curl http://localhost:9000/customers\?name\=Jane
ILXWG
Note that the stub was expecting Sherlock but not Jane. However, the name
query parameter can take a string according to the contract and Jane is a string.
So while Specmatic doesn’t know what to reply in response to the request for a customer named Jane, the request does meet the contract.
In this case, Specmatic generated a randomised response based on the format in the contract and returned it.
Strict mode
Sometimes when we send a request we thought we had stubbed, we get a randomised response because of lenient stubbing. But we do want the specific response we had stubbed out, and would like Specmatic to tell us why it is not returning it.
To do this, we must turn strict mode on.
java -jar path/to/specmatic.jar stub customer.spec --strict
Invariably this happens when the request matches the contract, but does not match any of the stubs. Perhaps it’s a small difference, which is why we missed it.
Let’s try this with the same contract as above:
# filename: customer.spec
Feature: Customer API
Scenario: Add customer
When GET /customers?name=(string)
Then status 200
And response-body (string)
Create a directory named customer_examples, and inside it a file named stub.json, with the following contents:
// filename: customer_examples/stub.json
{
"http-request": {
"method": "GET",
"path": "/customers",
"query": {
"name": "Sherlock"
}
},
"http-response": {
"status": 200,
"body": "22 Baker Street"
}
}
Now start the stub, and in a new tab, invoke the API with the name Jane instead of Sherlock and see the result:
> curl http://localhost:9000/customers\?name\=Jane
STRICT MODE ON
>> REQUEST.URL.QUERY-PARAMS.name
Expected string: "Sherlock", actual was string: "Jane"
This is exactly the same command we ran at the end of the previous section but we were not running in strict mode then.
Now in strict mode, Specmatic returns an error indicating what was wrong with our request.
Matching Path and Query Parameters in stub data json
Consider this contract file.
File: petstore.spec
Feature: Contract for the petstore service
Scenario: Fetch pet details
When GET /pets/(id:number)?name=(string)
Then status 200
And response-body (string)
The path parameter id and query parameter name can be setup in the corresponding data json file with below syntax.
File: petstore_examples/petstore.json
{
"http-request": {
"method": "GET",
"path": "/pets/2",
"query": {
"name": "Archie"
}
},
"http-response": {
"status": 200,
"body": "Golden Retriever"
}
}
The path parameter, id is set up to match the number 2. Query parameters cannot be mentioned as is, they have to separately setup under the “query” section.
So even below curl request will return “Golden Retriever” as long as path and query parameters matches.
> curl -vs http://0.0.0.0:9000/pets/2\?name\=Archie 2>&1 | less
Golden Retriever
If the path and query parameters do not match the stub, then a generated response will be returned.
> curl -vs http://0.0.0.0:9000/pets/2\?name\=Shiro 2>&1 | less
MJUKU
In strict mode (running the stub command with –strict option), the entire URL is matched.
> java -jar ~/Downloads/specmatic.jar stub ~/test.spec --strict
And in a new tab:
> curl -vs http://0.0.0.0:9000/pets/2\?name\=Archie 2>&1 | less
Golden Retriever
curl -vs http://0.0.0.0:9000/pets/2\?name\=Shiro 2>&1 | less
STRICT MODE ON
>> REQUEST.URL.QUERY-PARAMS.name
Expected string: "Archie", actual was string: "Shiro"* Closing connection 0
Datatype matching in stubs
Let us assume in the above example you are not interested in matching the pet id. As long as the name is “Archie” you want to return “Golden Retriever”.
{
"http-request": {
"method": "GET",
"path": "/pets/(id:number)",
"query": {
"name": "Archie"
}
},
"http-response": {
"status": 200,
"body": "Golden Retriever"
}
}
Another example where we can match any string in the query parameter “name” and only the number 2 for id.
{
"http-request": {
"method": "GET",
"path": "/pets/2",
"query": {
"name": "(string)"
}
},
"http-response": {
"status": 200,
"body": "Golden Retriever"
}
}
The Datatype matching works in (“–strict”)[#strict-mode] mode also.
Stub without hardcoding values in the response
Sometimes you don’t care what values come back in the response, you just need, say, a string.
# filename: customer.spec
Feature: Customer API
Scenario: Add customer
Given type Customer
| name | (string) |
| address | (string) |
When POST /customers
And request-body (Customer)
Then status 200
And response-body (string)
Create a directory named customer_examples, and a file named stub.json within it that contains this:
// filepath: customer_examples/stub.json
{
"http-request": {
"method": "POST",
"path": "/customers",
"body": {
"name": "Sherlock Holmes",
"address": "22 Baker Street"
}
},
"http-response": {
"status": 200,
"body": "(string)"
}
}
And the result:
> curl -X POST -H 'Content-Type: application/json' -d '{"name": "Sherlock Holmes", "address": "22 Baker Street"}' http://localhost:9000/customers
TPVNQ
The response is a random one, generated by Specmatic.
Creating dynamic stubs
You can setup a stub over HTTP, after Specmatic has been started.
Let’s try this out with a simple contract:
# filename: customer.spec
Feature: Customer API
Scenario: Add customer
Given type Customer
| name | (string) |
| address | (string) |
When POST /customers
And request-body (Customer)
Then status 200
And response-body (string)
Start it as a stub:
> specmatic stub customer.spec
Loading customer.spec
Stub server is running on http://0.0.0.0:9000. Ctrl + C to stop.
Now run the following curl command:
> curl -X POST -H 'Content-Type: application/json' -d '{"http-request": {"method": "POST", "path": "/customers", "body": {"name": "Jane Doe", "address": "12B Baker Street"}}, "http-response": {"status": 200, "body": "success"}}' http://localhost:9000/_specmatic/expectations
We just posted the stub content using the POST verb to Specmatic at the path /_specmatic/expectations.
Now the stub is setup with real values.
Note that the contents of the body, specified by the -d parameter, follows the same format as the sample stub json files that we have seen before.
Let’s invoke the stub now.
> curl -X POST -H 'Content-Type: application/json' -d '{"name": "Jane Doe", "address": "12B Baker Street"}' http://localhost:9000/customers
success
The stub returns success, just like we told it.
Altering the stubbed response to a request
Sometimes, in the same test suite, different tests may require different responses for the same request parameters.
Consider the following contract, in which we have scenarios for success as well as failure.
# filename: customer.spec
Feature: Customer API
Background:
Given type Customer
| name | (string) |
| address | (string) |
When POST /customers
And request-body (Customer)
Scenario: Update customer address
Then status 200
And response-body (string)
Scenario: Customer not found
Then status 404
And response-body (string)
Start it as a stub:
> specmatic stub customer.spec
Loading customer.spec
Stub server is running on http://0.0.0.0:9000. Ctrl + C to stop.
Suppose our first test expects to get success back, we can set it up dynamically using this curl command (which we already know from before):
> curl -X POST -H 'Content-Type: application/json' -d '{"http-request": {"method": "POST", "path": "/customers", "body": {"name": "Jane Doe", "address": "12B Baker Street"}}, "http-response": {"status": 200, "body": "success"}}' http://localhost:9000/_specmatic/expectations
And we will be able to invoke the stub:
> curl -X POST -H 'Content-Type: application/json' -d '{"name": "Jane Doe", "address": "12B Baker Street"}' http://localhost:9000/customers
success
Now, suppose our next test expects name not found
for Jane Doe at 12B Baker Street.
For the second test to work, the stub can’t return success
anymore. We need to alter it, so that given the old request body {"name": "Jane Doe", "address": "12B Baker Street"}
, it provides the new response needed by the second test.
To do this, just call the dynamic expectaion API again.
> curl -X POST -H 'Content-Type: application/json' -d '{"http-request": {"method": "POST", "path": "/customers", "body": {"name": "Jane Doe", "address": "12B Baker Street"}}, "http-response": {"status": 404, "body": "name not found"}}' http://localhost:9000/_specmatic/expectations
Let’s try invoking the /customers API again:
curl -H 'Content-Type: application/json' -d '{"name": "Jane Doe", "address": "12B Baker Street"}' http://localhost:9000/customers
name not found
We get the new stubbed response back.
Note that in both curl calls to /_specmatic/expectations, the request is the same, but the response status and body are different.
In short, the newer expectation overrides the older one with the same request parameters.
Introducing a delay in the Stub Response
At times, it is necessary to simulate a slow response from the application we are stubbing.
> curl -X POST -H 'Content-Type: application/json' -d '{"http-request": {"method": "POST", "path": "/customers", "body": {"name": "Jane Doe", "address": "12B Baker Street"}}, "http-response": {"status": 404, "body": "name not found"}, "delay-in-seconds": 15}' http://localhost:9000/_specmatic/expectations
The above dynamics expectation is exactly as in the previous section except the “delay-in-seconds” param. Every request that matches this specific expectation will respond with a 15 second delay. On all other requests, Specmatic responds immediately.
Forward Unstubbed Requests To An Actual Service
You can provide a URL to which Specmatic will forward all requests which have not been stubbed out.
This is done by start the stub like this:
> specmatic stub --passThroughTargetBase http://third-party-service.com customer-service.spec
Since nothing is stubbed at this point, Specmatic will forward all requests as is to http://third-party-service.com
, and relay the request back. In doing so, it acts as a plain vanilla proxy.
But if you create a stub:
> curl -X POST -H 'Content-Type: application/json' -d '{"http-request": {"method": "POST", "path": "/customers", "body": {"name": "Jane Doe", "address": "12B Baker Street"}}, "http-response": {"status": 200, "body": "success"}}' http://localhost:9000/_specmatic/expectations
Specmatic will handle the request:
> curl -X POST -H 'Content-Type: application/json' -d '{"name": "Jane Doe", "address": "12B Baker Street"}' http://localhost:9000/customers
success
Stub file format
Here is a sample json stub file, containing all the keys you can use, with inline comments.
{
"http-request": {
"method": "POST",
"path": "/url/path/(number)/some/more/path", // Path parameters can appear inline, query parameters need to mentioned separately in the query section below.
"headers": {
"X-Header-Name1": "(string)",
"X-Header-Name2": "(string)"
},
"query": {
"id": "(number)",
"type": "(string)"
},
// IMPORTANT You can have either body, form-fields or multipart-formdata, but not all 3
"form-fields": {
"Data": "(PredefinedJsonType)",
"MoreData": "some hardcoded value"
},
"multipart-formdata": [
{
"name": "customers",
// either content or filename, but not both
"content": "(string)",
"filename": "@data.csv", // must start with @
"contentType": "text/plain", // optional, to be used with filename, matched against Content-Type header
"contentEncoding": "gzip" // optional, matched against Content-Encoding header
}
]
"body": { // Body can also just be a string, such "Hello world", or an array, such as [1, 2, 3]
"name": "Jane Doe",
"address": "22 Baker Street"
}
},
"http-response": {
"status": 200, // http status expected in the response
"headers": { // same as request headers
"X-Header-Name": "(string)",
"X-Header-Name2": "(string)"
}
"body": "some value" // can be any json value, but must match the contract
}
}