Using Wiremock to Mock API Responses: Part 3 - Response Templating using Handlebars Helpers

October 18, 2021

In the previous 2 parts of this series, we:

  • Part 1: introduced Wiremock, and showed how it could be used to mock a simple response from a dummy “Songs API”
  • Part 2: showed how to return dynamic responses using details from the incoming request (i.e. request parameters)

In this part, we’ll continue with the topic of dynamic responses, but this time making use of Handlebars Helpers, as well as other built-in helpers, to generate even more dynamic responses.

This post is full of practical examples to ease understanding of Handlebars usage.

Note: Before proceeding, please ensure you’ve enabled response templating in Wiremock; this has been outlined in Part 2 of the series.

Let’s take another look at our Songs API response which we’re using for a demo:

{
    "metadata": {
        "searchQueryParam": "liked",
        "requestMethodType": "GET"
    },
    "data": [
        {
            "id": "8efe58ee-a5cf-4926-bcd2-92c7e2ec82ba",
            "songInfo": "Jam Now, Simmer Down - Blinky Bill",
            "likedOn": "2021-06-19",
            "listenCount": 34
        },
        {
            "id": "724d520f-15b0-4c2f-9999-a28b2531195c",
            "songInfo": "Dunia Ina Mambo - Just a Band",
            "likedOn": "2020-12-04",
            "listenCount": 182
        }
    ]
}

Let’s say, for example, that we want the songInfo to be in UPPER CASE; i.e. have the API return:

{
    "metadata": {
        "requestMethodType": "GET"
    },
    "data": [
        {
            "id": "8efe58ee-a5cf-4926-bcd2-92c7e2ec82ba",
            "songInfo": "JAM NOW, SIMMER DOWN - BLINKY BILL",
            "likedOn": "2021-06-19",
            "listenCount": 34
        },
        {
            "id": "724d520f-15b0-4c2f-9999-a28b2531195c",
            "songInfo": "DUNIA INA MAMBO - JUST A BAND",
            "likedOn": "2020-12-04",
            "listenCount": 182
        }
    ]
}

We can make use of the upper helper which is enabled by Handlebars:

{
    "metadata": {
        ...
    },
    "request": {
        "method": "ANY",
        "urlPattern": "/api/songs\\?dynamic=true"
    },
    "response": {
        ...
        "jsonBody": {
            "metadata": {
                "searchQueryParam": "{{ request.query.search.0 }}",
                "requestMethodType": "{{ request.method }}"
            },
            "data": [
                {
                    "id": "8efe58ee-a5cf-4926-bcd2-92c7e2ec82ba",
                    "songInfo": "{{ upper 'Jam Now, Simmer Down - Blinky Bill' }}",
                    "likedOn": "2021-06-19",
                    "listenCount": 34
                },
                {
                    "id": "724d520f-15b0-4c2f-9999-a28b2531195c",
                    "songInfo": "{{ upper 'Dunia Ina Mambo - Just a Band' }}",
                    "likedOn": "2020-12-04",
                    "listenCount": 182
                }
            ]
        }
    }
}

Note: we’re using a new URL pattern (dynamic=true) to allow differentiation between this and the previous version in Part 2.

Handlebars also provides a helper to get the current timestamp.

For example, let’s say we wanted to add a "requestedAt" field to the response metadata. For this, we can use the now helper:

{
    "metadata": {
        ...
    },
    "request": {
       ...
    },
    "response": {
        ...
        "jsonBody": {
            "metadata": {
                ...
                "requestedAt": "{{ now }}"
            },
            "data": [
                {
                    ...
                },
                {
                    ...
                }
            ]
        }
    }
}

The response would be:

{
  "metadata": {
    "requestMethodType": "GET",
    "requestedAt": "2021-09-26T20:31:32Z"
  },
  "data": [
    {
      "id": "8efe58ee-a5cf-4926-bcd2-92c7e2ec82ba",
      "songInfo": "JAM NOW, SIMMER DOWN - BLINKY BILL",
      "likedOn": "Monday, June 21, 2021",
      "listenCount": 34
    },
    {
      "id": "724d520f-15b0-4c2f-9999-a28b2531195c",
      "songInfo": "DUNIA INA MAMBO - JUST A BAND",
      "likedOn": "Saturday, December 4, 2021",
      "listenCount": 182
    }
  ]
}

