Pontos Principais
- Entender e utilizar os Testes Unitários corretamente é importante para as soluções Web API em ASP.NET Core
- Aprender e utilizar dados Mock nos testes unitários permite ter cenários de testes estáveis
- Criar projetos de dados Mock no .NET Core 2.1 para uso nas soluções Web API em ASP.NET Core
- Entender e configurar Testes de Integração para testar externamente uma Web API em ASP.NET Core 2.1 para uma solução completamente testada
Com o lançamento do .NET Core 2.0, a Microsoft possui uma nova versão principal da plataforma com propósito geral, modular, multiplataforma e de código aberto que foi lançada inicialmente em 2016. O .NET Core foi criado para ter muitas das APIs que estão disponíveis na versão atual do .NET Framework. Ele foi inicialmente criado para permitir a próxima geração de soluções ASP.NET, mas agora impulsiona e é a base para muitos outros cenários, incluindo IoT, nuvem e soluções móveis de próxima geração. Nesta segunda série, abrangendo o .NET Core, exploraremos mais alguns benefícios do .NET Core e como ele pode beneficiar não apenas desenvolvedores tradicionais de .NET, mas também todos os tecnólogos que precisam levar soluções robustas, eficazes e econômicas ao mercado.
Ao desenhar a arquitetura e desenvolver um rico conjunto de APIs usando ASP.NET Core 2.1 Web API é importante ter em mente que esse é apenas o primeiro passo para uma solução estável e produtiva. Ter um ambiente estável para a solução também é muito importante. A chave para uma grande solução inclui não apenas construir as APIs, mas também testá-las rigorosamente para garantir que os clientes tenham uma excelente experiência.
A solução e todo o código deste artigo está no github.
Primeiramente sobre o ASP.NET Core Web API
O ASP.NET Core é um framework web que a Microsoft construiu para substituir toda tecnologia legada desde o ASP.NET 1.0. Para cobrir todas as dependências legadas e desenvolver o framework do zero, o ASP.NET Core 2.1 traz melhor performance e é arquitetado para execução em multi-plataforma.
O que é Teste Unitário?
Testar software pode ser algo novo para algumas pessoas, mas é bem fácil. Começaremos pelo teste unitário. A definição do Wikipedia diz que (tradução livre): "é um método para testar software onde cada unidade individual de código, conjuntos de um ou mais módulos de programas associados a dados de controle, procedimentos de uso e de operação, são testados para certificar se estão aptos para uso". Uma definição de Iayman é que o Teste Unitário é usado para que seu código de solução execute conforme o esperado depois de adicionar uma nova funcionalidade ou correção. Testamos uma pequena parte do código para garantir que atingimos nossas expectativas. Abaixo um exemplo de teste unitário:
[Fact]
public async Task AlbumGetAllAsync()
{
// Arrange
// Act
var albums = await _repo.GetAllAsync();
// Assert
Assert.Single(albums);
}
Existem três partes em um bom teste unitário. A primeira é a Preparação (Arrange) que é usada para configurar quaisquer recursos necessários ao teste. No exemplo acima, não há nenhuma preparação, portanto ela está vazia (mas ainda há um comentário para ela). A parte seguinte chamada de Atuação (Act) é a parte que executa a ação de testar. No exemplo, é chamado o repositório de dados da entidade Album para retornar a lista completa de álbuns da fonte de dados que o repositório está usando. A última parte do teste é quando verificamos ou Validamos (Assert) que a ação do teste foi correta. Para esse teste, é verificado que um único Album foi retornado do repositório de dados.
Usaremos o xUnit para testes unitários neste artigo. O xUnit é um pacote open source para .NET Framework e também para o .NET Core. Iremos olhar para a versão do .NET Core que está incluído na instalação do .NET Core 2.1 SDK. Podemos criar um novo projeto de teste unitário pelo .NET Core cli pelo comando dotnet test
ou através de um template de projeto seja no Visual Studio 2017, VS Code ou o Rider da JetBrain.
Figura 1: Criando um novo projeto de Teste Unitário no Visual Studio 2017
Agora podemos mergulhar na solução ASP.NET Core Web API.
O que deve ser testado nas Web APIs?
Particularmente, sou um grande defensor de usar testes unitários para manter uma API estável e robusta aos clientes. Mas mantenho uma base saudável de como usar testes unitários e o que testar. MInha filosofia é de que testamos nossa solução apenas o suficiente, não mais que o necessário. Isso quer dizer que, posso gerar um monte de comentários sobre isso, mas não sou totalmente preocupado em ter 100% do código coberto por testes. Será que são necessários testes que cobrem as partes importantes da solução de API e isolem cada área independentemente para assegurar o contrato que cada segmento de código faz? Claro que sim, e é isso que quero discutir.
Uma vez que nosso projeto demo Chinook.API é muito fino e pronto para testes de integração (discutido mais à frente nesse artigo), iremos concentrar os testes unitários nos projetos de Domínio (Domain) e Dados (Data). Não irei detalhar mais sobre como fazer testes unitários, vou focar em testar ao máximo os projetos de Domínio e Dados ao consumirem dados que não dependem de nossa base de produção. Esse é o próximo tópico chamado de Dados e Objetos Mock.
Por quê utilizar Dados/Objetos Mock com Testes Unitários?
Vimos o porquê e o quê é necessário para testar unitariamente. É importante saber como testar corretamente unidades de código nas soluções ASP.NET Core Web API. Os dados são a chave para testar as APIs. Ter um conjunto de dados previsíveis que podem ser testados é essencial. É por isso que não recomendo utilizar dados de produção ou qualquer dado que pode mudar com o tempo sem o nosso conhecimento e fora das expectativas. Precisamos de dados estáveis para garantir que todos os testes unitários rodem, e confirmar que o contrato entre o trecho de código e o teste é respeitado. Como exemplo, quando testo o projeto Chinook.Domain para retornar um álbum de ID 42, quero ter certeza de que ele existe e tenha detalhes específicos como o nome do álbum e sua associação com um artista. Também quero garantir que quando retorno uma coleção de álbuns de uma fonte de dados, tenho o formato e tamanhos esperados para o teste unitário que escrevi.
Muitos na indústria usam o termo dados "mock" para identificar esse tipo de dado. Há várias maneiras de gerar dados "mockados" para testes unitários, e espero criarmos um conjunto de dados o mais mundo-real possível. Quanto melhor os dados criados para os testes melhor os testes irão performar. Sugiro termos certeza de que os dados sejam limpos de problemas de privacidade e não contenham dados pessoais ou sensíveis de uma empresa ou cliente.
Para atender à nossa necessidade de dados limpos e estáveis, criei projetos únicos que encapsulam os dados mock para os projetos de Teste Unitário. Vamos chamar o projeto de dados mock de Chinook.MockData (conforme o código fonte). Meu projeto MockData é quase idêntico ao projeto Chinook.Data. Ele tem o mesmo número de repositórios de dados, e cada um atende às mesmas interfaces. Quero que o projeto MockData seja armazenado no contêiner de Injeção de Dependência para que o projeto Chinook.Domain possa consumi-lo como o projeto Chinook.Data que está conectado à base de produção. É por isso que amo a injeção de dependência. Ela permite mudar entre projetos através de configuração e sem mudanças no código.
Testes de Integração: O que é esse novo teste para Web APIs?
Após termos executado e verificado os Testes Unitários (Unit Tests) para nossa solução ASP.NET Core Web API, iremos ver um diferente tipo de teste. Utilizo o Teste Unitário para verificar e confirmar as expectativas em componentes internos da solução. Quando estamos satisfeitos com a qualidade dos testes internos podemos avançar aos testes das APIs nas interfaces externas, que chamamos de Testes de Integração (Integration Testing).
Os Testes de Integração são executados em todos os componentes, para garantir que as APIs serão consumidas com a correta resposta HTTP. Vejo os testes unitários como testes independentes e segmentos de código isolados, enquanto os testes de integração são usados para testar toda a lógica de cada API no meu endpoint HTTP. Este teste seguirá o fluxo inteiro da API, do projeto de Controller até o projeto Supervisor de Domínio (Domain Supervisor) e finalmente no projeto de Dados Repositórios (e de volta o caminho todo para a resposta).
Criando o projeto de Testes de Integração
Aproveitando o conhecimento prévio em testes, a funcionalidade Testes de Integração (Integration Testing) é baseada nas bibliotecas de teste unitários atuais. Utilizarei o xUnit para criar os testes de integração. Após criar um projeto de testes xUnit chamado Chinook.IntegrationTest, precisamos adicionar os pacotes NuGet adequados. Adicione o pacote Microsoft.AspNetCore.TestHost
ao projeto Chinook.IntegrationTest. Esse pacote contém os recursos para executar testes de integração.
Figura 2: Adicionando o pacote Nuget Microsoft.AspNetCore.TestHost
Podemos agora criar nosso primeiro teste de integração para verificar nossa API externamente.
Criando nosso primeiro Teste de Integração
Para iniciar os testes externos de todas as APIs da solução, Iremos criar uma nova pasta chamada API para conter nossos testes. Também criaremos uma nova classe de teste para cada Entidade no domínio de nossa API. Nosso primeiro teste de integração irá cobrir a entidade do tipo Album.
Criando uma nova classe chamada AlbumAPITest.cs na pasta API, adicionamos os seguintes namespaces ao arquivo:
using Xunit;
using Chinook.API;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.Hosting;
Figura 3: Diretivas de Testes de Integração
Agora temos de configurar a classe com nosso TestServer
e HttpClient
para executar os testes. Precisamos de uma variável chamada _client do tipo HttpClient
que será criada com base na inicialização do TestServer do construtor da classe AlbumAPITest. O TestServer empacota um pequeno web server que é criado com base na classe Chinook.API Startup e no ambiente desejado. Neste caso, estamos usando o ambiente de desenvolvimento. Agora temos um web server que está rodando a nossa API e um cliente que entende como chamar as APIs no TestServer. Podemos agora escrever o código para os testes de integração.
Figura 4: Nosso primeiro teste de integração para recuperar todos os Albums
Além do construtor, a Figura 4 também mostra o código para o primeiro teste de integração. O método AlbumGetAllTestAsync
irá testar se a chamada para retornar todos os Albums da API funciona. Assim como na sessão anterior, onde discutimos testes unitários, a lógica para os testes de integração também utiliza a sequência Preparação/Atuação/Validação (Arrange/Assert/Act). Primeiro criamos um objeto HttpRequestMessage
com o verbo HTTP como uma variável da annotation InlineData e o segmento de URI que representa a chamada para todos os Albums ("/api/Album/"). Em seguida temos o HttpClient _client enviando uma requisição HTTP, e finalmente, iremos verificar se a resposta HTTP bate com nossas expectativas, que em nosso caso é um 200 OK. Na Figura 4 há duas maneiras de verificar nossa chamada à API. Podemos usar qualquer uma, mas prefiro a segunda que permite usar o mesmo padrão de verificação de respostas para códigos HTTP Response específicos.
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Também podemos criar testes de integração que precisam testar entidades específicas de nossas APIs. Para esse tipo de teste, iremos incluir um valor adicional ao InlineData que será passado por parâmetro através do método AlbumGetTestAsync . Nosso novo teste segue a mesma lógica e usa os mesmos recursos do teste anterior, mas iremos passar a entidade chave na URI da API para o objeto HttpRequestMessage. Como na Figura 5 abaixo.
Figura 5: O segundo teste de integração para um único Álbum
Após criar todos os testes de integração da API, iremos rodá-los pelo Test Runner e ter certeza que todos passarão. Todos os testes criados podem ser rodados durante o processo de Integração Contínua do DevOps para testar a API tanto no desenvolvimento quanto na entrega (deploy). Temos agora um caminho de execução da API bem testada e mantida pelas fases de desenvolvimento, qualidade (quality assurance), e deploy para os consumidores de nossa API terem uma grande experiência e sem incidentes.
Figura 6: Rodando os testes de integração (Integration Tests) no Visual Studio 2017
Conclusão
Ter um bem pensado plano de testes usando ambos, testes unitários para testes internos e testes de integração para verificações de chamadas externas à API, é tão importante quanto a arquitetura criada para o desenvolvimento da solução ASP.NET Core Web API.
Sobre o Autor
Chris Woodruff (Woody) (Woody) é graduado em Ciências da Computação pela Michigan State University’s College of Engineering. Woody desenvolve e arquiteta software há mais de 20 anos e já trabalhou com diferentes ferramentas e plataformas. Ele é um líder da comunidade, ajudando em eventos como o GRDevNight, o GRDevDay, o West Michigan Day of .NET e o CodeMash. Ele também contribui trazendo o evento Give Camp ao Oeste de Michigan onde profissionais de tecnologia doam seu tempo e expertise para ajudar organizações locais sem fins lucrativos. Como palestrante e podcaster, Woody já falou e discursou sobre diversos temas, incluindo design de banco de dados e open source. Ele é um Microsoft MVP em Visual C#, em Data Platform e em SQL. Em 2010 foi reconhecido como um dos top 20 MVPs do mundo. Woody é um Developer Advocate na JetBrains e evangeliza .NET, .NET Core e produtos JetBrains na América do Norte.
Com o lançamento do .NET Core 2.0, a Microsoft possui uma nova versão principal da plataforma com propósito geral, modular, multiplataforma e de código aberto que foi lançada inicialmente em 2016. O .NET Core foi criado para ter muitas das APIs que estão disponíveis na versão atual do .NET Framework. Ele foi inicialmente criado para permitir a próxima geração de soluções ASP.NET, mas agora impulsiona e é a base para muitos outros cenários, incluindo IoT, nuvem e soluções móveis de próxima geração. Nesta segunda série, abrangendo o .NET Core, exploraremos mais alguns benefícios do .NET Core e como ele pode beneficiar não apenas desenvolvedores tradicionais de .NET, mas também todos os tecnólogos que precisam levar soluções robustas, eficazes e econômicas ao mercado.