Featured image of post Testes de integração sem dependências de APIs com ASP.NET Core e WireMock.Net

Testes de integração sem dependências de APIs com ASP.NET Core e WireMock.Net

Criando mocks para APIs com WireMock.Net

Follow me

Introdução Link to this section

Embora não exista um consenso sobre o escopo de um teste de integração, Martin Fowler define testes de integração restritos (Narrow), nos quais a integração entre os sistemas é testada usando substitutos (Mocks), e testes de integração amplos (Broad), que se comunicam com APIs reais.

Nesse post, explicarei como criar mocks para APIs HTTP em testes de integração restritos usando a biblioteca WireMock.Net.

O que “mockar”? Link to this section

Vladimir Khorikov tem um conceito de dependências gerenciadas e não gerenciadas, que considero complementar ao conceito de Martin Fowler, para escolhermos o que deve e o que não deve ser “mockado”.

Segundo Vladimir, dependências gerenciadas são sistemas externos controlados por nós e acessados apenas pela nossa aplicação (por exemplo, um banco de dados). Por outro lado, dependências não gerenciadas são sistemas externos não controlados por nós ou que são acessados também por outras aplicações (como uma API de terceiros e um message broker).

Vladimir defende que devemos fazer nossos testes de integração consumindo dependências gerenciadas e “mockando” dependências não gerenciadas. Eu acredito que essa definição serve mais como um norte do que como uma regra. Por exemplo, em um cenário onde nosso sistema posta mensagens em um message broker para outro sistema ler, ou seja, o message broker é uma dependência não gerenciada, podemos testar a integração com o message broker para validar que a mensagem está sendo postada no formato correto (contrato definido). Isso pode ser valioso para validar que atualizações na biblioteca usada para se comunicar com o message broker não modificou o formato e o conteúdo das mensagens.

Por que usar mocks? Link to this section

O intuito dos testes de integração restritos é testar se seus componentes, testados individualmente através de testes de unidade, funcionam corretamente de forma integrada entre si. Quando consumimos uma API, seguimos um protocolo e confiamos em um contrato de comunicação, isto é, que ela vai aceitar uma entrada X e retornar uma resposta Y.

Dessa forma, o funcionamento da API externa não está no escopo dos testes integrados.

Isso não exclui a necessidade de realizar testes funcionais; apenas diminui a quantidade necessária desses testes, que, em geral, são mais custosos de serem executados.

Restringindo os testes de integração apenas ao nosso sistema, temos alguns benefícios:

  • Velocidade nos testes, pois retiramos a latência da rede;
  • Não dependência de dados para teste em sistemas externos;
  • Menor fragilidade dos testes, que podem quebrar em caso de instabilidade ou de dados que foram alterados nos sistemas externos, dificultando a interpretação dos resultados;
  • Maior confiança nos resultados dos testes.

Usando WireMock.Net Link to this section

Nesse exemplo, construí uma API que usa o serviço PokéAPI para buscar os dados de um Pokémon e retornar os dados para o cliente da nossa API.

Diagrama da API

Controller Link to this section

O controller é simples e usa a biblioteca Refit para abstrair a chamada da API e, por simplicidade, retorna o mesmo objeto.

 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
using Microsoft.AspNetCore.Mvc;
using Refit;

namespace PokemonInfoAPI.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class PokemonInfoController : ControllerBase
    {
        private readonly IConfiguration _configuration;

        public PokemonInfoController(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        [HttpGet("{pokemonName}")]
        public async Task<ActionResult<PokemonInfo>> GetAsync(string pokemonName)
        {
            try
            {
                var pokeApi = RestService.For<IPokeApi>(_configuration["PokeApiBaseUrl"]);

                return Ok(await pokeApi.GetPokemonInfo(pokemonName));
            }
            catch (ApiException ex)
            {
                if (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
                {
                    return NotFound();
                }

                return StatusCode(500);
            }
        }
    }
}

Teste de integração padrão Link to this section

Começamos com um teste de integração padrão, usando a classe WebApplicationFactory do ASP.NET Core.

O teste cria uma instância da nossa aplicação e faz uma chamada ao endpoint /pokemoninfo passando o parâmetro charmander. A chamada ao endpoint faz nossa aplicação chamar a API PokéAPI.

💡 Você pode usar qualquer classe do projeto da API para instanciar o WebApplicationFactory nos seus testes. Se estiver usando statements na sua aplicação, você pode usar a classe do controller. Por exemplo, WebApplicationFactory<PokemonInfoController>.

 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
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Text.Json;

namespace PokemonInfoAPI.IntegrationTests
{
    public class PokemonInfoTests: IClassFixture<WebApplicationFactory<Program>>
    {
        private readonly WebApplicationFactory<Program> _factory;

        public PokemonInfoTests(WebApplicationFactory<Program> factory)
        {
            _factory = factory;
        }

        [Fact]
        public async Task Get_Existing_Pokemon_Returns_200()
        {
            //Arrange
            var HttpClient = Factory.CreateClient();

            //Act
            var HttpResponse = await HttpClient.GetAsync("/pokemoninfo/charmander");

            //Assert
            HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK);

            var ResponseJson = await HttpResponse.Content.ReadAsStringAsync();
            var PokemonInfo = JsonSerializer.Deserialize<PokemonInfo>(ResponseJson);

            PokemonInfo.Should().BeEquivalentTo(ResponseObj);
        }
    }
}