Handlebars also provides date helpers to allow for formatting of timestamps.

For example, let’s say we wanted the likedOn date format to be something like ‘Saturday, 4th December, 2021’. To do this, we can use the dateFormat helper:

{
    "metadata": {
        ...
    },
    "request": {
        "method": "ANY",
        "urlPattern": "/api/songs\\?dynamic=true"
    },
    "response": {
        ...
        "jsonBody": {
            ...
            "data": [
                {
                    "id": "8efe58ee-a5cf-4926-bcd2-92c7e2ec82ba",
                    "songInfo": "{{ upper 'Jam Now, Simmer Down - Blinky Bill' }}",
                    "likedOn": "{{ dateFormat '2021-06-19' full }}",
                    "listenCount": 34
                },
                {
                    "id": "724d520f-15b0-4c2f-9999-a28b2531195c",
                    "songInfo": "{{ upper 'Dunia Ina Mambo - Just a Band' }}",
                    "likedOn": "{{ dateFormat '2020-12-04' full }}",
                    "listenCount": 182
                }
            ]
        }
    }
}

This would produce:

{
  "metadata": {
    "requestMethodType": "GET",
    "requestedAt": "2021-10-17T15:27:32Z"
  },
  "data": [
    {
      "id": "8efe58ee-a5cf-4926-bcd2-92c7e2ec82ba",
      "songInfo": "JAM NOW, SIMMER DOWN - BLINKY BILL",
      "likedOn": "Monday, June 21, 2021",
      "listenCount": 34
    },
    {
      "id": "724d520f-15b0-4c2f-9999-a28b2531195c",
      "songInfo": "DUNIA INA MAMBO - JUST A BAND",
      "likedOn": "Saturday, December 4, 2021",
      "listenCount": 182
    }
  ]
}

upper, now, dateFormat are only a subset of all the available helpers.

For a full list, check out the StringHelpers.java file on the Handlebars repo.

Additionally, I’ve provided a “kitchen sink” API that demonstrates all of these helpers.

