Ao adotar o padrão CQRS (Command Query Responsibility Segregation), é comum utilizar a biblioteca MediatR para facilitar a separação das responsabilidades e manter o código desacoplado e escalável. Embora o padrão CQRS traga benefícios significativos, ele também aumenta a complexidade do sistema. Por isso, o uso de ferramentas como o MediatR pode ser bastante vantajoso. No entanto, por ser uma biblioteca externa, surgem dúvidas sobre como abordar a criação de testes unitários de maneira eficaz e se existe uma abordagem padrão para isso.

Neste post, vou mostrar como implementei e criei testes unitários para os Handlers do MediatR, que são responsáveis por lidar com os casos de uso do projeto.

Utilitários para Testes

Antes de mais nada, precisamos configurar um projeto dedicado para criar utilitários de teste. Esses utilitários são essenciais para a criação de Mocks e para evitar a repetição de código em todos os testes, além de centralizar as modificações em um único lugar, facilitando a manutenção do código.

Para este projeto, utilizaremos duas bibliotecas fundamentais:

  • Bogus: uma biblioteca .NET que gera dados fictícios realistas de maneira fácil e controlada. Ela é ideal para testes que exigem objetos populados com dados consistentes, como nomes de usuários, endereços ou e-mails, sem depender de dados reais ou de uma base de dados.

  • Moq: uma das bibliotecas de mocking mais populares para .NET. Com o Moq, é possível criar objetos simulados (mocks) que imitam o comportamento de dependências externas, como serviços ou repositórios. Isso facilita o teste de unidades de código sem a necessidade de interagir com implementações reais.

A seguir, apresento os mocks que serão utilizados no exemplo de teste:

Mock do Repositório de Escrita

public class ClienteWriteOnlyRepositorioBuilder
{
    public static IClienteWriteOnlyRepositorio Instancia()
    {
        var mock = new Mock<IClienteWriteOnlyRepositorio>();
        return mock.Object;
    }
}

Esta classe cria e retorna um mock de IClienteWriteOnlyRepositorio usando o Moq. O método Instancia cria um objeto simulado do repositório de escrita, permitindo a realização de testes sem depender de uma implementação real.

Mock do Repositório de Leitura

public class ClienteReadOnlyRepositorioBuilder
{
    private readonly Mock<IClienteReadOnlyRepositorio> _repositorio;
 
    public ClienteReadOnlyRepositorioBuilder() => _repositorio = new Mock<IClienteReadOnlyRepositorio>();
 
    public ClienteReadOnlyRepositorioBuilder RecuperarTodos(List<Cliente> clientes)
    {
        _repositorio.Setup(repositorio => repositorio.RecuperarTodos()).ReturnsAsync(clientes);
        return this;
    }
 
    public ClienteReadOnlyRepositorioBuilder RecuperarPorId(Cliente cliente)
    {
        _repositorio.Setup(repositorio => repositorio.RecuperarPorId(cliente.Id)).ReturnsAsync(cliente);
        return this;
    }
 
    public ClienteReadOnlyRepositorioBuilder RecuperarClienteExistente(string nomeEmpresa)
    {
        _repositorio.Setup(repositorio => repositorio.ExisteClienteComEmpresa(nomeEmpresa)).ReturnsAsync(true);
        return this;
    }
 
    public IClienteReadOnlyRepositorio Instancia() => _repositorio.Object;
}

Esta classe cria e configura um mock de IClienteReadOnlyRepositorio. O ClienteReadOnlyRepositorioBuilder inicializa o mock e define comportamentos esperados para métodos como RecuperarTodos, RecuperarPorId, e RecuperarClienteExistente. Esses métodos configuram o mock para retornar dados simulados conforme necessário nos testes.

Mock da Unidade de Trabalho (UnitOfWork)

public static class UnidadeDeTrabalhoBuilder
{
    public static IUnidadeDeTrabalho Instancia()
    {
        var mock = new Mock<IUnidadeDeTrabalho>();
        return mock.Object;
    }
}

Esta classe cria e retorna um mock de IUnidadeDeTrabalho utilizando o Moq. O método Instancia fornece um objeto simulado da unidade de trabalho, permitindo testar o comportamento sem uma implementação real.

Mock do RegistrarClienteCommand

public class RegistrarClienteCommandBuilder
{
    public static RegistrarClienteCommand Instancia()
    {
        var faker = new Faker();
 
        return new RegistrarClienteCommand(
            new RequisicaoClienteJson(
                faker.Company.CompanyName(),
                (SistemaCliente.Communication.Enums.Porte)faker.Random.Int(0, 2)
            )
        );
    }
}

