Partilhar via


Testes de unidade e integração em aplicativos de API mínima

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 10 deste artigo.

Advertência

Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e do .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

Por Fiyaz Bin Hasan e Rick Anderson

Introdução aos testes de integração

Os testes de integração avaliam os componentes de um aplicativo em um nível mais amplo do que testes de unidade. Os testes de unidade são usados para testar componentes de software isolados, como métodos de classe individuais. Os testes de integração confirmam que dois ou mais componentes do aplicativo trabalham juntos para produzir um resultado esperado, possivelmente incluindo todos os componentes necessários para processar totalmente uma solicitação.

Esses testes mais amplos são usados para testar a infraestrutura e toda a estrutura do aplicativo, geralmente incluindo os seguintes componentes:

  • Base de dados
  • Sistema de ficheiros
  • Dispositivos de rede
  • Pipeline de solicitação-resposta

Os testes de unidade usam componentes fabricados, conhecidos como falsos ou objetos falsificados, no lugar de componentes de infraestrutura.

Em contraste com os testes de unidade, os testes de integração:

  • Use os componentes reais que o aplicativo usa na produção.
  • Exigem mais código e processamento de dados.
  • Leve mais tempo para correr.

Portanto, limite o uso de testes de integração aos cenários de infraestrutura mais importantes. Se um comportamento puder ser testado usando um teste de unidade ou um teste de integração, escolha o teste de unidade.

Em discussões de testes de integração, o projeto testado é frequentemente chamado de System Under Test, ou "SUT" para abreviar. "SUT" é usado ao longo deste artigo para se referir ao aplicativo ASP.NET Core que está sendo testado.

Não escreva testes de integração para cada de permutação de dados e acesso a arquivos com bancos de dados e sistemas de arquivos. Independentemente de quantos lugares em um aplicativo interagem com bancos de dados e sistemas de arquivos, um conjunto focado de testes de integração de leitura, gravação, atualização e exclusão geralmente é capaz de testar adequadamente os componentes do banco de dados e do sistema de arquivos. Use testes de unidade para testes de rotina de lógica de método que interagem com esses componentes. Em testes de unidade, o uso de falsificações ou simulações de infraestrutura resulta em execução de teste mais rápida.

ASP.NET Testes de integração principais

Os testes de integração no ASP.NET Core requerem o seguinte:

  • Um projeto de teste é usado para conter e executar os testes. O projeto de teste tem uma referência ao SUT.
  • O projeto de teste cria um host de teste para o SUT e usa um cliente de servidor de teste para lidar com solicitações e respostas com o SUT.
  • Um corredor de teste é usado para executar os testes e relatar os resultados do teste.

Os testes de integração seguem uma sequência de eventos que incluem as etapas usuais de teste Preparar, Agire Asserir:

  1. A hospedagem web da SUT está configurada.
  2. Um cliente de servidor de teste é criado para enviar solicitações ao aplicativo.
  3. A etapa de teste Organizar é executada: o aplicativo de teste prepara uma solicitação.
  4. A etapa de teste do Act é executada: o cliente envia a solicitação e recebe a resposta.
  5. A etapa de teste Assert é executada: A resposta real é validada como uma de aprovação ou falha com base em uma resposta esperada.
  6. O processo continua até que todos os testes sejam executados.
  7. Os resultados dos testes são divulgados.

Normalmente, o host da Web de teste é configurado de forma diferente do host normal do aplicativo para as execuções de teste. Por exemplo, um banco de dados diferente ou configurações de aplicativo diferentes podem ser usados para os testes.

Os componentes de infraestrutura, como o host da Web de teste e o servidor de teste na memória (TestServer), são fornecidos ou gerenciados pelo pacote Microsoft.AspNetCore.Mvc.Testing. O uso deste pacote simplifica a criação e execução de testes.

O pacote Microsoft.AspNetCore.Mvc.Testing lida com as seguintes tarefas:

  • Copia o arquivo de dependências (.deps) do SUT para o diretório bin do projeto de teste.
  • Define a raiz de conteúdo como a raiz do projeto do SUT para que arquivos estáticos e páginas/visualizações sejam encontrados quando os testes forem executados.
  • Fornece a classe WebApplicationFactory, para simplificar o processo de configuração do SUT com TestServer.

A documentação de testes de unidade descreve como configurar um projeto de teste e uma ferramenta de execução de testes, juntamente com instruções detalhadas sobre como executar testes e recomendações sobre como nomear testes e classes de teste.

Separe os testes de unidade dos testes de integração em diferentes projetos. Separar os testes:

  • Ajuda a garantir que os componentes de teste de infraestrutura não sejam incluídos acidentalmente nos testes de unidade.
  • Permite controlar qual conjunto de testes é executado.

O código de exemplo no GitHub fornece um exemplo de testes de unidade e integração em um aplicativo de API mínima.

Tipos de implementação de IResult para teste de unidade

O exemplo a seguir mostra como testar por unidade manipuladores de rota mínimos que retornam IResult usando a estrutura de teste xUnit . O banco de dados externo é substituído por um banco de dados em memória durante o teste, a implementação do MockDb pode ser encontrada no código de exemplo.

Os tipos de implementação pública IResult no Microsoft.AspNetCore.Http.HttpResults namespace podem ser usados para testar em unidade manipuladores de rota mínimos ao usar métodos nomeados em vez de lambdas.

O código a seguir usa a classe NotFound<TValue>:

[Fact]
public async Task GetTodoReturnsNotFoundIfNotExists()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    // Act
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var notFoundResult = (NotFound) result.Result;

    Assert.NotNull(notFoundResult);
}

O código a seguir usa a classe Ok<TValue>:

[Fact]
public async Task GetTodoReturnsTodoFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title",
        Description = "Test description",
        IsDone = false
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var okResult = (Ok<Todo>)result.Result;

    Assert.NotNull(okResult.Value);
    Assert.Equal(1, okResult.Value.Id);
}

Nos exemplos anteriores, o resultado é convertido para um tipo concreto porque o endpoint em teste pode retornar vários tipos de resultado (NotFound<TValue> ou Ok<TValue>). No entanto, se o ponto de extremidade retornar um tipo TypedResults único, o resultado será automaticamente inferido para esse tipo e nenhuma conversão será necessária.

O código a seguir usa a Ok classe e o tipo do valor é uma coleção de Todo:

[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title 1",
        Description = "Test description 1",
        IsDone = false
    });

    context.Todos.Add(new Todo
    {
        Id = 2,
        Title = "Test title 2",
        Description = "Test description 2",
        IsDone = true
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetAllTodos(context);

    //Assert
    Assert.IsType<Ok<Todo[]>>(result);
    
    Assert.NotNull(result.Value);
    Assert.NotEmpty(result.Value);
    Assert.Collection(result.Value, todo1 =>
    {
        Assert.Equal(1, todo1.Id);
        Assert.Equal("Test title 1", todo1.Title);
        Assert.False(todo1.IsDone);
    }, todo2 =>
    {
        Assert.Equal(2, todo2.Id);
        Assert.Equal("Test title 2", todo2.Title);
        Assert.True(todo2.IsDone);
    });
}

Recursos adicionais