Mocking and Fuzz Testing Distributed Micro Services with Record/Play, Templates and OpenAPI Specifications
Building large distributed systems often requires integrating with multiple distributed micro-services that makes development a particularly difficult as it’s not always easy to deploy and test all dependent services in a local environment with constrained resources. In addition, you might be working on a large system with multiple teams where you may have received new API specs from another team but the API changes are not available yet. Though, you can use mocking frameworks based on API specs when writing a unit tests but integration or functional testing requires an access to the network service. A common solution that I have used in past projects is to configure a mock service that can simulate different API operations. I wrote a JVM based mock-service many years ago with following use-cases:
Use-Cases
- As a service owner, I need to mock remote dependent service(s) by capturing/recording request/responses through an HTTP proxy so that I can play it back when testing the remote service(s) without connecting with them.
- As a service owner, I need to mock remote dependent service(s) based on a open-api/swagger specifications so that my service client can test all service behavior per specifications for the remote service(s) even when remote service is not fully implemented or accessible.
- As a service owner, I need to mock remote dependent service(s) based on a mock scenario defined in a template so that my service client can test service behavior per expected request/response in the template even when remote service is not fully implemented or accessible.
- As a service owner, I need to inject various response behavior and faults to the output of a remote service so that I can build a robust client that prevents cascading failures and is more resilient to unexpected faults.
- As a service owner, I need to define test cases with faulty or fuzz responses to test my own service so that I can predict how it will behave with various input data and assert the service response based on expected behavior.
Features
API mock service for REST/HTTP based services with following features:
- Record API request/response by working as a proxy server (native http/https proxy or via API) between client and remote service.
- Playback API response that were previously recorded based on request parameters.
- Define API behavior manually by specifying request parameters and response contents.
- Generate API behavior from open standards such as Open API or Swagger.
- Customize API behavior using a template language so that users can generate dynamic contents based on input parameters or other configuration. The template language can be used to generate response of any size from small to very large so that you can test performance of your system.
- Define multiple test scenarios for the API based on different input parameters or simulating various error cases that are difficult to reproduce with real services.
- Store API request/responses locally as files so that it’s easy to port stubbed request/responses to any machine.
- Allow users to define API request/response with various formats such as XML/JSON/YAML and upload them to the mock service.
- Support test fixtures that can be uploaded to the mock service and can be used to generate mock responses.
- Define a collection of helper methods to generate different kind of random data such as UDID, dates, URI, Regex, text and numeric data.
- Ability to playback all test scenarios or a specific scenario and change API behavior dynamically with different input parameters.
- Inject error conditions and artificial delays so that you can test how your system handles error conditions that are difficult to reproduce.
I used this service in many past projects, however I felt it needed a bit fresh approach to meet above goals so I rewrote it in GO language, which has a robust support for writing network services. You can download the new version from https://github.com/bhatti/api-mock-service. As, it’s written in GO, you can either download GO runtime environment or use Docker to install it locally. If you haven’t installed docker, you can download the community version from https://docs.docker.com/engine/installation/ or find installer for your OS on https://docs.docker.com/get-docker/.
or pull an image from docker hub (https://hub.docker.com/r/plexobject/api-mock-service), e.g.
Alternatively, you can run it locally with GO environment, e.g.,
For full command line options, execute api-mock-service -h
that will show you command line options such as:
Recording a Mock Scenario via HTTP/HTTPS Proxy
Once you have the API mock service running, the mock service will start two ports on startup, first port (default 8080) will be used to record/play mock scenarios, updating templates or uploading OpenAPIs. The second port (default 8081) will setup an HTTP/HTTPS proxy server that you can point to record your scenarios, e.g.
Above curl command will automatically record all requests and responses and create mock scenario to play it back. For example, if you call the same API again, it will return a local response instead of contacting the server. You can customize the proxy behavior for record by adding X-Mock-Record: true
header to your request.
Recording a Mock Scenario via API
Alternatively, you can use invoke an internal API as a pass through to invoke a remote API so that you can automatically record API behavior and play it back later, e.g.
In above example, the curl command is passing the URL of real service as an HTTP header X-Mock-Url
. In addition, you can pass other authorization headers as needed.
Viewing the Recorded Mock Scenario
The API mock-service will store the request/response in a YAML
file under a data directory that you can specify. For example, you may see a file under:
Note: the sensitive authentication or customer keys are masked in above example but you will see following contents in the captured data file:
Above example defines a mock scenario for testing /v1/customers/cus_**/cash_balance
path. A test scenario includes:
Predicate
- This is a boolean condition if you need to enable or disable a scenario test based on dynamic parameters or request count.
Group
- This specifies the group for related test scenarios.
Request Matching Parameters:
The matching request parameters will be used to select the mock scenario to execute and you can use regular expressions to validate:
- URL Query Parameters
- URL Request Headers
- Request Body
You can use these parameters so that test scenario is executed only when the parameters match, e.g.
match_query_params:
name: [a-z0-9]{1,50}
match_headers:
Content-Type: "application/json"
The matching request parameters will be used to select the mock scenario to execute and you can use regular expressions to validate, e.g. above example will be matched if content-type is application/json
and it will validate that name query parameter is alphanumeric from 1-50 size.
Example Request Parameters:
The example request parameters show the contents captured from the record/play so that you can use and customize to define matching parameters.
- URL Query Parameters
- URL Request Headers
- Request Body
Response Properties
The response properties will include:
- Response Headers
- Response Body statically defined or loaded from a test fixture
- Response can also be loaded from a test fixture file
- Status Code
- Matching header and contents
- Assertions You can copy recorded scenario to another folder and use templates to customize it and then upload it for playback.
The matching header and contents use match_headers
and match_contents
similar to request to validate response in case you want to test response from a real service for chaos testing. Similarly, assertions
defines a set of predicates to test against response from a real service:
assertions:
- VariableContains contents.id 10
- VariableContains contents.title illo
- VariableContains headers.Pragma no-cache
Above example will check API response and verify that id
property contains 10
, title
contains illo
and result headers include Pragma: no-cache
header.
Playback the Mock API Scenario
You can playback the recorded response from above example as follows:
Which will return captured response such as:
Debug Headers from Playback
The playback request will return mock-headers to indicate the selected mock scenario, path and request count, e.g.
X-Mock-Path: /v1/jobs/{jobId}/state
X-Mock-Request-Count: 13
X-Mock-Scenario: setDefaultState-bfb86eb288c9abf2988822938ef6d4aa3bd654a15e77158b89f17b9319d6f4e4
Upload Mock API Scenario
You can customize the recorded scenario, e.g. you can add path variables
to above API as follows:
In above example, I assigned a name stripe-cash-balance
to the mock scenario and changed API path to /v1/customers/:customer/cash_balance
so that it can capture customer-id as a path variable. I added a regular expression to ensure that the HTTP request includes an Authorization
header matching Bearer sk_test_[0-9a-fA-F]{10}$
and defined dynamic properties such as {{.customer}}, {{.page}}
and {{.pageSize}}
so that they will be replaced at runtime.
and then play it back as follows:
As you can see, you will need to pass Authorization header and it will generate:
As you can see, the values of customer, page and pageSize
are dynamically updated. You can upload multiple mock scenarios for the same API and the mock API service will play it back sequentially. For example, you can upload another scenario for above API as follows:
And then play it back:
which will return response with following error response
Dynamic Templates with Mock API Scenarios
You can use loops and conditional primitives of template language and custom functions provided by the API mock library to generate dynamic responses as follows:
Above example includes a number of template primitives and custom functions to generate dynamic contents such as:
Loops
GO template support loops that can be used to generate multiple data entries in the response, e.g.
Builtin functions
GO template supports custom functions that you can add to your templates. The mock service includes a number of helper functions to generate random data such as:
Add numbers
"Num": "{{Add 1 2}}",
Date/Time
"LastSeen": "{{Time}}",
"Date": {{Date}},
"DateFormatted": {{TimeFormat "3:04PM"}},
"LastSeen": "{{Time}}",
Comparison
{{if EQ .MyVariable 10 }}
{{if GE .MyVariable 10 }}
{{if GT .MyVariable 10 }}
{{if LE .MyVariable 10 }}
{{if LT .MyVariable 10 }}
{{if Nth .MyVariable 10 }}
Enums
"StrEnum": {{EnumString "ONE TWO THREE"}},
"IntEnum": {{EnumInt 10 20 30}},
Random Data
"SerialNumber": "{{Udid}}",
"AssetNumber": "{{RandString 20}}",
"LastSeen": "{{Time}}",
"Host": "{{RandHost}}",
"Email": "{{RandEmail}}",
"Phone": "{{RandPhone}}",
"URL": "{{RandURL}}",
"EnrollmentStatus": {{SeededBool $val}}
"ComplianceStatus": {{RandRegex "^AC[0-9a-fA-F]{32}$"}}
"City": {{RandCity}},
"Country": {{RandCountry}},
"CountryCode": {{RandCountryCode}},
"Completed": {{RandBool}},
"Date": {{TimeFormat "3:04PM"}},
"BatteryLevel": "{{RandNumMax 100}}%",
"Object": "{{RandDict}}",
"IntHistory": {{RandIntArrayMinMax 1 10}},
"StringHistory": {{RandStringArrayMinMax 1 10}},
"FirstName": "{{SeededName 1 10}}",
"LastName": "{{RandName}}",
"Score": "{{RandNumMinMax 1 100}}",
"Paragraph": "{{RandParagraph 1 10}}",
"Word": "{{RandWord 1 1}}",
"Sentence": "{{RandSentence 1 10}}",
"Colony": "{{RandString}}",
Request count and Conditional Logic
{{if NthRequest 10 }} -- for every 10th request
{{if GERequest 10 }} -- if number of requests made to API so far are >= 10
{{if LTRequest 10 }} -- if number of requests made to API so far are < 10
The template syntax allows you to define a conditional logic such as:
{{if NthRequest 10 }}
status_code: {{AnyInt 500 501}}
{{else}}
status_code: {{AnyInt 200 400}}
{{end}}
In above example, the mock API will return HTTP status 500 or 501 for every 10th request and 200 or 400 for other requests. You can use conditional syntax to simulate different error status or customize response.
Loops
{{- range $val := Iterate 10}}
{{if LastIter $val 10}}{{else}},{{end}}
{{ end }}
Variables
{{if VariableContains "contents" "blah"}}
{{if VariableEquals "contents" "blah"}}
{{if VariableSizeEQ "contents" "blah"}}
{{if VariableSizeGE "contents" "blah"}}
{{if VariableSizeLE "contents" "blah"}}
Test fixtures
The mock service allows you to upload a test fixture that you can refer in your template, e.g.
"Line": { {{SeededFileLine "lines.txt" $val}}, "Type": "Public", "IsManaged": false },
Above example loads a random line from a lines.txt fixture. As you may need to generate a deterministic random data in some cases, you can use Seeded functions to generate predictable data so that the service returns same data. Following example will read a text fixture to load a property from a file:
"Amount": {{JSONFileProperty "props.yaml" "amount"}},
This template file will generate content as follows:
{ "Devices": [
{
"Udid": "fe49b338-4593-43c9-b1e9-67581d000000",
"Line": { "ApplicationName": "Chase", "Version": "3.80", "ApplicationIdentifier": "com.chase.sig.android", "Type": "Public", "IsManaged": false },
"Amount": {"currency":"$","value":100},
"SerialNumber": "47c2d7c3-c930-4194-b560-f7b89b33bc2a",
"MacAddress": "1e015eac-68d2-42ee-9e8f-73fb80958019",
"Imei": "5f8cae1b-c5e3-4234-a238-1c38d296f73a",
"AssetNumber": "9z0CZSA03ZbUNiQw2aiF",
"LocationGroupId": {
"Id": {
"Value": 980
},
"Name": "Houston",
"Udid": "3bde6570-c0d4-488f-8407-10f35902cd99"
},
"DeviceFriendlyName": "Device for Alexander",
"LastSeen": "2022-10-29T11:25:25-07:00",
"Email": "john.smith@abc.com",
"Phone": "1-408-454-1507",
"EnrollmentStatus": true,
"ComplianceStatus": "ACa3E07B0F2cA00d0fbFe88f5c6DbC6a9e",
"Group": "Chicago",
"Date": "11:25AM",
"BatteryLevel": "43%",
"StrEnum": "ONE",
"IntEnum": 20,
"ProcessorArchitecture": 243,
"TotalPhysicalMemory": 320177,
"VirtualMemory": 768345,
"AvailablePhysicalMemory": 596326,
"CompromisedStatus": false,
"Add": 3
},
...
], "Page": 2, "PageSize": 55, "Total": 55 }
Artificial Delays
You can specify artificial delay for the API request as follows:
Above example shows delay based on page number but you can use any parameter to customize this behavior.
Conditional Logic
The template syntax allows you to define a conditional logic such as:
In above example, the mock API will return HTTP status 500 or 501 for every 10th request and 200 or 400 for other requests. You can use conditional syntax to simulate different error status or customize response.
Test fixtures
The mock service allows you to upload a test fixture that you can refer in your template, e.g.
Above example loads a random line from a lines.txt fixture. As you may need to generate a deterministic random data in some cases, you can use Seeded functions to generate predictable data so that the service returns same data. Following example will read a text fixture to load a property from a file:
This template file will generate content as follows:
Playing back a specific mock scenario
You can pass a header for Mock-Scenario to specify the name of scenario if you have multiple scenarios for the same API, e.g.
Using Test Fixtures
You can define a test data in your test fixtures and then upload as follows:
In above example, test fixtures for lines.txt
and props.yaml
will be uploaded and will be available for all GET
requests under /devices
URL path. You can then refer to above fixture in your templates. You can also use this to serve any binary files, e.g. you can define an image template file as follows:
Then upload a binary image using:
And then serve the image using:
Custom Functions
The API mock service defines following custom functions that can be used to generate test data:
Numeric Random Data
Following functions can be used to generate numeric data within a range or with a seed to always generate deterministic test data:
- Random
- SeededRandom
- RandNumMinMax
- RandIntArrayMinMax
Text Random Data
Following functions can be used to generate numeric data within a range or with a seed to always generate deterministic test data:
- RandStringMinMax
- RandStringArrayMinMax
- RandRegex
- RandEmail
- RandPhone
- RandDict
- RandCity
- RandName
- RandParagraph
- RandPhone
- RandSentence
- RandString
- RandStringMinMax
- RandWord
Email/Host/URL
- RandURL
- RandEmail
- RandHost
Boolean
Following functions can be used to generate boolean data:
- RandBool
- SeededBool
UDID
Following functions can be used to generate UDIDs:
- Udid
- SeededUdid
String Enums
Following functions can be used to generate a string from a set of Enum values:
- EnumString
Integer Enums
Following functions can be used to generate an integer from a set of Enum values:
- EnumInt
Random Names
Following functions can be used to generate random names:
- RandName
- SeededName
City Names
Following functions can be used to generate random city names:
- RandCity
- SeededCity
Country Names or Codes
Following functions can be used to generate random country names or codes:
- RandCountry
- SeededCountry
- RandCountryCode
- SeededCountryCode
File Fixture
Following functions can be used to generate random data from a fixture file:
- RandFileLine
- SeededFileLine
- FileProperty
- JSONFileProperty
- YAMLFileProperty
Generate Mock API Behavior from OpenAPI or Swagger Specifications
If you are using Open API or Swagger for API specifications, you can simply upload a YAML based API specification. For example, here is a sample Open API specification from Twilio:
You can then upload the API specification as:
It will generate a mock scenarios for each API based on mime-type, status-code, parameter formats, regex, data ranges, e.g.,
In above example, the account_sid uses regex to generate data and URI format to generate URL. Then invoke the mock API as:
Which will generate dynamic response as follows:
Listing all Mock Scenarios
You can list all available mock APIs using:
https://gist.github.com/bhatti/0afab789dfe3134b3fdaf90a2f38fb5a
Which will return summary of APIs such as:
Chaos Testing
In addition to serving a mock service, you can also use a builtin chaos client to test remote services for stochastic testing by generating random data based on regex or API specifications. For example, you may capture a test scenario for a remote API using http proxy such as:
export http_proxy="http://localhost:8081"
export https_proxy="http://localhost:8081"
curl -k https://jsonplaceholder.typicode.com/todos
This will capture a mock scenario such as:
method: GET
name: recorded-todos-ff9a8e133347f7f05273f15394f722a9bcc68bb0e734af05ba3dd98a6f2248d1
path: /todos
description: recorded at 2022-12-12 02:23:42.845176 +0000 UTC for https://jsonplaceholder.typicode.com:443/todos
group: todos
predicate: ""
request:
match_query_params: {}
match_headers:
Content-Type: ""
match_contents: '{}'
example_path_params: {}
example_query_params: {}
example_headers:
Accept: '*/*'
User-Agent: curl/7.65.2
example_contents: ""
response:
headers:
Access-Control-Allow-Credentials:
- "true"
Age:
- "19075"
Alt-Svc:
- h3=":443"; ma=86400, h3-29=":443"; ma=86400
Cache-Control:
- max-age=43200
Cf-Cache-Status:
- HIT
Cf-Ray:
- 7782ffe4bd6bc62c-SEA
Connection:
- keep-alive
Content-Type:
- application/json; charset=utf-8
Date:
- Mon, 12 Dec 2022 02:23:42 GMT
Etag:
- W/"5ef7-4Ad6/n39KWY9q6Ykm/ULNQ2F5IM"
Expires:
- "-1"
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Pragma:
- no-cache
contents: |-
[
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
},
{
"userId": 1,
"id": 2,
"title": "quis ut nam facilis et officia qui",
"completed": false
},
...
]
contents_file: ""
status_code: 200
match_headers: {}
match_contents: '{"completed":"__string__.+","id":"(__number__[+-]?[0-9]{1,10})","title":"(__string__\\w+)","userId":"(__number__[+-]?[0-9]{1,10})"}'
assertions: []
You can then customize this scenario with additional assertions and you may remove all response contents as they won’t be used. Note that above scenario is defined with group todos
. You can then submit a request for chaos testing as follows:
curl -k -v -X POST http://localhost:8080/_chaos/todos -d '{"base_url": "https://jsonplaceholder.typicode.com", "execution_times": 10}'
Above request will submit 10 requests to the todo server with random data and return response such as:
{"errors":null,"failed":0,"succeeded":10}
If you have a local captured data, you can also run chaos client with a command line without running mock server, e.g.:
go run main.go chaos --base_url https://jsonplaceholder.typicode.com --group todos --times 10
Static Assets
The mock service can serve any static assets from a user-defined folder and then serve it as follows:
Summary
Building and testing distributed systems often requires deploying a deep stack of dependent services, which makes development hard on a local environment with limited resources. Ideally, you should be able to deploy and test entire stack without using network or requiring a remote access so that you can spend more time on building features instead of configuring your local environment. Above examples show how you use the https://github.com/bhatti/api-mock-service to mock APIs for testing purpose and define test scenarios for simulating both happy and error cases as well as injecting faults or network delays in your testing processes so that you can test for fault tolerance. This mock library can be used to define the API mock behavior using record/play, template language or API specification standards. I have found a great use of tools like this when developing micro services and hopefully you find it useful. Feel free to connect with your feedback or suggestions.