Esta classe cria uma instância de RegistrarClienteCommand usando a biblioteca Bogus para gerar dados fictícios. O Faker cria um nome de empresa e um valor de porte aleatório, que são usados para construir um objeto RequisicaoClienteJson. Este objeto é então utilizado para criar um RegistrarClienteCommand, facilitando a simulação de Commands em testes.

O caso de uso que será testado é o Registrar da entidade Cliente. Como mencionado anteriormente, criei um mock de RegistrarClienteCommand, que é um record utilizado pelo Handle do caso de uso Registrar.

public record RegistrarClienteCommand(RequisicaoClienteJson requisicaoCliente) : IRequest<RespostaClienteJson>;
 
public class RegistrarClienteCommandHandler : IRequestHandler<RegistrarClienteCommand, RespostaClienteJson>
{}
 
// Implementação ...

Com a configuração dos mocks e a definição do caso de uso, podemos agora criar os testes. Para isso, você deve criar um projeto de teste utilizando Xunit e adicionar o pacote FluentAssertions para facilitar as asserções em seus testes.

Não irei detalhar a configuração do projeto de teste e a adição de pacotes aqui para manter a brevidade. Certifique-se de adicionar as referências aos projetos necessários para que os testes possam acessar e verificar o comportamento do código.

RegistrarClienteCommandTest

Primeiro, precisamos de um método que crie um caso de uso fictício e retorne o Handler de RegistrarClienteCommand.

private static RegistrarClienteCommandHandler CriarUseCase(string? nomeEmpresa = null)
{
    var repositorioWrite = ClienteWriteOnlyRepositorioBuilder.Instancia();
    var repositorioRead = new ClienteReadOnlyRepositorioBuilder();
    var unidadeDeTrabalho = UnidadeDeTrabalhoBuilder.Instancia();
 
    if (!string.IsNullOrWhiteSpace(nomeEmpresa))
        repositorioRead.RecuperarClienteExistente(nomeEmpresa);
 
    return new RegistrarClienteCommandHandler(repositorioWrite, repositorioRead.Instancia(), unidadeDeTrabalho);
}

O método CriarUseCase configura o ambiente para o teste, criando instâncias dos mocks necessários. Ele configura o repositorioRead para simular a existência de um cliente, se um nomeEmpresa for fornecido. Em seguida, retorna uma instância do RegistrarClienteCommandHandler configurado com esses mocks. Esse método ajuda a simplificar a criação do handler para diferentes cenários de teste.

Testes

Teste de Sucesso

[Fact]
public async Task Sucesso()
{
    var command = RegistrarClienteCommandBuilder.Instancia();
 
    var useCase = CriarUseCase();
 
    var resultado = await useCase.Handle(command, default);
 
    resultado.Should().NotBeNull();
    resultado.NomeEmpresa.Should().Be(command.requisicaoCliente.NomeEmpresa);
    resultado.Porte.Should().Be(command.requisicaoCliente.Porte);
}

Neste teste, um Command é criado usando RegistrarClienteCommandBuilder.Instancia(), e o caso de uso é configurado com o método CriarUseCase. O Handle do handler é chamado com o Command, e o resultado é verificado usando FluentAssertions. Asserções são feitas para garantir que o resultado não seja nulo e que os valores retornados correspondam aos valores fornecidos no Command.

Teste de Falha

[Fact]
public async Task ClienteExistente_DeveRetornarErro()
{
    var command = RegistrarClienteCommandBuilder.Instancia();
 
    var useCase = CriarUseCase(command.requisicaoCliente.NomeEmpresa);
 
    Func<Task> acao = async () => await useCase.Handle(command, default);
 
    var resultado = await acao.Should().ThrowAsync<Exception>();
 
    resultado.Where(ex => ex.Message.Contains(ClienteMensagensDeErro.CLIENTE_JA_REGISTRADO));
}

Neste teste, um Command é criado e o caso de uso é configurado para simular a existência de um cliente com o nome fornecido. O teste verifica se o Handle do handler lança uma exceção quando o cliente já está registrado. A exceção é verificada com FluentAssertions para garantir que a mensagem de erro contenha a mensagem esperada.

Conclusão

Neste post, vimos como testar handlers do MediatR usando o padrão CQRS. Exploramos a configuração de mocks, além da criação de testes unitários com Xunit e FluentAssertions. Essas práticas garantem que seus handlers funcionem corretamente e ajudam a manter seu código limpo e confiável.

Projeto