Testes de Unidade - Usando xUnit e MOQ
Hoje verermos como criar testes de unidade na plataforma .NET usando o xUnit e o MOQ. |
Um teste de unidade pode ser considerado como o menor pedaço de código que pode ser logicamente isolado em um sistema, geralmente pensamos no menor pedaço de código logicamente isolado como nossas funções. Com este pequeno pedaço de código, podemos executar testes automatizados que garantem que nosso código esteja sempre gerando o resultado correto.
Por que devo testar meu código ?
Vejamos alguns bons motivos para realizar os testes de unidade.
Na plataforma .NET, existem várias ferramentas disponíveis para realizar testes de unidade. Aqui estão algumas das ferramentas mais populares:
Dentre as 3 opções de ferramentas mais comumente usadas o MSTest é maduro mas é mais lento, o NUnit também é maduro e rápido e o xUNit é mais novo e rápido.
Vamos criar dois projetos usando o VS Code :
Recursos usados:
Microsoft.NET.Test.Sdk
Criando e configurando o projeto
Abra um terminal de comandos e digite o comando : dotnet new console -n TestesUnidade -f net7.0
Este comando cria um projeto console usando a versão do .NET 7.0 pois no meu ambiente eu já baixei o NET 8.0 que esta em preview.
Com isso criamos um projeto console na pasta TestesUnidade no sistema local.
Estes
pacotes NuGet são usados no ecossistema .NET para habilitar a
execução e relatórios de testes de unidade.
Agora entre no projeto criado e crie a classe Usuario.cs com o código abaixo:
namespace TesteUnidade { public record Usuario(string Nome, string Sobrenome) { public int Id { get; init; } public DateTime DataCriacao { get; init; } = DateTime.UtcNow; public string Telefone { get; set; } = "+55 "; public bool EmailVerificado { get; set; } = false; } public class GerenciaUsuario { private readonly List<Usuario> _usuarios = new (); private int contador = 1; public IEnumerable<Usuario> TodosUsuarios => _usuarios; public void AdicionaUsuario(Usuario usuario) { _usuarios.Add(usuario with {Id = contador++}); } public void AtualizaTelefone(Usuario usuario) { var dbUsuario = _usuarios.First(x => x.Id == usuario.Id); dbUsuario.Telefone = usuario.Telefone; } public void VerificarEmail(int id) { var dbUsuario = _usuarios.First(x => x.Id == id); dbUsuario.EmailVerificado = true; } } } |
Entendendo o código usado:
Iniciamos criando a o record Usuario onde a palavra-chave "record" introduzida no C# 9 permite definir classes de dados imutáveis de maneira mais concisa e elegante. Nesse caso, a classe "Usuario" tem dois campos, "Nome" e "Sobrenome", que são definidos no construtor da classe.
A seguir, definimo na classe "Usuario" quatro propriedades: "Id", "DataCriacao", "Telefone" e "EmailVerificado".
A propriedade "Id" é uma propriedade auto-implementada que permite obter e inicializar o valor do identificador do usuário. As propriedades "DataCriacao", "Telefone" e "EmailVerificado" são propriedades de inicialização que permitem definir valores padrão para essas propriedades no momento da criação do objeto.
A propriedade "DataCriacao" é inicializada com a hora atual em UTC usando a classe "DateTime" do .NET. A propriedade "Telefone" é inicializada com a string "+55 ", representando o código do país, seguido de um espaço em branco. A propriedade "EmailVerificado" é inicializada como "false", indicando que o e-mail do usuário ainda não foi verificado.
Finalmente, as propriedades "Telefone" e "EmailVerificado" são propriedades auto-implementadas que permitem obter e definir os valores dessas propriedades. No entanto, a propriedade "Telefone" só permite definir um valor para a propriedade, enquanto a propriedade "EmailVerificado" permite definir e obter o valor da propriedade.
A seguir criamos a classe GerenciaUsuario que utiliza o record Usuario e que é responsável por gerenciar uma lista de usuários. Essa classe possui três métodos principais: "AdicionaUsuario", "AtualizaTelefone" e "VerificarEmail", que permitem adicionar, atualizar e verificar o email de um usuário na lista.
A classe utiliza o record "Usuario" que foi definido anteriormente para representar os dados dos usuários. O campo "_usuarios" é uma lista privada de usuários, que é inicializada na declaração da classe usando a sintaxe de inicialização de objeto.
O método "AdicionaUsuario" adiciona um novo usuário à lista, atribuindo um ID único ao usuário. O campo "contador" é utilizado para manter o controle dos IDs dos usuários, e é incrementado a cada vez que um novo usuário é adicionado.
O método "AtualizaTelefone" atualiza o número de telefone de um usuário existente na lista. Ele busca o usuário correspondente na lista utilizando o ID do usuário, e atualiza o número de telefone do usuário encontrado com o número de telefone do usuário passado como parâmetro.
O método "VerificarEmail" marca um usuário existente na lista como tendo seu email verificado. Ele busca o usuário correspondente na lista utilizando o ID do usuário e, em seguida, define a propriedade "EmailVerificado" do usuário encontrado como verdadeiro.
Esses métodos são exemplos simples de como podemos utilizar records em C# para representar dados e realizar operações comuns de CRUD (create, read, update, delete) em coleções de objetos. A utilização de records torna o código mais legível e menos propenso a erros, já que a sintaxe concisa e declarativa dos registros permite que nos concentremos nas operações importantes que estamos realizando, em vez de nos detalhes de implementação.
Criando o projeto de testes de unidade
Vamos
criar agora o projeto de testes . Para isso , vamos digitar no terminal de comandos:
dotnet new xunit -n TesteUnidade.Test -f net7.0
dot
Uma vez que o projeto foi criado com sucesso, precisamos adicionar uma referência neste projeto ao nosso projeto inicial TestesUnidade digitando o comando:
dotnet add TesteUnidade.Test/TesteUnidade.Test.csproj reference TesteUnidade\TesteUnidade.csproj
Neste momento o arquivo de projeto TesteUnidade.Test.csproj deverá ter o seguinte código:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TesteUnidade\TesteUnidade.csproj" />
</ItemGroup>
</Project>
|
Observe as referências aos pacotes xunit e a projeto TesteUnidade.csproj.
Agora vamos criar a classe UsuarioTests no projeto de testes definindo o seguinte código:
namespace TesteUnidade.Test;
public class UsuarioTests
{
[Fact]
public void Add_CriaNovoUsuario()
{
// Arrange
var gerenciaUsuario = new GerenciaUsuario();
// Act
gerenciaUsuario.AdicionaUsuario(new(
"José Carlos", "Macoratti"
));
// Assert
var usuarioSalvo = Assert.Single(gerenciaUsuario.TodosUsuarios);
Assert.NotNull(usuarioSalvo);
Assert.Equal("José Carlos", usuarioSalvo.Nome);
Assert.Equal("Macoratti", usuarioSalvo.Sobrenome);
Assert.NotEmpty(usuarioSalvo.Telefone);
Assert.False(usuarioSalvo.EmailVerificado);
}
[Fact]
public void Verify_VerificaEmailAddress()
{
// Arrange
var gerenciaUsuario = new GerenciaUsuario();
// Act
gerenciaUsuario.AdicionaUsuario(new(
"José Carlos", "Macoratti"
));
var primeiroUsuario = gerenciaUsuario.TodosUsuarios.ToList().First();
gerenciaUsuario.VerificarEmail(primeiroUsuario.Id);
// Assert
var usuarioSalvo = Assert.Single(gerenciaUsuario.TodosUsuarios);
Assert.True(usuarioSalvo.EmailVerificado);
}
[Fact]
public void Update_UpdateTelefoneNumber()
{
// Arrange
var gerenciaUsuario = new GerenciaUsuario();
// Act
gerenciaUsuario.AdicionaUsuario(new(
"José Carlos", "Macoratti"
));
var primeiroUsuario = gerenciaUsuario.TodosUsuarios.ToList().First();
primeiroUsuario.Telefone = "+5501070808080";
gerenciaUsuario.AtualizaTelefone(primeiroUsuario);
// Assert
var usuarioSalvo = Assert.Single(gerenciaUsuario.TodosUsuarios);
Assert.Equal("+5501070808080", usuarioSalvo.Telefone);
}
}
|
Cada um dos métodos de teste é marcado com o atributo [Fact], indicando que é um método de teste que deve ser executado pela estrutura de testes.
Vamos entender o código :
Esses métodos de teste são importantes porque ajudam a garantir que os métodos da classe "GerenciaUsuario" estejam funcionando corretamente e sem erros. Eles também ajudam a documentar o comportamento esperado dos métodos da classe, tornando mais fácil para outros desenvolvedores entenderem o que a classe faz e como ela deve ser usada.
Este seria um exemplo básico do uso do xUnit para criar testes de unidade na plataforma .NET.
Estando no projeto de testes vamos executar o comando : dotnet test
Ao executar o comando "dotnet test", o SDK do .NET compila o projeto de teste e os projetos de destino em modo de depuração e, em seguida, executa os testes de unidade em paralelo em um processo separado.
O resultado dos testes é exibido no console, mostrando se cada teste passou ou falhou e, em caso de falha, quais foram as informações adicionais sobre o erro. Além disso, um arquivo de relatório de teste pode ser gerado para facilitar a análise dos resultados dos testes.
Usando o MOQ
Agora vamos para a próxima etapa e verificar como podemos utilizar MOQ.
Mas o que é Moq e o que significa Mocking?
Se é a primeira vez que você ouve os termos "mock", "objetos mock", "mocar" e "mocking" você vai estranhar, e, isso é normal, afinal essa palavra ainda não foi incorporada ao seu vocabulário. A partir de hoje você aprendeu mais uma palavra.
Existe na língua portuguesa o verbo mocar que significa enganar, atraiçoar ou ainda esconder (popular), mas na área de software mocar objetos ou Mock Objects significa objetos que imitam objetos reais para realização de testes de software.
Na programação orientada a objeto, objetos mock ou fictícios são objetos simulados que imitam o comportamento de objetos reais. Os objetos Mocks são geralmente usados em testes de unidade.
Assim, os objetos Mock são criados para testar o comportamento de algum outro objeto(real); com isso estamos mocando, ou seja, simulando ou fingindo o objeto real e fazendo algumas operações de forma controlada de modo que o resultado retornado (teste) é sempre válido.
Desta forma quando um serviço depende de outro serviço e queremos testar esse serviço, em vez de fazer o processo de inicialização completo do segundo serviço, podemos mocar o serviço (fingindo que ele está totalmente funcional) e podemos executar nossos testes com base nisso.
Para mostrar isso vamos criar uma nova classe no projeto TesteUnidade chamada CarrinhoCompra incluindo o código a seguir:
namespace TesteUnidade; public record Produto(int Id, string Nome, double Preco); public interface IDatabaseService { bool SalvarItemCarrinhoCompra(Produto produto); bool RemoverItemCarrinhoCompra(int? id); } // CarrinhoCompra public class CarrinhoCompra { private IDatabaseService _dbService; public CarrinhoCompra(IDatabaseService dbService) { _dbService = dbService; } public bool AddProduto(Produto? produto) { if (produto == null) return false; if (produto.Id == 0) return false; _dbService.SalvarItemCarrinhoCompra(produto); return true; } public bool DeleteProduto(int? id) { if (id == null) return false; if (id == 0) return false; _dbService.RemoverItemCarrinhoCompra(id); return true; } } |
A interface IDatabaseService possui dois métodos: SalvarItemCarrinhoCompra e RemoverItemCarrinhoCompra, que são responsáveis por salvar e remover um item do carrinho no banco de dados, respectivamente.
A classe Produto é um record que representa um produto com um Id, um Nome e um Preco.
O método AddProduto da classe CarrinhoCompra adiciona um produto no carrinho, verificando se o objeto passado não é nulo e se o Id do produto é diferente de zero. Caso positivo, chama o método SalvarItemCarrinhoCompra da instância de IDatabaseService injetada via construtor e retorna true. Caso contrário, retorna false.
O método DeleteProduto da classe CarrinhoCompra remove um produto do carrinho, verificando se o id passado não é nulo e se é diferente de zero. Caso positivo, chama o método RemoverItemCarrinhoCompra da instância de IDatabaseService injetada via construtor e retorna true. Caso contrário, retorna false.
Criando testes de unidade usando o MOQ
Agora vamos começar a criar testes de unidade para essas funcionalidades definidas na classe CarrinhoCompra em nosso projeto de testes.
Como você pode ver, esta funcionalidade depende de um serviço database, o que significa que precisamos simular esse serviço para que possamos testar as implementações que escrevemos.
Vamos iniciar instalando o pacote
Moq no projeto de testes :
dotnet add package Moq
Agora vamos criar uma nova classe dentro do nosso
projeto de testes
TesteUnidade.Test chamada
CarrinhoCompraTest usando o seguinte código :
using Moq;
namespace TesteUnidade.Test;
public class CarrinhoCompraTest
{
public readonly Mock<IDatabaseService> _dbServiceMock = new();
[Fact]
public void AddProduto_Success()
{
var product = new Produto(1, "Caderno", 5);
_dbServiceMock.Setup(x => x.SalvarItemCarrinhoCompra(product)).Returns(true);
// Arrange
CarrinhoCompra carrinhoCompra = new (_dbServiceMock.Object);
// Act
var result = carrinhoCompra.AddProduto(product);
// Assert
Assert.True(result);
_dbServiceMock.Verify(x => x.SalvarItemCarrinhoCompra(It.IsAny<Produto>()), Times.Once);
}
[Fact]
public void AddProduto_Failure_InvalidPayload()
{
// Arrange
CarrinhoCompra carrinhoCompra = new (_dbServiceMock.Object);
// Act
var result = carrinhoCompra.AddProduto(null);
// Assert
Assert.False(result);
_dbServiceMock.Verify(x => x.SalvarItemCarrinhoCompra(It.IsAny<Produto>()), Times.Never);
}
[Fact]
public void RemoveProduto_Success()
{
var product = new Produto(1, "Caderno", 5);
_dbServiceMock.Setup(x => x.RemoverItemCarrinhoCompra(product.Id)).Returns(true);
// Arrange
CarrinhoCompra carrinhoCompra = new (_dbServiceMock.Object);
// Act
var result = carrinhoCompra.DeleteProduto(product.Id);
// Assert
Assert.True(result);
_dbServiceMock.Verify(x => x.RemoverItemCarrinhoCompra(It.IsAny<int>()), Times.Once);
}
[Fact]
public void RemoveProduto_Failed()
{
_dbServiceMock.Setup(x => x.RemoverItemCarrinhoCompra(null)).Returns(false);
// Arrange
CarrinhoCompra carrinhoCompra = new (_dbServiceMock.Object);
// Act
var result = carrinhoCompra.DeleteProduto(null);
// Assert
Assert.False(result);
_dbServiceMock.Verify(x => x.RemoverItemCarrinhoCompra(null), Times.Never);
}
[Fact]
public void RemoveProduto_Failed_InvalidId()
{
_dbServiceMock.Setup(x => x.RemoverItemCarrinhoCompra(null)).Returns(false);
// Arrange
CarrinhoCompra carrinhoCompra = new (_dbServiceMock.Object);
// Act
var result = carrinhoCompra.DeleteProduto(0);
// Assert
Assert.False(result);
_dbServiceMock.Verify(x => x.RemoverItemCarrinhoCompra(null), Times.Never);
}
}
|
O objetivo do teste é garantir que os métodos AddProduto e RemoveProduto da classe CarrinhoCompra funcionem corretamente.
Para isso, são definidos quatro testes de unidade, cada um usando um cenário diferente, que são os seguintes:
Executando o teste usando o comando : dotnet test teremos o resultado abaixo:
Nos exemplos apresentados, são utilizados os seguintes recursos do pacote MOQ:
Pegue os projetos aqui: TesteUnidadeMOQ.zip (sem as referências)
"Esta é uma palavra fiel, e digna de toda a aceitação, que Cristo
Jesus veio ao mundo, para salvar os pecadores, dos quais eu sou o
principal."
1 Timóteo
1:15
Referências: