Why my WireMock mocks aren't working?

Daniel Genezini - Apr 12 '23 - - Dev Community

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
  }
]
Enter fullscreen mode Exit fullscreen mode

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
        }
      ]
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

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
        }
      ]
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

Lastly, we access the endpoint using the URL returned by the Url property of the wireMockSvr object:

Debugging and accessing WireMock's admin endpoints

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();
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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):

no_proxy environment variable configuration

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 status 200;
  • Test 2 configures /pokemon/charmander to return status 404.

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();
}
Enter fullscreen mode Exit fullscreen mode

References and Links

. . . . . . . . . . . . . . . . . . .