{
    "metadata": {
        "description": "Provides a demo API utilizing all the supported Handlebars String Helpers in Wiremock",
        "blogPost": "https://blog.muya.co.ke/wiremock-response-templating-part-3/"
    },
    "request": {
        "method": "GET",
        "urlPattern": "/handlebarsKitchenSink"
    },
    "response": {
        "status": 200,
        "headers": {
            "Content-Type": "application/json"
        },
        "jsonBody": {
            "abbreviate": "{{ abbreviate 'Truncate long sentence up to # of characters and add ellipses' 28 }}",
            "capitalize": {
                "capitalize first letter of all words": "{{ capitalize 'ONLY first letter capitalized' }}",
                "capitalize first letter of all words AND lower case other characters": "{{ capitalize 'FULLY first letter capitalized' fully=true }}"
            },
            "capitalizeFirst": "{{ capitalize 'only first string' }}",
            "center": {
                "center a string": "{{ center 'centerAStringWithEmptySpaces' size=40 }}",
                "center string with padding": "{{ center 'centerAStringWithAsteriskPadding' size=40 pad='*'  }}"
            },
            "cut": {
                "remove number 7 from a string": "{{ cut 'string 7 with 7 number 7s' '7' }}",
                "remove spaces from a string": "{{ cut 'string with spaces' }}"
            },
            "dateFormat": {
                "display current date and time in custom format": "{{ dateFormat (now)  format='yyyy-MM-dd HH:mm:ss'}}",
                "display current date in full format": "{{ dateFormat (now) format='full' }}",
                "display current date in medium format": "{{ dateFormat (now) format='medium' }}",
                "display current date in short format": "{{ dateFormat (now) format='short' }}",
                "parse timestamp from value in specific format, and display in full format": "{{ dateFormat (parseDate '2021-06-21' format='yyyy-MM-dd') format='full' }}"
            },
            "defaultIfEmpty": {
                "set NOTHING as value provided value is empty ": "{{ defaultIfEmpty '' 'NOTHING'  }}",
                "set empty string if value provided is empty": "{{ defaultIfEmpty ''  }}"
            },
            "join": {
                "join a list of items with custom joiner (last item in list is considered the joiner)": "{{ join 'a' 'b' '-' }}",
                "join a list of items with custom joiner and prefix": "{{ join 'a' 'b' '-' prefix='[' }}",
                "join a list of items with custom joiner and suffix": "{{ join 'a' 'b' '-' suffix=']' }}",
                "join a list of items with custom joiner, prefix and suffix": "{{ join 'a' 'b' '-' prefix='[' suffix=']' }}"
            },
            "ljust": {
                "left align a given string in a 30 width space": "{{ ljust 'left aligned' size=30 }}",
                "left align a given string in a 30 width space with padding": "{{ ljust 'left aligned' size=30 pad='*' }}"
            },
            "lower": "{{ lower 'CHANGE VALUE TO LOWER CASE' }}",
            "now": {
                "display current date time": "{{ now }}",
                "display current date time in custom format": "{{ now format='yyyy-MM-dd HH:mm:ss.SSSSSS' tz='Africa/Nairobi' }}"
            },
            "numberFormat": {
                "format number in currency format": "{{ numberFormat 30 'currency' }}",
                "format number in currency format with locale": "{{ numberFormat 30 'currency' 'fr'}}",
                "format number in custom decimal format": "{{ numberFormat 3000000 '#,###,##0.000' }}",
                "format number in integer format": "{{ numberFormat 30 'integer' }}",
                "format number in percent format": "{{ numberFormat 30 'percent' }}",
                "format number with defined maximum integer and fraction digits": "{{ numberFormat 4542.3733 maximumFractionDigits=3 maximumIntegerDigits=2 }}",
                "format number with defined minimum integer and fraction digits": "{{ numberFormat 0.37 minimumFractionDigits=3 minimumIntegerDigits=2 }}"
            },
            "replace": "{{ replace 'Replaces placeholder with another string' 'another string' 'provided replacement' }}",
            "rjust": {
                "right align a given string in a 30 width space": "{{ rjust 'right aligned' size=30 }}",
                "right align a given string in a 30 width space with padding": "{{ rjust 'right aligned' size=30 pad='*' }}"
            },
            "slugify": "{{ slugify 'Creates A Slug Useful For Blog Post URLs' }}",
            "stringFormat": {
                "boolean": "{{ stringFormat 'isSet: %b isNotSet: %b' 'yes' null }}",
                "string": "{{ stringFormat 'applies %s formatting capabilities. All Java formatting options supported.' 'string' }}"
            },
            "stripTags": "{{ stripTags '<span>Removes all (X)HTML tags</span>' }}",
            "substring": {
                "substring from 3rd (exclusive) to 7th character": "{{ substring '0123456789' 3 7 }}",
                "substring from 5th character (exclusive)": "{{ substring '0123456789' 5 }}"
            },
            "upper": "{{ upper 'change value to upper case' }}",
            "yesno": {
                "set true/false/null to yes/no/maybe": "{{ yesno true }} | {{ yesno false }} | {{ yesno null }}",
                "set true/false/null to ndio/hapana/labda (Swahili)": "{{ yesno true yes='ndio' }} | {{ yesno false no='hapana'  }} | {{ yesno null maybe='labda' }}",
                "set true/false/null to sí/no/quizás (Spanish)": "{{ yesno true yes='sí' }} | {{ yesno false no='no'  }} | {{ yesno null maybe='quizás' }}"
            }
        }
    }
}

NB: Some special notes on exclusions from the kitchen sink:

  • There seems to be a bug with handling of JSON that has special characters (e.g. newline characters) between Handlebars and Wiremock. As a result, the following helpers have not been added to the kitchen sink: wordWrap
  • numberFormat has flags for parseIntegerOnly and roundingMode, but these don’t seem to be in use

We’ll use these exclusions as use-cases later in this series to build custom extensions for Wiremock.

Summary

In this part of the series, we dove deeper into Response Templating in Wiremock, seeing how to make use of Handlebars Helpers to enable us to generate even more dynamic responses.

We also showed how (almost) all of the string helpers can be used via a “Kitchen Sink API” (this is available on this series’ accompanying repository on GitHub: wiremock-docker-demo).

References

Shout out to rodolpheche for building and maintaining the Wiremock docker image.

All the code used in this blog post is available in this repository: wiremock-docker-demo

Coming in Part 4: Using Custom Extensions in Wiremock

In the next part, we’ll see how to load custom extensions to Wiremock, for when we want to enable very customized functionality in our mocked APIs.

We’ll use the custom extensions to go around some of the exclusions noted above.

Until then, happy coding, and stay safe!