Featured image of post Por que meus mocks do WireMock não estão funcionando?

Por que meus mocks do WireMock não estão funcionando?

Como solucionar problemas do WireMock.Net + quatro problemas comuns

Introdução Link to this section

WireMock.Net é uma ótima ferramenta para remover dependências externas ao escrever testes de integração, mas como é altamente configurável, pode ser difícil descobrir por que seus mocks não estão funcionando.

Neste post, explicarei como solucionar problemas em sua configuração e mostrarei alguns problemas comuns que acontecem no meu trabalho diário.

O que é WireMock.Net? Link to this section

WireMock.Net é uma biblioteca para stubbing e mocking APIs HTTP. Escrevi sobre como e por que usá-lo anteriormente e usarei os exemplos do post anterior neste.

API de administração do WireMock.Net Link to this section

WireMock.Net tem uma API de administração que é essencial para depurar problemas em nossos mocks.

A API oferece muitos endpoints, mas mostrarei os dois que serão usados para encontrar problemas nos mocks.

ℹ️ A lista completa de endpoints pode ser vista na documentação do WireMock.Net.

Endpoint de mappings Link to this section

O endpoint /__admin/mappings retorna os mappings configurados para os mocks, incluindo todos os matchers que precisam ser cumpridos para que o mock responda e a resposta que será retornada.

Aqui está um exemplo do retorno:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[
  {
    "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
  }
]

Endpoint de solicitações Link to this section

O endpoint /__admin/requests retorna o histórico de solicitações feitas ao WireMock. Ele inclui informações sobre a solicitação feita e a resposta recebida pelo solicitante.

Ele também inclui a propriedade PartialRequestMatchResult, que mostra qual matcher foi bem-sucedido e qual não foi.

Aqui está um exemplo de uma solicitação que atingiu o mock:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
[
  {
    "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
        }
      ]
    }
  }
]

O RequestMatchResult mostra que o mock tem 2 matchers configurados (TotalNumber) e a pontuação total foi 2 (TotalScore). O MatchDetails também mostra uma pontuação de 1.0 (100%) para todos os matchers.

Aqui está um exemplo de uma solicitação que não cumpriu todos os matchers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
[
  {
    "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
        }
      ]
    }
  }
]

O PartialRequestMatchResult mostra que o mock tem 2 matchers configurados (TotalNumber) e a pontuação total foi 1 (TotalScore). No MatchDetails podemos ver que o problema estava no PathMatcher.

Olhando no endpoint de mappings, vemos que o caminho foi configurado para /pokemon/charmander, em vez de /pokemon/squirtle que estava na solicitação.

Configurando a Interface de Administração Link to this section

Para usar a interface de administração, precisamos apenas iniciar o servidor do WireMock com o método StartWithAdminInterface em vez do método Start:

var wiremockServer = WireMockServer.StartWithAdminInterface();

Em seguida, para poder acessar a interface de administração, incluímos um atraso após a parte Act do teste:

await Task.Delay(TimeSpan.FromMinutes(100));

Por último, acessamos o endpoint usando o URL retornado pela propriedade Url do objeto wireMockSvr:

Código de teste completo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[Fact]
public async Task Get_Existing_Pokemon_Returns_200()
{
    //Arrange
    var wireMockSvr = WireMockServer.StartWithAdminInterface(); //Inicia o WireMock com a Interface de Administração

    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)); //Atraso para poder examinar a interface de administração

    //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();
}

Problemas comuns Link to this section

Parâmetros de string de consulta Link to this section

Vamos pegar o endpoint /pokemon?type={typeName} como um exemplo.

O mock abaixo não funcionará:

1
2
3
4
5
6
7
8
WireMockSvr
    .Given(Request.Create()
        .WithPath("/pokemon?type=fire")
        .UsingGet())
    .RespondWith(Response.Create()
        .WithBody(ResponseObjJson)
        .WithHeader("Content-Type", "application/json")
        .WithStatusCode(HttpStatusCode.OK));

Para criar um mock para endpoints com strings de consulta, temos que usar a propriedade WithParam:

1
2
3
4
5
6
7
8
9
WireMockSvr
    .Given(Request.Create()
        .WithPath("/pokemon")
        .WithParam("type", "fire")
        .UsingGet())
    .RespondWith(Response.Create()
        .WithBody(ResponseObjJson)
        .WithHeader("Content-Type", "application/json")
        .WithStatusCode(HttpStatusCode.OK));

Parâmetros na rota Link to this section

Ao contrário dos parâmetros na string de consulta, os parâmetros no caminho não funcionarão quando configurados com WithParam. Por exemplo, o endpoint /pokemon/{pokemonName} não será alcançado com:

1
2
3
4
5
6
7
8
9
WireMockSvr
    .Given(Request.Create()
        .WithPath("/pokemon")
        .WithParam("pokemonName", "charmander")
        .UsingGet())
    .RespondWith(Response.Create()
        .WithBody(ResponseObjJson)
        .WithHeader("Content-Type", "application/json")
        .WithStatusCode(HttpStatusCode.OK));

Ele tem que ser definido no método WithPath:

1
2
3
4
5
6
7
8
WireMockSvr
    .Given(Request.Create()
        .WithPath("/pokemon/charmander")
        .UsingGet())
    .RespondWith(Response.Create()
        .WithBody(ResponseObjJson)
        .WithHeader("Content-Type", "application/json")
        .WithStatusCode(HttpStatusCode.OK));

Executando atrás de um proxy de rede Link to this section

Ao executar atrás de um proxy de rede, o WireMock pode estar inacessível, causando um tempo limite no aplicativo.

Para ignorar o proxy para localhost, precisamos configurar a variável de ambiente no_proxy, adicionando localhost ao seu valor (mais valores podem ser incluídos, separados por vírgula):

Servidor WireMock compartilhado para todos os testes Link to this section

Compartilhar a instância do servidor do WireMock entre os testes pode causar problemas aleatórios porque as definições de mock são substituídas quando configuradas pela segunda vez. Por exemplo, pegue dois testes em execução em paralelo:

  • O Teste 1 configura /pokemon/charmander para retornar o status 200;
  • O Teste 2 configura /pokemon/charmander para retornar o status 404.

O primeiro teste a configurar o mock será interrompido porque seu mock será substituído e o resultado não será o esperado.

Para evitar esses problemas aleatórios, precisamos usar uma instância WireMock para cada teste:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[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();
}
💬 Like or have something to add? Leave a comment below.
Ko-fi
GitHub Sponsor
Licensed under CC BY-NC-SA 4.0
Criado com Hugo
Tema Stack desenvolvido por Jimmy