Adds support to transport state across different stubs.
- Store a state for a context
- overwrite states
- append new states a state list
- Delete a state
- delete states from state list (first, last, by index, by state propery comparison)
- Request matching against context existence/non-existence
- Response templating integration
- get state for a given context
- get state list entry by index
- Templating support in all configuration options of this extension
Term | Description |
---|---|
context |
States are scoped by a context. Behavior is similar to a key in a map. |
state |
The actual state. There can be only one per context - but it can be overwritten. |
property |
A property of a state . A state can have multiple properties. |
list |
Next to the singularic state, a context can have a list of states . The list of states can be modified but states within the list can't. |
classDiagram
direction LR
Store "1" *-- "*" Context
Context "1" *-- "1" State
Context "1" *-- "1" List
List "1" *-- "*" State
State "1" *-- "*" Property
class Property {
+String key
+String value
}
WireMock supports Response Templating and Scenarios to add dynamic behavior and state. Both approaches have limitations:
Response templating
only allows accessing data submitted in the same requestScenarios
cannot transport any data other than the state value itself
In order to mock more complex scenarios which are similar to a sandbox for a web service, it can be required to use parts of a previous request.
Create a sandbox for a webservice. The web service has two APIs:
POST
to create a new identity (POST /identity
)- Request:
{ "firstName": "John", "lastName": "Doe" }
- Response:
{ "id": "kn0ixsaswzrzcfzriytrdupnjnxor1is", # Random value "firstName": "John", "lastName": "Doe" }
GET
to retrieve this value (GET /identity/kn0ixsaswzrzcfzriytrdupnjnxor1is
)
-
Response:
{ "id": "kn0ixsaswzrzcfzriytrdupnjnxor1is", "firstName": "John", "lastName": "Doe" }
The sandbox should have no knowledge of the data that is inserted. While the POST
can be achieved
with Response Templating,
the GET
won't have any knowledge of the previous post.
POST
add a new item (POST /queue
)- Request:
{ "firstName": "John", "lastName": "Doe" }
- Response:
{ "id": "kn0ixsaswzrzcfzriytrdupnjnxor1is", # Random value "firstName": "John", "lastName": "Doe" }
2POST
add another new item (POST /queue
)
-
Request:
{ "firstName": "Jane", "lastName": "Doe" }
-
Response:
{ "id": "54owywgurlqepq1wc5xvyc2hipe8xp4u", # Random value "firstName": "Jane", "lastName": "Doe" }
GET
to retrieve the first value (GET /queue
)
-
Response:
{ "id": "kn0ixsaswzrzcfzriytrdupnjnxor1is", "firstName": "John", "lastName": "Doe" }
GET
to retrieve the second value (GET /queue
)
-
Response:
{ "id": "54owywgurlqepq1wc5xvyc2hipe8xp4u", "firstName": "Jane", "lastName": "Doe" }
wiremock-state-extension version |
WireMock version |
---|---|
0.0.3 + |
3.0.0-beta-11 + |
0.0.6 + |
3.0.0-beta-14 + |
0.1.0 + |
3.0.0 + |
dependencies {
testImplementation("org.wiremock.extensions:wiremock-state-extension:<your-version>")
}
<dependencies>
<dependency>
<groupId>org.wiremock.extensions</groupId>
<artifactId>wiremock-state-extension</artifactId>
<version>your-version</version>
<scope>test</scope>
</dependency>
</dependencies>
You can also install the dependencies from GitHub Packages. Follow the instructions on GitHub Docs to add authentication to GitHub packages.
Use GitHub Packages in Gradle
repositories {
maven {
url = uri("https://maven.pkg.github.com/wiremock/wiremock-extension-state")
}
}
dependencies {
testImplementation("org.wiremock.extensions:wiremock-state-extension:<your-version>")
}
Use GitHub Packages in Maven
```xml github-wiremock-state-extension WireMock Extension State Apache Maven Packages https://maven.pkg.github.com/wiremock/wiremock-state-extension org.wiremock.extensions wiremock-state-extension your-version test ```This extension makes use of WireMock's ExtensionFactory
, so only one extension has to be registered: StateExtension
.
In order to use them, templating has to be enabled as well. A store for all state data has to be provided. This extension
provides a CaffeineStore
which can be used - or you can provide your own store:
public class MySandbox {
private final WireMockServer server;
public MySandbox() {
var stateRecordingAction = new StateRecordingAction();
var store = new CaffeineStore();
server = new WireMockServer(
options()
.dynamicPort()
.templatingEnabled(true)
.globalTemplating(true)
.extensions(new StateExtension(store))
);
server.start();
}
}
This extension uses the ServiceLoader
extension to be loaded by WireMock. As Standalone version, it will use CaffeineStore
for
storing any data.
The standalone jar can be downloaded from GitHub .
java -cp "wiremock-state-extension-standalone-0.4.0.jar:wiremock-standalone-3.3.0.jar" wiremock.Run
Using the extension with docker is similar to its usage with usage standalone: it just has to be available on
the classpath to be loaded automatically - it does not have to be added via --extensions
.
Note: This extension depends on the current WireMock beta development, thus the tag 3x
has to be used:
docker run -it --rm \
-p 8080:8080 \
--name wiremock \
-v $PWD/extensions:/var/wiremock/extensions \
wiremock/wiremock:3.3.0 \
-- --global-response-templating
The state is recorded in serveEventListeners
of a stub. The following functionalities are provided:
state
: stores a state in a context. Storing the state multiple times can be used to selectively overwrite existing properties.- to delete a selective property, set it to
null
(as string).
- to delete a selective property, set it to
list
: stores a state in a list. Can be used to prepend/append new states to an existing list. List elements cannot be modified (only read/deleted).
state
and list
can be used in the same ServeEventListener
(would count as ONE updates). Adding multiple recordState
ServeEventListener
is supported.
The following parameters have to be provided:
Parameter | Type | Example |
---|---|---|
|
String |
|
|
Object |
{
"id": "{{jsonPath response.body '$.id'}}",
"firstName": "{{jsonPath request.body '$.firstName'}}",
"lastName": "{{jsonPath request.body '$.lastName'}}"
} |
|
Dictionary
|
{
"addLast": {
"id": "{{jsonPath response.body '$.id'}}",
"firstName": "{{jsonPath request.body '$.firstName'}}",
"lastName": "{{jsonPath request.body '$.lastName'}}"
}
} |
Templating (as in Response Templating) is supported for these. The following models are exposed:
request
: All model elements of as in Response Templatingresponse
:body
andheaders
Full example for storing a state:
{
"request": {},
"response": {},
"serveEventListeners": [
{
"name": "recordState",
"parameters": {
"context": "{{jsonPath response.body '$.id'}}",
"state": {
"id": "{{jsonPath response.body '$.id'}}",
"firstName": "{{jsonPath request.body '$.firstName'}}",
"lastName": "{{jsonPath request.body '$.lastName'}}"
}
}
}
]
}
To record a complete response body, use:
{
"request": {},
"response": {},
"serveEventListeners": [
{
"name": "recordState",
"parameters": {
"context": "{{jsonPath response.body '$.id'}}",
"state": {
"fullBody": "{{jsonPath response.body '$'}}"
}
}
}
]
}
To delete a selective property, ensure that the field has the value null
as string, e.g. by specifying default='null
for jsonpath
:
{
"request": {},
"response": {},
"serveEventListeners": [
{
"name": "recordState",
"parameters": {
"context": "{{jsonPath response.body '$.id'}}",
"state": {
"id": "{{jsonPath response.body '$.id'}}",
"firstName": "{{jsonPath request.body '$.firstName' default='null'}}",
"lastName": "{{jsonPath request.body '$.lastName' default='null'}}"
}
}
}
]
}
To append a state to a list:
{
"request": {},
"response": {},
"serveEventListeners": [
{
"name": "recordState",
"parameters": {
"context": "{{jsonPath response.body '$.id'}}",
"list": {
"addLast": {
"id": "{{jsonPath response.body '$.id'}}",
"firstName": "{{jsonPath request.body '$.firstName'}}",
"lastName": "{{jsonPath request.body '$.lastName'}}"
}
}
}
}
]
}
You can use the state
helper to temporarily access the previous state. Use the state
helper in the same way as you would use it when
you retrieve a state.
Note: This extension does not keep a history in itself but it's an effect of the evaluation order.
As templates are evaluated before the state is written, the state you access in recordState
is the one before you store the new one
(so there might be none - you might want to use default
for these cases). In case you have multiple recordState
serveEventListeners
, you will have new
states
being created in between, thus the previous state is the last stored one (so: not the one before the request).
- listener 1 is executed
- accesses state n
- stores state n+1
- listener 2 is executed
- accesses state n+1
- stores state n+2
The evaluation order of listeners within a stub as well as across stubs is not guaranteed.
{
"request": {},
"response": {},
"serveEventListeners": [
{
"name": "recordState",
"parameters": {
"context": "{{jsonPath response.body '$.id'}}",
"state": {
"id": "{{jsonPath response.body '$.id'}}",
"firstName": "{{jsonPath request.body '$.firstName'}}",
"lastName": "{{jsonPath request.body '$.lastName'}}",
"birthName": "{{state context='$.id' property='lastName' default=''}}"
}
}
}
]
}
Similar to recording a state, its deletion can be initiated in serveEventListeners
of a stub.
The following parameters have to be provided:
Task | Parameter | Type | Example |
---|---|---|---|
context deletion |
|
String |
|
|
Array An empty array or unknown contexts are silently ignored. |
|
|
|
String (regex) An invalid regex results in an exception. If there are no matches, this is silently ignored. |
|
|
List entry deletion |
If |
Dictionary - only one option is interpreted (top to bottom as listed here)
|
|
Templating (as in Response Templating) is supported for these. The following models are exposed:
request
: All model elements of as in Response Templatingresponse
:body
andheaders
Full example:
{
"request": {},
"response": {},
"serveEventListeners": [
{
"name": "deleteState",
"parameters": {
"context": "{{jsonPath response.body '$.id'}}"
}
}
]
}
This extension provides a CaffeineStore
which uses caffeine to store the current state and to achieve an expiration (
to avoid memory leaks).
The default expiration is 60 minutes. The default value can be overwritten (0
= default = 60 minutes):
int expiration = 1024;
var store = new CaffeineStore(expiration);
To have a WireMock stub only apply when there's actually a matching context, you can use the StateRequestMatcher
. This helps to model different
behavior for requests with and without a matching context. The parameter supports templates.
{
"request": {
"method": "GET",
"urlPattern": "/test/[^\/]+",
"customMatcher": {
"name": "state-matcher",
"parameters": {
"hasContext": "{{request.pathSegments.[1]}}"
}
}
},
"response": {
"status": 200
}
}
In addition to the existence of a context, you can check for the existence or absence of a property within that context. The following matchers are available:
hasProperty
hasNotProperty
As for other matchers, templating is supported.
{
"request": {
"method": "GET",
"urlPattern": "/test/[^\/]+/[^\/]+",
"customMatcher": {
"name": "state-matcher",
"parameters": {
"hasContext": "{{request.pathSegments.[1]}}",
"hasProperty": "{{request.pathSegments.[2]}}"
}
}
},
"response": {
"status": 200
}
}
In case you want full flexibility into matching on a property, you can simply specify property
and use one of WireMock's built-in matchers, allowing you to
configure
logical operators, regex, date matchers, absence and much more. The basic syntax:
"property": {
<property-a>: <matcher-a>,
<property-b>: <matcher-b>
}
Example:
{
"request": {
"method": "GET",
"urlPattern": "/test/[^\/]+/[^\/]+",
"customMatcher": {
"name": "state-matcher",
"parameters": {
"property": {
"myProperty": {
"contains": "myValue"
}
}
}
},
"response": {
"status": 200
}
}
The implementation makes use of WireMock's internal matching system and supports any implementation of StringValuePattern
. As of WireMock 3.3, this includes
equalTo
,equalToJson
,matchesJsonPath
,matchesJsonSchema
,equalToXml
,matchesXPath
,contains
,not
,doesNotContain
,matches
,doesNotMatch
,before
,
after
,equalToDateTime
,anything
,absent
,and
,or
,matchesPathTemplate
.
For documentation on using these matchers, check the WireMock documentation
Whenever a request with a serve event listener recordState
or deleteState
is processed, the internal context update counter is increased.
The update count is increased by one whenever there is at least one change to a context (so: property adding/change, list entry addition/deletion). Multiple
event listeners with multiple changes of a single context within a single request only result in an increase by one.
for request matching as well. The following matchers are available:
updateCountEqualTo
updateCountLessThan
updateCountMoreThan
As for other matchers, templating is supported. In case the provided value for this check is not numeric, it is handled as non-matching. No error will be reported or logged.
{
"request": {
"method": "GET",
"urlPattern": "/test/[^\/]+",
"customMatcher": {
"name": "state-matcher",
"parameters": {
"hasContext": "{{request.pathSegments.[1]}}",
"updateCountEqualTo": "1"
}
}
},
"response": {
"status": 200
}
}
The list size (which is modified via recordState
or deleteState
) can be used
for request matching as well. The following matchers are available:
listSizeEqualTo
listSizeLessThan
listSizeMoreThan
As for other matchers, templating is supported. In case the provided value for this check is not numeric, it is handled as non-matching. No error will be reported or logged.
{
"request": {
"method": "GET",
"urlPattern": "/test/[^\/]+",
"customMatcher": {
"name": "state-matcher",
"parameters": {
"hasContext": "{{request.pathSegments.[1]}}",
"listSizeEqualTo": "1"
}
}
},
"response": {
"status": 200
}
}
Similar to properties, you have full flexibility into matching on a property of a list entry by specifying list
and using one of WireMock's built-in matchers
The basic syntax:
"list": {
<index-a>: {
<property-a>: <matcher-a>,
<property-b>: <matcher-b>
},
<index-b>: {
<property-a>: <matcher-a>,
<property-b>: <matcher-b>
}
}
As index, you can use the actual index as well as first
, last
, -1
.
Example:
{
"request": {
"method": "GET",
"urlPattern": "/test/[^\/]+/[^\/]+",
"customMatcher": {
"name": "state-matcher",
"parameters": {
"list": {
"1": {
"myProperty": {
"contains": "myValue"
}
}
}
}
},
"response": {
"status": 200
}
}
The implementation makes use of WireMock's internal matching system and supports any implementation of StringValuePattern
. As of WireMock 3.3, this includes
equalTo
,equalToJson
,matchesJsonPath
,matchesJsonSchema
,equalToXml
,matchesXPath
,contains
,not
,doesNotContain
,matches
,doesNotMatch
,before
,
after
,equalToDateTime
,anything
,absent
,and
,or
,matchesPathTemplate
.
For documentation on using these matchers, check the WireMock documentation
{
"request": {
"method": "GET",
"urlPattern": "/test/[^\/]+",
"customMatcher": {
"name": "state-matcher",
"parameters": {
"hasNotContext": "{{request.pathSegments.[1]}}"
}
}
},
"response": {
"status": 400
}
}
A state can be retrieved using a handlebar helper. In the example above, the StateHelper
is registered by the name state
.
In a jsonBody
, the state can be retrieved via: "clientId": "{{state context=request.pathSegments.[1] property='firstname'}}",
The handler has the following parameters:
context
: has to match the context data was registered withproperty
: the property of the state context to retrieve, so e.g.firstName
property='updateCount
retrieves the number of updates to a certain state. The number matches the one described in Context update count matchproperty='listSize
retrieves the number of entries oflist
property='list
get the whole list as array, e.g. to use it with handlebars #each- this property always has a default value (empty list), which can be overwritten with a JSON list
list
: Getting an entry of the context'slist
, identified via a JSON path. Examples:- getting the first state in the list:
list='[0].myProperty
- getting the last state in the list:
list='[-1].myProperty
- getting an element based on a path segment::
list=(join '[' request.pathSegments.[1] '].myProperty' '')
- getting the first state in the list:
default
(Optional): value to return in case the context or property wasn't found. Without a default value, an error message would be returned instead.
You have to choose either property
or list
(otherwise, you will get a configuration error).
To retrieve a full body, use tripple braces: {{{state context=request.pathSegments.[1] property='fullBody'}}}
.
When registering this extension, this helper is available via WireMock's response templating as well as in all configuration options of this extension.
You can use handlebars #each to build a full JSON response with the current list's content.
Things to consider:
- this syntax only works with
body
. It DOES NOT work withjsonBody
- as this might get ugly, consider using
bodyFileName
/withBodyFile()
have proper indentation
- as this might get ugly, consider using
- the default response for non-existent context as well as non-existent list in a context is an empty list. These states cannot be differentiated here
- if you still want a different response, consider using a StateRequestMatcher
- the default value for this property has to be a valid JSON list - otherwise you will get an error log and the empty list response
- JSON does not allow trailing commas, so in order to create a valid JSON list, use
{{#unless @last}},{{/unless}
before{{/each}}
Example with inline body:
{
"request": {
"urlPathPattern": "/listing",
"method": "GET"
},
"response": {
"status": 200,
"body": "[\n{{# each (state context='list' property='list' default='[]') }} {\n \"id\": \"{{id}}\",\n \"firstName\": \"{{firstName}}\",\n \"lastName\": \"{{lastName}}\" }{{#unless @last}},{{/unless}}\n{{/each}}]",
"headers": {
"content-type": "application/json"
}
}
}
Example with bodyFileName:
{
"request": {
"urlPathPattern": "/listing",
"method": "GET"
},
"response": {
"status": 200,
"bodyFileName": "body.json",
"headers": {
"content-type": "application/json"
}
}
}
[
{{# each (state context='list' property='list' default='[]') }}
{
"id": {{id}},
"firstName": "{{firstName}}",
"lastName": "{{lastName}}"
}{{#unless @last}},{{/unless}}
{{/each}}
]
Missing Helper properties as well as unknown context properties result in using a built-in default.
You can also specify a default
for the state
helper: "clientId": "{{state context=request.pathSegments.[1] property='firstname' default='John'}}",
.
If unsure, you may consult the log for to see whether an error occurred.
Properties and their defaults:
Property | Built-in | Interprets default |
---|---|---|
updateCount |
"0" (0 as string) |
yes |
listSize (when context is not present) |
"0" (0 as string) |
yes |
listSize (when context is present) |
not applied as list is present but empty | not applied as list is present but empty |
list (when context is not present) |
[] (empty list) |
yes |
list (when context is present) |
not applied as list is present but empty | not applied as list is present but empty |
any other state property | "" (empty string) |
yes |
any other list property | "" (empty string) |
yes |
Defaults have to be strings or valid objects in order to result in proper JSONs in all configuration scenarios. In order to create
a JSON response with a null
property or to ignore unknown properties in your resulting JSON, you may consider using a body file
with handlebar logic to create the JSON you need: handlebar interprets an empty string as false
.
body file with handlebars to create myProperty=null
:
{
{{#with (state context=request.pathSegments.[1] property='myProperty') as | value |}}
"myProperty": "{{value}}"
{{else}}
"myProperty": null
{{/with}}
}
body file with handlebars to ignore a missing property:
{
{{#with (state context=request.pathSegments.[1] property='myProperty') as | value |}}
"myProperty": "{{value}}"
{{else}}
{{/with}}
}
This extension is at the moment not optimized for distributed setups or high degrees concurrency. While it will basically work, there are some limitations that should be held into account:
- The store used for storing the state is on instance-level only
- while it can be exchanged for a distributed store, any atomicity assurance on instance level is not replicated to the distributed setup. Thus concurrent operations on different instances might result in state overwrites
- Lock-level is basically the whole context store
- while the lock time is kept small, this can still impact measurements when being used in load tests
- Single updates to contexts (property additions or changes, list entry additions or deletions) are atomic on instance level
- Concurrent requests are currently allowed to change the same context. Atomicity prevents overwrites but does not provide something like a transaction, so: the context can change while a request is performed
For any kind of usage with parallel write requests, it's recommended to use a different context for each parallel stream.
In general, you can increase verbosity, either by register a notifier
and setting verbose=true
or starting WireMock standalone (or docker) with verbose=true
.
- EventListeners and Matchers report errors with WireMock-internal exceptions. Additionally, errors are logged. In order to see them, register a notifier.
- Response templating errors are printed in the actual response body.
- Various actions and decisions of this extensions are logged on info level, along with the context they are happening in.
Various test examples can be found in the tests of this extension.
JSON stub mapping can be found in the resource files of the tests .