Introduction
WireMock.Net is a great tool to remove external dependencies when writing integration tests, but because it is highly configurable, it can be hard to find why its mocks aren’t working.
In this post, I’ll explain how to troubleshoot problems in its configuration and show some common problems that happen in my day-to-day work.
What is WireMock.Net?
WireMock.Net is a library for stubbing and mocking HTTP APIs. I wrote about how and why to use it previously, and will use the previous post examples in this one.
WireMock.Net's Admin API
WireMock.Net has an Admin API that is essential for debugging problems in our mocks.
The API offers many endpoints, but I'll show the two that will be used to find problems in the mocks.
ℹ️ The complete list of endpoints can be seen in the WireMock.Net's documentation.
Mappings endpoint
The endpoint /__admin/mappings
returns the mappings configured for the mocks, including all the matchers that need to be fulfilled for the mock to respond, and the response that will be returned.
Here is an example of the return:
[
{
"Guid": "4dbbabf7-aac9-4f31-9fb9-a02360a454ea",
"Request": {
"Path": {
"Matchers": [
{
"Name": "WildcardMatcher",
"Pattern": "/pokemon/charmander",
"IgnoreCase": false
}
]
},
"Methods": [
"GET"
]
},
"Response": {
"StatusCode": 200,
"BodyDestination": "SameAsSource",
"Body": "{\"abilities\":[{\"ability\":{\"name\":\"name8f22045d-c183-4f1c-a32d-aaa6d337a9b7\",\"url\":\"url768c48e4-0dec-4952-b810-06472f602b4d\"},\"is_hidden\":true,\"slot\":192},
...
\"url\":\"url716f6971-abd8-4e35-a9af-b7bd7a9a9cd6\"}}],\"weight\":128}",
"Headers": {
"Content-Type": "application/json"
}
},
"UseWebhooksFireAndForget": false
}
]
Requests endpoint
The endpoint /__admin/requests
returns the history of requests made to WireMock. It includes information about the request made and the response received by the requester.
It also includes the PartialRequestMatchResult
property, that shows which matcher was successful and which was not.
Here is an example of a request that reached the mock:
[
{
"Guid": "d2759bea-e4cc-442b-a63c-506ac6d61527",
"Request": {
"ClientIP": "::1",
"DateTime": "2023-04-09T21:23:52.2879698Z",
"Path": "/pokemon/charmander",
"AbsolutePath": "/pokemon/charmander",
"Url": "http://localhost:64378/pokemon/charmander",
"AbsoluteUrl": "http://localhost:64378/pokemon/charmander",
"Query": {},
"Method": "GET",
"Headers": {
"Host": [
"localhost:64378"
],
"traceparent": [
"00-fc2bc88c5bfc8b273e12d5a64cfd67cc-4ffca316becbeb2b-00"
]
},
"Cookies": {}
},
"Response": {
"StatusCode": 200,
"Headers": {
"Content-Type": [
"application/json"
]
},
"BodyDestination": "SameAsSource",
"Body": "{\"abilities\":[{\"ability\":{\"name\":\"name8f22045d-c183-4f1c-a32d-aaa6d337a9b7\",\"url\":\"url768c48e4-0dec-4952-b810-06472f602b4d\"},\"is_hidden\":true,\"slot\":192},{\"ability\":{\"name\":\"name7e833a25-8050-4369-a5b9-811b4b00ee69\",
...
\"url\":\"url716f6971-abd8-4e35-a9af-b7bd7a9a9cd6\"}}],\"weight\":128}",
"BodyEncoding": {
"CodePage": 65001,
"EncodingName": "Unicode (UTF-8)",
"WebName": "utf-8"
},
"DetectedBodyType": 1
},
"MappingGuid": "4dbbabf7-aac9-4f31-9fb9-a02360a454ea",
"RequestMatchResult": {
"TotalScore": 2.0,
"TotalNumber": 2,
"IsPerfectMatch": true,
"AverageTotalScore": 1.0,
"MatchDetails": [
{
"Name": "PathMatcher",
"Score": 1.0
},
{
"Name": "MethodMatcher",
"Score": 1.0
}
]
},
"PartialMappingGuid": "4dbbabf7-aac9-4f31-9fb9-a02360a454ea",
"PartialRequestMatchResult": {
"TotalScore": 2.0,
"TotalNumber": 2,
"IsPerfectMatch": true,
"AverageTotalScore": 1.0,
"MatchDetails": [
{
"Name": "PathMatcher",
"Score": 1.0
},
{
"Name": "MethodMatcher",
"Score": 1.0
}
]
}
}
]
The RequestMatchResult
shows that the mock has 2 matchers configured (TotalNumber
) and the total score was 2 (TotalScore
). The MatchDetails
also shows a score of 1.0 (100%) for all the matchers.
Here is an example of a request that didn't fulfill all of the matchers:
[
{
"Guid": "60812be4-2d28-436b-afe3-4597b57e1995",
"Request": {
"ClientIP": "::1",
"DateTime": "2023-04-10T22:09:24.4115811Z",
"Path": "/pokemon/squirtle",
"AbsolutePath": "/pokemon/squirtle",
"Url": "http://localhost:57546/pokemon/squirtle",
"AbsoluteUrl": "http://localhost:57546/pokemon/squirtle",
"Query": {},
"Method": "GET",
"Headers": {
"Host": [
"localhost:57546"
],
"traceparent": [
"00-f89a0136b596e297767aea9602ed6df2-3e4ef33765b915d1-00"
]
},
"Cookies": {}
},
"Response": {
"StatusCode": 404,
"Headers": {
"Content-Type": [
"application/json"
]
},
"BodyAsJson": {
"Status": "No matching mapping found"
},
"DetectedBodyType": 2
},
"PartialMappingGuid": "bab89116-6318-4ecb-8460-39e5116aeaec",
"PartialRequestMatchResult": {
"TotalScore": 1.0,
"TotalNumber": 2,
"IsPerfectMatch": false,
"AverageTotalScore": 0.5,
"MatchDetails": [
{
"Name": "PathMatcher",
"Score": 0.0
},
{
"Name": "MethodMatcher",
"Score": 1.0
}
]
}
}
]
The PartialRequestMatchResult
shows that the mock has 2 matchers configured (TotalNumber
) and the total score was 1 (TotalScore
). In the MatchDetails
we can see that the problem was on the PathMatcher
.
Looking in the mappings endpoint, we see that the path was configured to /pokemon/charmander
, instead of the /pokemon/squirtle
that was in the request.
Configuring the Admin Interface
To use the admin interface, we just need to start WireMock's server with the StartWithAdminInterface
method instead of the Start
method:
```csharp {linenos=false}
var wiremockServer = WireMockServer.StartWithAdminInterface();
Then, to be able to access the admin interface, we include a delay after the **Act** part of the test:
```csharp {linenos=false}
await Task.Delay(TimeSpan.FromMinutes(100));
Lastly, we access the endpoint using the URL returned by the Url
property of the wireMockSvr
object:
Full test code:
[Fact]
public async Task Get_Existing_Pokemon_Returns_200()
{
//Arrange
var wireMockSvr = WireMockServer.StartWithAdminInterface(); //Start WireMock with Admin Interface
var Factory = _factory
.WithWebHostBuilder(builder =>
{
builder.UseSetting("PokeApiBaseUrl", wireMockSvr.Url);
});
var HttpClient = Factory.CreateClient();
Fixture fixture = new Fixture();
var ResponseObj = fixture.Create<PokemonInfo>();
var ResponseObjJson = JsonSerializer.Serialize(ResponseObj);
wireMockSvr
.Given(Request.Create()
.WithPath("/pokemon/charmander")
.UsingGet())
.RespondWith(Response.Create()
.WithBody(ResponseObjJson)
.WithHeader("Content-Type", "application/json")
.WithStatusCode(HttpStatusCode.OK));
//Act
var HttpResponse = await HttpClient.GetAsync("/pokemoninfo/charmander");
await Task.Delay(TimeSpan.FromMinutes(30)); //Delay to be able to examine the admin interface
//Assert
HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var ResponseJson = await HttpResponse.Content.ReadAsStringAsync();
var PokemonInfo = JsonSerializer.Deserialize<PokemonInfo>(ResponseJson);
PokemonInfo.Should().BeEquivalentTo(ResponseObj);
wireMockSvr.Stop();
}
Common problems
Query string params
Let's take the endpoint /pokemon?type={typeName}
as an example.
The mock below won't work:
WireMockSvr
.Given(Request.Create()
.WithPath("/pokemon?type=fire")
.UsingGet())
.RespondWith(Response.Create()
.WithBody(ResponseObjJson)
.WithHeader("Content-Type", "application/json")
.WithStatusCode(HttpStatusCode.OK));
To create a mock for endpoints with query strings, we have to use the WithParam
property:
WireMockSvr
.Given(Request.Create()
.WithPath("/pokemon")
.WithParam("type", "fire")
.UsingGet())
.RespondWith(Response.Create()
.WithBody(ResponseObjJson)
.WithHeader("Content-Type", "application/json")
.WithStatusCode(HttpStatusCode.OK));
Params in the route
Contrary to params in the query string, params in the path won't work when configured with WithParam
. For example, the endpoint /pokemon/{pokemonName}
won't be reached with:
WireMockSvr
.Given(Request.Create()
.WithPath("/pokemon")
.WithParam("pokemonName", "charmander")
.UsingGet())
.RespondWith(Response.Create()
.WithBody(ResponseObjJson)
.WithHeader("Content-Type", "application/json")
.WithStatusCode(HttpStatusCode.OK));
It has to be set in the WithPath
method:
WireMockSvr
.Given(Request.Create()
.WithPath("/pokemon/charmander")
.UsingGet())
.RespondWith(Response.Create()
.WithBody(ResponseObjJson)
.WithHeader("Content-Type", "application/json")
.WithStatusCode(HttpStatusCode.OK));
Running behind a network proxy
When running behind a network proxy, WireMock may be unreachable, causing a timeout in the application.
To ignore the proxy for localhost
, we need to configure the no_proxy
environment variable, adding localhost
to its value (more values can be included, separated by comma):
Shared WireMock server for all tests
Sharing WireMock's server instance between tests can cause random problems because mock definitions are overridden when configured for the second time. For example, take two tests running in parallel:
- Test 1 configures
/pokemon/charmander
to return status200
; - Test 2 configures
/pokemon/charmander
to return status404
.
The first test to configure the mock will break because its mock will be overridden and the result won't be as expected.
To avoid this random problems, we need to use one WireMock instance for each test:
[Fact]
public async Task Get_Existing_Pokemon_Returns_200()
{
var WireMockSvr = WireMockServer.StartWithAdminInterface();
...
WireMockSvr.Stop();
}
[Fact]
public async Task Get_NotExisting_Pokemon_Returns_404()
{
var WireMockSvr = WireMockServer.StartWithAdminInterface();
...
WireMockSvr.Stop();
}