Dynamic State Shopping Basket Example
To learn how to use WireMock Cloud’s Dynamic State capabilities, let’s look at a working example of using dynamic state to mock CRUD operations on a shopping basket.
Getting started
You can start your own copy of this example Mock API from a template just by clicking on this link: Launch Shopping Basket Mock
Exploring the behaviour
Get an empty basket
Let’s start by seeing what is in a shopping basket. Make a request to any basket:
curl -v 'https://my-basket-demo.wiremockapi.cloud/baskets/1/items'
You should see a response that looks like this:
{
"items" : [ ],
"total" : 0
}
You’ll get the same thing if you request this:
curl -v 'https://my-basket-demo.wiremockapi.cloud/baskets/2/items'
Add some items to some baskets & retrieve those baskets
Let’s add some items to some baskets. First let’s add a couple of items to the basket with id 1
:
curl -v -X POST \
-d '{ "id": "1", "item": "Socks", "quantity": 5 }' \
'https://my-basket-demo.wiremockapi.cloud/baskets/1/items'
curl -v -X POST \
-d '{ "id": "2", "item": "Shoes", "quantity": 3 }' \
'https://my-basket-demo.wiremockapi.cloud/baskets/1/items'
Now when we get the items in the basket with id 1
we should see the items we just added:
curl -v 'https://my-basket-demo.wiremockapi.cloud/baskets/1/items'
returns
{
"items" : [ {
"id" : "1",
"item" : "Socks",
"quantity" : 5
}, {
"id" : "2",
"item" : "Shoes",
"quantity" : 3
} ],
"total" : 2
}
However, when we retrieve a different basket it will still be empty:
curl -v 'https://my-basket-demo.wiremockapi.cloud/baskets/2/items'
returns
{
"items" : [ ],
"total" : 0
}
Have a play with adding items to different baskets and retrieving those baskets; you can use just about anything as the basketId
in the path template /baskets/{basketId}/items
.
Retrieve specific items from a basket
We can also retrieve a specific item from a basket by id. Try this:
curl -v 'https://my-basket-demo.wiremockapi.cloud/baskets/1/items/2'
It should return
{
"id" : "2",
"item" : "Shoes",
"quantity" : 3
}
However, if you try and get an item you never added, you should get a 404:
curl -v 'https://my-basket-demo.wiremockapi.cloud/baskets/1/items/56'
returns
{
"error": "Cannot find item id 56 in basket 1"
}
Remove an item from a basket
You can also remove a single item from a basket. Try this:
curl -v -X DELETE \
'https://my-basket-demo.wiremockapi.cloud/baskets/1/items/2'
curl -v 'https://my-basket-demo.wiremockapi.cloud/baskets/1/items'
should now return
{
"items" : [ {
"id" : "1",
"item" : "Socks",
"quantity" : 5
} ],
"total" : 1
}
and
curl -v 'https://my-basket-demo.wiremockapi.cloud/baskets/1/items/2'
should now return
{
"error": "Cannot find item id 2 in basket 1"
}
Entirely empty the basket
You can remove all items from a basket. Try this:
curl -v -X DELETE \
'https://my-basket-demo.wiremockapi.cloud/baskets/1/items'
curl -v 'https://my-basket-demo.wiremockapi.cloud/baskets/1/items'
should now return
{
"items" : [ ],
"total" : 0
}
Conclusion
You should now be pretty happy that your Mock API that you have spun up from the template is behaving like a fairly orthodox JSON over HTTP API, and crucially that it is both stateful, and that the state is localized to a specific basket id.
So let’s start to understand how it works.
How it Works
We’ll take it stub by stub, examining how the stub delivers the functionality we need.
The “Get basket contents” Stub
This stub uses the “Path template” match type for a method GET
and a path of /baskets/{basketId}/items
. That means it will accept any valid URI path segment as a basketId
. It will be available to Handlebars templates as request.path.basketId
.
You will note that the “State” section is still folded up. That’s because this is a read only stub; it doesn’t alter state at all. Any stub can render state variables in its response, you don’t need to toggle Dynamic State on to do that.
The stateful configuration comes in the Response definition. Unsurprisingly “Enable dynamic response templating” is checked. The body contains the following Handlebars template:
Let’s break it down.
In the first line, the state
helper reads the value of the key 'basketItems'
in the context request.path.basketId
. You’ll notice the key there is a string, but the context parameter is not - it looks up the basketId
path parameter from the request. Those two values are combined to look up the items.
At first the lookup will return null
, because we have never initialised the 'basketItems'
key in any context, including the current value of request.path.basketId
. To handle that case we use the val
helper, which takes a value as its context, but can use an or
option to default to a different value should that value be null
. In this case we default it to '[]'
; state values are all stored as strings, and we interpret them as JSON when we retrieve them.
Finally, we use the assign
option on the val
helper to put the result of the val
helper into the Handlebars model with the key basketItems
. From now on we can just use basketItems
in Handlebars helpers, or {{ basketItems }}
to render them.
Now let’s look at what we actually output:
This is just JSON with some Handlebars interpolations. "items": {{basketItems}}
renders the basketItems
key we defined in the val
helper above - either the value of the basketItems
state key or []
if it was null
.
"total": {{size (parseJson basketItems)}}
uses a combination of the parseJson
helper to turn basketItems
into JSON in memory (remember state is just stored as a string) and the size
helper to work out how big the resulting array is.
Let’s move on to adding items.
The “Add item” Stub
Like the “Get basket contents” stub, this stub uses the “Path template” match type for a path of /baskets/{basketId}/items
, but this time it matches if the method is POST
.
It also has a Body matcher, which uses JsonUnit expressions to only match the format of JSON we expect in a new item.
This time the “State” panel is opened, and “Dynamic state” is toggled on. This is because we now have a single “State operation” defined.
It has its “Context” set to {{request.path.basketId}}
. “Context” identifiers are just strings, but they can be built using Handlebars templates. In this case the context is derived from the basketId
path parameter on the request. It has its “Key” set to 'basketItems'
, its “Operation” to SET
and its “Value” to
Just like context identifiers, state values are just strings emitted by Handlebars templates. They have a special key provided to them, previousValue
; it is set to the value of the key in the context when the operation runs. It will be null
if the key has no value in this context. In this case we use the val
helper described above to default it to an empty JSON array string. We then use the jsonArrayAdd
helper to append the entire request body as a new item in the array. We know the request body will be a valid JSON object because of the Body matcher defined above.
The “Get item by ID (success)” Stub
This stub uses the “Path template” match type for a method GET
and a path of /baskets/{basketId}/items/{itemId}
. That means it will accept any valid URI path segment as a basketId
or an itemId
and expose them to Handlebars templates as request.path.basketId
and request.path.itemId
respectively.
Like the “Add item” stub, the “State” panel is opened, and “Dynamic state” is toggled on. However, instead of a state operation, this stub defines a request matcher. The matcher has its “Context” set to {{request.path.basketId}}
and its “Key” set to 'basketItems'
, just like the “Add item” state operation. However, it then has the standard matching form. In this case we use the “matches JSONPath” matcher to say that this stub only matches if the JSONPath expression $.[?(@.id == '{{request.path.itemId}}')]
finds an object. This expression assumes the top level JSON is an array, and looks for an item inside that array with a key id
and a value '{{request.path.itemId}}'
. You’ll note that the expression here is actually a Handlebars template, so it is able to match against parts of the request model.
This means that this entire stub will only match if the itemId
is in the basketItems
for the current basketId
context.
The request body (“Enable dynamic response templating” checked) then looks like this:
We’ve built up exactly the same JSONPath expression as in the matcher, and assigned it to the key findExpression
. We have inlined retrieving the basketItems
key from the state with the correct context using (state 'basketItems' context=request.path.basketId)
. We can then use the jsonPath
helper to extract that item from the basketItems
as so: jsonPath (state 'basketItems' context=request.path.basketId) findExpression
. We know it will be there because otherwise the stub would not have matched. Unfortunately JSONPath provides no way to debrief a result array as part of the expression, so we have to wrap it in a further jsonPath
helper call to retrieve the single element of the resulting array.
The “Get item by ID (not found)” Stub
The “Get item by ID (not found)” stub uses exactly the same GET
and path template /baskets/{basketId}/items/{itemId}
matchers as the “Get item by ID (success)” stub. However, it is set to a lower Priority (7) than the success stub (5), and consequently it only matches if the success stub does not match. This means we do not need to do any cleverness with dynamic state; we can simply make it return a 404 with a useful error message derived dynamically from the path.
The “Delete item by ID” Stub
The “Delete item by ID” stub uses the same path template /baskets/{basketId}/items/{itemId}
matcher as the “Get item by ID (success)” stub, but with the DELETE
method.
The “State” panel is opened, and “Dynamic state” is toggled on, with a single state operation very similar to the “Add item” stub. As we are used to it has its “Context” set to {{request.path.basketId}}
, its “Key” set to 'basketItems'
and its “Operation” to SET
. However, its “Value” is
The removalExpression
should be familiar - it’s the same JSONPath expression as we used in the “Get item by ID (success)” stub, both to match and to render the item in the basketItems
array. This time however we pass it to the jsonRemove
helper which will render an array without any of the elements from the previousValue
that match the expression.
The “Empty Basket” Stub
The “Empty Basket” stub uses the same path template /baskets/{basketId}/items
matcher as the “Get basket contents” stub, but with the DELETE
method.
The “State” panel is opened, and “Dynamic state” is toggled on, with a single state operation very similar to the “Add item” stub. As we are used to it has its “Context” set to {{request.path.basketId}}
and its “Key” set to 'basketItems'
. However, its “Operation” is set to DELETE
. This means that when this stub matches the basketItems
key will be entirely removed in the {{request.path.basketId}}
context.
(We could instead have used a SET
operation with a “Value” of []
.)