Criando um mock para o sistema externo PokéAPI Link to this section

O WireMock.Net é uma biblioteca que possibilita a criação de mocks para APIs HTTP. Ele cria um servidor web no mesmo processo dos nossos testes e expõe uma URL para ser acessada pela nossa aplicação.

Usando o WireMock.Net e o WebApplicationFactory teremos o seguinte cenário:

Diagrama de um teste de integração usando WireMock.Net e WebApplicationFactory

Primeiro, instalo o pacote Nuget WireMock.Net no projeto de testes.

Utilizando o Package Manager pelo Visual Studio Link to this section

Install-Package WireMock.Net

Ou

Utilizando o .NET CLI pela linha de comando Link to this section

dotnet add package WireMock.Net

Iniciando o servidor do WireMock.Net Link to this section

Para iniciar o servidor do WireMock.Net, chamo o método Start da classe WireMockServer, que retorna um objeto com os dados do servidor.

1
var WireMockSvr = WireMockServer.Start();

Sobrescrevendo as configurações da nossa aplicação Link to this section

Com o servidor iniciado, sobrescrevo o parâmetro PokeApiBaseUrl, que contém a URL do PokéAPI, nas configurações da minha aplicação usando o método WithWebHostBuilder do WebApplicationFactory:

1
2
3
4
5
6
var HttpClient = _factory
    .WithWebHostBuilder(builder =>
    {
        builder.UseSetting("PokeApiBaseUrl", WireMockSvr.Url);
    })
    .CreateClient();

Criando o mock para o endpoint /pokemon Link to this section

Em seguida, crio o mock para o endpoint /pokemon recebendo o parâmetro charmander.

No exemplo abaixo, estou usando o AutoFixture para gerar um objeto com valores aleatórios, que será retornado pelo mock.

ℹ️ Usar um objeto me possibilita comparar o retorno do meu sistema com esse objeto, mas também é possível configurar o retorno a partir de um arquivo, utilizando WithBodyFromFile.

Também configuro os Headers que serão retornados e o status HTTP.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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));

Feito isso, minha aplicação instanciada dentro dos testes já estará consumindo o mock, em vez da API real.

Código completo do teste Link to this section

 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
[Fact]
public async Task Get_Existing_Pokemon_Returns_200()
{
    //Arrange
    var WireMockSvr = WireMockServer.Start();

    var HttpClient = _factory
        .WithWebHostBuilder(builder =>
            {
                builder.UseSetting("PokeApiBaseUrl", WireMockSvr.Url);
            })
        .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");

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

Exemplo simulando um cenário de não sucesso Link to this section

Pelo contrato da API, sabemos que ela retorna o status 404 (Not Found) quando recebe um pokémon inválido como parâmetro, então criei um mock que retorna esse status ao receber o parâmetro picapau e valido que o retorno da minha aplicação está correto.

 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
[Fact]
public async Task Get_NotExisting_Pokemon_Returns_404()
{
    //Arrange
    var WireMockSvr = WireMockServer.Start();

    var Factory = _factory.WithWebHostBuilder(builder =>
    {
        builder.UseSetting("PokeApiBaseUrl", WireMockSvr.Url);
    });

    var HttpClient = Factory.CreateClient();

    Fixture fixture = new Fixture();

    WireMockSvr
        .Given(Request.Create()
            .WithPath("/pokemon/picapau")
            .UsingGet())
        .RespondWith(Response.Create()
            .WithHeader("Content-Type", "application/json")
            .WithStatusCode(HttpStatusCode.NotFound));

    //Act
    var HttpResponse = await HttpClient
        .GetAsync("/pokemoninfo/picapau");

    //Assert
    HttpResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);

    WireMockSvr.Stop();
}

Fonte completo Link to this section

https://github.com/dgenezini/PokemonInfoAPIWireMockTests

💬 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