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:

{{val (state 'basketItems' context=request.path.basketId) or='[]' assign='basketItems'}}
{
    "items": {{basketItems}},
    "total": {{size (parseJson basketItems)}}
}

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:

{
    "items": {{basketItems}},
    "total": {{size (parseJson basketItems)}}
}

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

{{jsonArrayAdd (val previousValue or='[]') request.body}}

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:

{{#assign 'findExpression'}}$.[?(@.id == '{{request.path.itemId}}')]{{/assign}}
{{jsonPath (jsonPath (state 'basketItems' context=request.path.basketId) findExpression) '$[0]'}}

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

{{#assign 'removalExpression'}}$.[?(@.id == '{{request.path.itemId}}')]{{/assign}}
{{jsonRemove previousValue removalExpression}}

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 [].)