Mock HTTP API for Testing with Wiremock
Wiremock is an open-source tool used for mocking API responses. It helps developers to write reliable and efficient tests for their APIs by mimicking the behavior of external services and improve the overall quality of software.It allows them to simulate various scenarios and responses without having to set up complicated infrastructure or rely on external dependencies.
In this blog post, I will explain features and benefits of Wiremock and providing examples of how it can be used in practice.
For demonstrating the mocking API with the help of Spring Boot projects which calls some external services.
How Wiremock works
Wiremock starts local http server which mimics the external service, then we configure application to call the local http server instead of external service during the testing and send the response which mimics the different scenarios.
Adding dependency
To use the wiremock first we need to add the following dependency
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>3.0.0-beta-8</version>
<scope>test</scope>
</dependency>
Code language: Java (java)
Sample Application
To demonstrate Wiremock working we will build a sample application which is going to call external services.
Given a currency code, we will call currency conversion service which gives equivalent of USD conversion rate.
Given a country name,we will call a service to get the currency code of the nation then again call the currency conversion service to get the USD conversion rate.
@RestController
@RequestMapping("/currencyCodeVersion")
@Slf4j
public class CurrencyConverterController {
@Autowired
private RestTemplate restTemplate;
@Value("${currencyconverter.url}")
private String currencyConverterUrl;
@Value("${country.url}")
private String countryUrl;
@GetMapping( "/currencyCode/{currencyCode}")
public String convertByCurrencyCode(@PathVariable String currencyCode) {
try {
String currencyConversionUrl = String.format(currencyConverterUrl,currencyCode.toLowerCase());
Object obj = restTemplate.getForObject(currencyConversionUrl,Object.class);
if ( obj == null) {
throw new RestClientException("no information found");
}
return obj.toString();
} catch (HttpClientErrorException e) {
throw new ResponseStatusException(e.getStatusCode(),e.getMessage());
}
}
@GetMapping( "/country/{countryCode}")
public String convertCurrencyByCountryCode(@PathVariable String countryCode) {
String countryurl = String.format(countryUrl,countryCode.toLowerCase());
Object obj = null;
try {
obj = restTemplate.getForObject(countryurl,Object.class);
} catch (HttpClientErrorException e) {
throw new ResponseStatusException(e.getStatusCode(),e.getMessage());
}
if ( obj == null) {
throw new RestClientException("no information found");
}
int index = obj.toString().indexOf("currencies");
int index2 = obj.toString().indexOf("idd");
if (index == -1 || index2 == -1 ) {
Map<String,String> responseMap = (Map<String,String>)obj;
throw new ResponseStatusException(HttpStatus.valueOf(404),responseMap.get("message"));
}
String currency = obj.toString().substring(index,index2);
Pattern p = Pattern.compile("([A-Z])\\w+=" );
log.info("currencies :{}", currency);
Matcher m = p.matcher(currency);
String currencyCode = "";
if (m.find()) {
currencyCode = m.group().substring(0,m.group().length()-1);
}
if ("".equals(currencyCode)) {
throw new ResponseStatusException(HttpStatus.valueOf(404),"CurrencyCode not found");
}
try {
String currencyConversionUrl = String.format(currencyConverterUrl,currencyCode.toLowerCase());
Object obj2 = restTemplate.getForObject(currencyConversionUrl,Object.class);
if ( obj2 == null) {
throw new RestClientException("no information found");
}
return obj2.toString();
} catch (HttpClientErrorException e) {
throw new ResponseStatusException(e.getStatusCode(),e.getMessage());
}
}
}
Code language: Java (java)
We are going to use following service to get the country’s currency code and it conversion rate to USD.
https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/usd/<currency-code>.json
Code language: Java (java)
https://restcountries.com/v3.1/name/<country-code>
Code language: Java (java)
Place following properties in the application.properties file
currencyconverter.url=https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/usd/%s.json
country.url=https://restcountries.com/v3.1/name/%s
Code language: Java (java)
Using Wiremock for Testing
Wiremock has JUnit Jupiter extension which simplifies running of one or more WireMock instances in a Junit5 test class.
It supports two modes of operation
- declarative (simple, limited configuration options)
- programmatic (verbose, highly configurable)
Declarative Usage
The Wiremock server can be started by annotating your test class with @WireMockTest.
This will run a single WireMock server, defaulting to a random port, HTTP only
You can also specify the port number @WireMockTest (httpPort = xxxx), if you want to run the wiremock server on specific port.
@SpringBootTest
@AutoConfigureMockMvc
@WireMockTest (httpPort = 8889)
class ApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
public void testConvertByCurrencyCode(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
String currencyCode = "jpy";
String response = """
{
"date": "2023-05-19",
"jpy": 138.544529
}""";
String currencyConversionUrl = String.format("/gh/fawazahmed0/currency-api@1/latest/currencies/usd/%s.json",currencyCode.toLowerCase());
stubFor(WireMock.get(urlEqualTo(currencyConversionUrl)).willReturn(WireMock.aResponse().withStatus(200).withBody(response)));
this.mockMvc.perform(get("/currencyCodeVersion/currencyCode/{currencyCode}", currencyCode))
.andExpect(status().isOk());
}
}
Code language: Java (java)
In above example test code
@WireMockTest (httpPort = 8889) – start wiremock server on localhost and port 8889
stubFor –> is used to match external service url which program calls and send the canned HTTP response when it is matched.
Note : Please observe that in the url path I am only using path not using domain name.
Create application.properties file in under test/resources directory and place the following properties.
currencyconverter.url=http://localhost:8889/gh/fawazahmed0/currency-api@1/latest/currencies/usd/%s.json
country.url=http://localhost:8889/v3.1/name/%s
Code language: Java (java)
These properties are same as we placed in src/resources/application.properties, these properties point to Wiremock server running on localhost.
The above testcase shows the success response scenario.
But we can not expect success for every request. So let’s try the scenario when external service returns error message
The below example shows the returning of error message from
@Test
public void testConvertByCurrencyCodeWhenConversionFoundWithGivenCurrencyCode(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
String currencyCode = "abc";
String currencyConversionUrl = String.format("/gh/fawazahmed0/currency-api@1/latest/currencies/usd/%s.json", currencyCode.toLowerCase());
stubFor(WireMock.get(urlEqualTo(currencyConversionUrl)).willReturn(WireMock.aResponse().withStatus(403).withBody("Package size exceeded the configured limit of 50 MB")));
this.mockMvc.perform(get("/currencyCodeVersion/currencyCode/{currencyCode}", currencyCode))
.andExpect(status().isForbidden());
}
Code language: Java (java)
In above code the stub method is returning a error response for the external service requestwhich helps you in testing your code in those scenarios.
WireMock server lifecycle
In the above example a WireMock server will be started before the first test method in the test class and stopped after the last test method has completed.
Stub mappings and requests will be reset before each test method.
Running on Dynamic Port
The above example shows the running of Wiremock server on fixed port.
If you want to run the Wiremock on random port, you just need to remove the httpPort paramter on the annotation.
@WireMockTest
public class DeclarativeWireMockTest {
}
Code language: Java (java)
Enabling HTTPS
You can also enable HTTPS via the httpsEnabled
annotation parameter. By default a random port will be assigned:
@WireMockTest(httpsEnabled = true)
public class DeclarativeWireMockTest {
}
Code language: Java (java)
you can also fix the HTTPS port number via the httpsPort
parameter
@WireMockTest(httpsEnabled = true, httpsPort = 8443)
public class DeclarativeWireMockTest {
}
Code language: Java (java)
Programmatic Usage
When using Wiremock declaratively we can start only one server. But there may be situations where you want to run more than one Wiremock server instance then you can start them programatically which gives more control over configuration.
@SpringBootTest
@AutoConfigureMockMvc
public class ProgrammaticWireMockTest {
@RegisterExtension
static WireMockExtension currencyServer = WireMockExtension.newInstance()
.options(wireMockConfig().port(4141).extensions(new ResponseTemplateTransformer(true)))
.build();
@RegisterExtension
static WireMockExtension countryServer = WireMockExtension.newInstance()
.options(wireMockConfig().port(4040).extensions(new ResponseTemplateTransformer(true)))
.build();
@Autowired
private MockMvc mockMvc;
@Test
void contextLoads() {
}
@Test
public void testCurrencyConversionByCountry() throws Exception {
String url = String.format("%s/v3.1/name/%s", currencyServer.baseUrl(),"%s");
System.out.println("url :" + url);
String currencyCode = "jpy";
String currencyConversionUrl = String.format("/gh/fawazahmed0/currency-api@1/latest/currencies/usd/%s.json",currencyCode.toLowerCase());
String country = "japan";
String countryurl = String.format("/v3.1/name/%s",country.toLowerCase());
String response = """
[{"name":{"common":"Japan","official":"Japan","nativeName":{"jpn":{"official":"日本","common":"日本"}}},"tld":[".jp",".みんな"],"cca2":"JP","ccn3":"392","cca3":"JPN","cioc":"JPN","independent":true,"status":"officially-assigned","unMember":true,"currencies":{"JPY":{"name":"Japanese yen","symbol":"¥"}},"idd":{"root":"+8","suffixes":["1"]},"capital":["Tokyo"],"altSpellings":["JP","Nippon","Nihon"],"region":"Asia","subregion":"Eastern Asia","languages":{"jpn":"Japanese"},"translations":{"ara":{"official":"اليابان","common":"اليابان"},"bre":{"official":"Japan","common":"Japan"},"ces":{"official":"Japonsko","common":"Japonsko"},"cym":{"official":"Japan","common":"Japan"},"deu":{"official":"Japan","common":"Japan"},"est":{"official":"Jaapan","common":"Jaapan"},"fin":{"official":"Japani","common":"Japani"},"fra":{"official":"Japon","common":"Japon"},"hrv":{"official":"Japan","common":"Japan"},"hun":{"official":"Japán","common":"Japán"},"ita":{"official":"Giappone","common":"Giappone"},"jpn":{"official":"日本","common":"日本"},"kor":{"official":"일본국","common":"일본"},"nld":{"official":"Japan","common":"Japan"},"per":{"official":"ژاپن","common":"ژاپن"},"pol":{"official":"Japonia","common":"Japonia"},"por":{"official":"Japão","common":"Japão"},"rus":{"official":"Япония","common":"Япония"},"slk":{"official":"Japonsko","common":"Japonsko"},"spa":{"official":"Japón","common":"Japón"},"srp":{"official":"Јапан","common":"Јапан"},"swe":{"official":"Japan","common":"Japan"},"tur":{"official":"Japonya","common":"Japonya"},"urd":{"official":"جاپان","common":"جاپان"},"zho":{"official":"日本国","common":"日本"}},"latlng":[36.0,138.0],"landlocked":false,"area":377930.0,"demonyms":{"eng":{"f":"Japanese","m":"Japanese"},"fra":{"f":"Japonaise","m":"Japonais"}},"flag":"\\uD83C\\uDDEF\\uD83C\\uDDF5","maps":{"googleMaps":"https://goo.gl/maps/NGTLSCSrA8bMrvnX9","openStreetMaps":"https://www.openstreetmap.org/relation/382313"},"population":125836021,"gini":{"2013":32.9},"fifa":"JPN","car":{"signs":["J"],"side":"left"},"timezones":["UTC+09:00"],"continents":["Asia"],"flags":{"png":"https://flagcdn.com/w320/jp.png","svg":"https://flagcdn.com/jp.svg","alt":"The flag of Japan features a crimson-red circle at the center of a white field."},"coatOfArms":{"png":"https://mainfacts.com/media/images/coats_of_arms/jp.png","svg":"https://mainfacts.com/media/images/coats_of_arms/jp.svg"},"startOfWeek":"monday","capitalInfo":{"latlng":[35.68,139.75]},"postalCode":{"format":"###-####","regex":"^(\\\\d{7})$"}}]""";
String response1 = """
{
"date": "2023-05-19",
"jpy": 138.544529
}""";
currencyServer.stubFor(WireMock.get(urlMatching(currencyConversionUrl)).willReturn(WireMock.aResponse().withStatus(200).withBody(response1)));
countryServer.stubFor(WireMock.get(urlMatching(countryurl)).willReturn(WireMock.aResponse().withStatus(200).withBody(response)));
this.mockMvc.perform(get("/currencyCodeVersion/country/{countryCode}", country))
.andExpect(status().isOk());
}
@Test
public void testCurrencyConversionByCountryWhenNoCountryFoundWithGivenCode() throws Exception {
String currencyCode = "abc";
String currencyConversionUrl = String.format("/gh/fawazahmed0/currency-api@1/latest/currencies/usd/%s.json",currencyCode.toLowerCase());
String country = "japan";
String countryurl = String.format("/v3.1/name/%s",country.toLowerCase());
String countryResponse = """
{"status":404,"message":"Not Found"}""";
String CurrencyConversionResponse = """
{
"date": "2023-05-19",
"jpy": 138.544529
}""";
currencyServer.stubFor(WireMock.get(urlMatching(currencyConversionUrl)).willReturn(WireMock.aResponse().withStatus(200).withBody(CurrencyConversionResponse)));
countryServer.stubFor(WireMock.get(urlMatching(countryurl)).willReturn(WireMock.aResponse().withStatus(200).withBody(countryResponse)));
this.mockMvc.perform(get("/currencyCodeVersion/country/{countryCode}", country))
.andDo(res -> System.out.println(res.getResponse()))
.andExpect(status().isNotFound())
;
}
@Test
public void testConvertByCurrencyCode() throws Exception {
String currencyCode = "jpy";
String response = """
{
"date": "2023-05-19",
"jpy": 138.544529
}""";
String currencyConversionUrl = String.format("/gh/fawazahmed0/currency-api@1/latest/currencies/usd/%s.json",currencyCode.toLowerCase());
//stubFor(WireMock.get(urlEqualTo(currencyConversionUrl)).willReturn(WireMock.aResponse().withStatus(200).withJsonBody(response)));
currencyServer.stubFor(WireMock.get(urlEqualTo(currencyConversionUrl)).willReturn(WireMock.aResponse().withStatus(200).withBody(response)));
this.mockMvc.perform(get("/currencyCodeVersion/currencyCode/{currencyCode}", currencyCode))
.andExpect(status().isOk()).andExpect(content().string("{date=2023-05-19, jpy=138.544529}"));
}
@Test
public void testConvertByCurrencyCodeWhenConversionFoundWithGivenCurrencyCode() throws Exception {
String currencyCode = "abc";
String currencyConversionUrl = String.format("/gh/fawazahmed0/currency-api@1/latest/currencies/usd/%s.json", currencyCode.toLowerCase());
currencyServer.stubFor(WireMock.get(urlEqualTo(currencyConversionUrl)).willReturn(WireMock.aResponse().withStatus(403).withBody("Package size exceeded the configured limit of 50 MB")));
this.mockMvc.perform(get("/currencyCodeVersion/currencyCode/{currencyCode}", currencyCode))
.andExpect(status().isForbidden());
}
@DynamicPropertySource
public static void properties(DynamicPropertyRegistry registry) {
registry.add("country.url",()-> String.format("%s/v3.1/name/%s", countryServer.baseUrl(),"%s"));
registry.add("currencyconverter.url",()->String.format("%s/gh/fawazahmed0/currency-api@1/latest/currencies/usd/%s.json", currencyServer.baseUrl(),"%s"));
}
}
Code language: Java (java)
Either you can create application.properties file in under test/resources directory and place the following properties or use @DynamicResources
currencyconverter.url=http://localhost:4141/gh/fawazahmed0/currency-api@1/latest/currencies/usd/%s.json
country.url=http://localhost:4040/v3.1/name/%s
Code language: Java (java)
In the above example, as with the declarative form, each WireMock server will be started before the first test method in the test class and stopped after the last test method has completed, with a call to reset before each test method.
However, if the extension fields are declared at the instance scope (without the static
modifier) each WireMock server will be created and started before each test method and stopped after the end of the test method.
If you want to use dynamic port while using Wiremock programmatically you can use like below and use @DynamicProperties annotation to register the Wiremock urls.
@SpringBootTest
@AutoConfigureMockMvc
public class ProgrammaticWireMockDynamicPortTest{
@RegisterExtension
static WireMockExtension currencyServer = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort().extensions(new ResponseTemplateTransformer(true)))
.build();
@RegisterExtension
static WireMockExtension countryServer = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort().extensions(new ResponseTemplateTransformer(true)))
.build();
@Autowired
private MockMvc mockMvc;
@Test
void contextLoads() {
}
@Test
public void testCurrencyConversionByCountry() throws Exception {
String url = String.format("%s/v3.1/name/%s", currencyServer.baseUrl(),"%s");
System.out.println("url :" + url);
String currencyCode = "jpy";
String currencyConversionUrl = String.format("/gh/fawazahmed0/currency-api@1/latest/currencies/usd/%s.json",currencyCode.toLowerCase());
String country = "japan";
String countryurl = String.format("/v3.1/name/%s",country.toLowerCase());
String response = """
[{"name":{"common":"Japan","official":"Japan","nativeName":{"jpn":{"official":"日本","common":"日本"}}},"tld":[".jp",".みんな"],"cca2":"JP","ccn3":"392","cca3":"JPN","cioc":"JPN","independent":true,"status":"officially-assigned","unMember":true,"currencies":{"JPY":{"name":"Japanese yen","symbol":"¥"}},"idd":{"root":"+8","suffixes":["1"]},"capital":["Tokyo"],"altSpellings":["JP","Nippon","Nihon"],"region":"Asia","subregion":"Eastern Asia","languages":{"jpn":"Japanese"},"translations":{"ara":{"official":"اليابان","common":"اليابان"},"bre":{"official":"Japan","common":"Japan"},"ces":{"official":"Japonsko","common":"Japonsko"},"cym":{"official":"Japan","common":"Japan"},"deu":{"official":"Japan","common":"Japan"},"est":{"official":"Jaapan","common":"Jaapan"},"fin":{"official":"Japani","common":"Japani"},"fra":{"official":"Japon","common":"Japon"},"hrv":{"official":"Japan","common":"Japan"},"hun":{"official":"Japán","common":"Japán"},"ita":{"official":"Giappone","common":"Giappone"},"jpn":{"official":"日本","common":"日本"},"kor":{"official":"일본국","common":"일본"},"nld":{"official":"Japan","common":"Japan"},"per":{"official":"ژاپن","common":"ژاپن"},"pol":{"official":"Japonia","common":"Japonia"},"por":{"official":"Japão","common":"Japão"},"rus":{"official":"Япония","common":"Япония"},"slk":{"official":"Japonsko","common":"Japonsko"},"spa":{"official":"Japón","common":"Japón"},"srp":{"official":"Јапан","common":"Јапан"},"swe":{"official":"Japan","common":"Japan"},"tur":{"official":"Japonya","common":"Japonya"},"urd":{"official":"جاپان","common":"جاپان"},"zho":{"official":"日本国","common":"日本"}},"latlng":[36.0,138.0],"landlocked":false,"area":377930.0,"demonyms":{"eng":{"f":"Japanese","m":"Japanese"},"fra":{"f":"Japonaise","m":"Japonais"}},"flag":"\\uD83C\\uDDEF\\uD83C\\uDDF5","maps":{"googleMaps":"https://goo.gl/maps/NGTLSCSrA8bMrvnX9","openStreetMaps":"https://www.openstreetmap.org/relation/382313"},"population":125836021,"gini":{"2013":32.9},"fifa":"JPN","car":{"signs":["J"],"side":"left"},"timezones":["UTC+09:00"],"continents":["Asia"],"flags":{"png":"https://flagcdn.com/w320/jp.png","svg":"https://flagcdn.com/jp.svg","alt":"The flag of Japan features a crimson-red circle at the center of a white field."},"coatOfArms":{"png":"https://mainfacts.com/media/images/coats_of_arms/jp.png","svg":"https://mainfacts.com/media/images/coats_of_arms/jp.svg"},"startOfWeek":"monday","capitalInfo":{"latlng":[35.68,139.75]},"postalCode":{"format":"###-####","regex":"^(\\\\d{7})$"}}]""";
String response1 = """
{
"date": "2023-05-19",
"jpy": 138.544529
}""";
currencyServer.stubFor(WireMock.get(urlMatching(currencyConversionUrl)).willReturn(WireMock.aResponse().withStatus(200).withBody(response1)));
countryServer.stubFor(WireMock.get(urlMatching(countryurl)).willReturn(WireMock.aResponse().withStatus(200).withBody(response)));
this.mockMvc.perform(get("/currencyCodeVersion/country/{countryCode}", country))
.andExpect(status().isOk());
}
@DynamicPropertySource
public static void properties(DynamicPropertyRegistry registry) {
registry.add("country.url",()-> String.format("%s/v3.1/name/%s", countryServer.baseUrl(),"%s"));
registry.add("currencyconverter.url",()->String.format("%s/gh/fawazahmed0/currency-api@1/latest/currencies/usd/%s.json", currencyServer.baseUrl(),"%s"));
}
}
Code language: Java (java)
You can download source code for the blog post from GitHub