Testes Unitários com .NET 5, xUnit, Moq, AutoFixture e Shouldly

Neste artigo vou ensinar sobre escrita de testes unitários em .NET 5, de código utilizando padrão CQRS, utilizando as seguintes ferramentas/bibliotecas:

  • xUnit
  • Moq
  • AutoFixture
  • Shouldly

Vai ter uma abordagem mais prática que teórica, mas vou me esforçar para cobrir os conceitos necessários para o entendimento!

Código-fonte: https://github.com/luisdeol/xunit-moq-autofixture-shoudly-article


Para que Testes Unitários?

Testes Unitários se referem à escrita de código para testar unidades de funcionalidade em seu código.

Entre os principais benefícios, estão:

  • Melhorar o desenho do código, por ser fácil detectar quando um código está fortemente acoplado a implementações, e consequentemente diminuindo a sua testabilidade e forçando sua melhoria.
  • Cobertura de caminhos principais, alternativos e de exceção;
  • Detecção de regressão de erros;
  • Documentação de regra de negócio;

Eu falo melhor sobre esses pontos e os desafios nesse artigo, que é parte de uma série com 4 artigos. Nessa série você vai aprender também sobre xUnit, padrões de escrita de testes unitários, Moq e AutoFixture. Por conta disso, não vou detalhar sobre essas bibliotecas aqui.


O que vai ser testado

O sistema fictício é de gestão de alunos de uma escola, e o componente a ser testado é o Command AddStudentHandler, seguindo o padrão CQRS. Ele tem duas dependências: o IStudentRepository e IErpIntegrationService. Para ter um cenário de maior utilização das bibliotecas Moq e Shoudly, o método Handle do AddStudentHandler vai retornar um objeto AddStudentViewModel, permitindo checagens de suas propriedades em comparação aos dados de entrada.

Logo abaixo coloco o código do AddCommand e AddCommandHandler.

Código AddStudent

    public class AddStudent : IRequest<AddStudentViewModel>
    {
        public string FullName { get; set; }
        public string Document { get; set; }
        public string Class { get; set; }
        public DateTime BirthDate { get; set; }

        public Student ToEntity() 
            => new Student(FullName, Document, Class, BirthDate);
    }

Código AddStudentHandler

public class AddStudentHandler : IRequestHandler<AddStudent, AddStudentViewModel>
    {
        private readonly IStudentRepository _studentRepository;
        private readonly IErpIntegrationService _erpIntegrationService;
        public AddStudentHandler(IStudentRepository studentRepository, IErpIntegrationService erpIntegrationService)
        {
            _studentRepository = studentRepository;
            _erpIntegrationService = erpIntegrationService;
        }

        public async Task<AddStudentViewModel> Handle(AddStudent request, CancellationToken cancellationToken)
        {
            var student = request.ToEntity();

            await _studentRepository.AddAsync(student);

            var erpStudent = ErpStudent.FromEntity(student);
            await _erpIntegrationService.SyncStudent(erpStudent);

            return AddStudentViewModel.FromEntity(student);
        }
    }

A idéia então é validar algumas coisas, como:

  • Métodos do IStudentRepository e IErpIntegrationService foram chamados uma vez;
  • Objeto retornado tem informações consistentes dos dados do parâmetro AddCommand.

Para verificar os outros componentes, indico olhar no código-fonte, já que colocar tudo aqui deixaria “poluído”.

Escrevendo o teste unitário

Primeiramente, a ferramenta utilizada para criação e execução de testes unitários é o xUnit. É uma ferramenta bem popular para isso, e com outras alternativas com o NUnit e MSTest (este último não é tão comum de ser utilizado).

O primeiro passo é criar a classe AddStudentTests, que é onde ficará os métodos de testes. Para marcar um método como teste unitário do xUnit, basta adicionar o atributo [Fact].

O padrão de nomenclatura utilizado é o Given_When_Then, e o padrão de estrutura é o Arrange, Act, Assert.

public class AddStudentTests
    {
        [Fact]
        public async Task ValidStudent_HandlerIsCalled_ReturnValidStudentViewModel() {
            // Arrange

            // Act

            // Assert
        }
    }

Em seguida, é estruturada a parte Arrange, onde as dependências são definidas e seu comportamento alterado através de Mocks. As seguintes chamadas de dependências precisam ser “mockadas” (isto é, ter seu comportamento alterado, seja com retorno ou não):

  • IStudentRepository.AddAsync: aqui não é necessário definir nada mais além do Mock básico, sem alteração explícita, por não ter tipo de retorno;
  • IErpIntegrationService.SyncStudent: neste caso, é definido um retorno de sucesso para este método;

Além disso, ao invés de instanciar manualmente um objeto de tipo AddStudent (que é parâmetro do método Handle do AddStudentHandler), é utilizada a biblioteca AutoFixture para gerar um objeto rapidamente.

Os mocks são então passados para uma instância do AddStudentHandler, através de sua propriedade Object, resultando no código abaixo.

public class AddStudentTests
    {
        [Fact]
        public async Task ValidStudent_HandlerIsCalled_ReturnValidStudentViewModel() {
            // Arrange
            var addStudent = new Fixture().Create<AddStudent>();
            
            var studentRepositoryMock = new Mock<IStudentRepository>();
            var erpIntegrationService = new Mock<IErpIntegrationService>();
            erpIntegrationService.Setup(e => e.SyncStudent(It.IsAny<ErpStudent>())).Returns(Task.FromResult(true));

            var addStudentHandler = new AddStudentHandler(studentRepositoryMock.Object, erpIntegrationService.Object);

            // Act

            // Assert
        }
    }

Terminada essa parte, é feito então o Act, que consiste da chamada do método AddStudentHandler.Handle, passando o objeto de tipo AddStudent criado com o AutoFixture e um CancellationToken.

[Fact]
        public async Task ValidStudent_HandlerIsCalled_ReturnValidStudentViewModel() {
            // Arrange
            var addStudent = new Fixture().Create<AddStudent>();
            
            var studentRepositoryMock = new Mock<IStudentRepository>();
            var erpIntegrationService = new Mock<IErpIntegrationService>();
            erpIntegrationService.Setup(e => e.SyncStudent(It.IsAny<ErpStudent>())).Returns(Task.FromResult(true));

            var addStudentHandler = new AddStudentHandler(studentRepositoryMock.Object, erpIntegrationService.Object);

            // Act
            var result = await addStudentHandler.Handle(addStudent, new CancellationToken());

            // Assert
        }

Finalmente, na parte de Assert, é feita a verificação comentada anteriormente utilizando o Shouldly, que oferece uma sintaxe mais fluída de checagem do que a classe Assert utilizando métodos como o ShouldBe, além do Moq para as checagens das chamadas dos métodos dos mocks.

    public class AddStudentTests
    {
        [Fact]
        public async Task ValidStudent_HandlerIsCalled_ReturnValidStudentViewModel() {
            // Arrange
            var addStudent = new Fixture().Create<AddStudent>();
            
            var studentRepositoryMock = new Mock<IStudentRepository>();
            var erpIntegrationService = new Mock<IErpIntegrationService>();
            erpIntegrationService.Setup(e => e.SyncStudent(It.IsAny<ErpStudent>())).Returns(Task.FromResult(true));

            var addStudentHandler = new AddStudentHandler(studentRepositoryMock.Object, erpIntegrationService.Object);

            // Act
            var result = await addStudentHandler.Handle(addStudent, new CancellationToken());

            // Assert
            result.FullName.ShouldBe(addStudent.FullName);
            result.Document.ShouldBe(addStudent.Document);
            result.Class.ShouldBe(addStudent.Class);
            result.BirthDate.ShouldBe(addStudent.BirthDate);

            studentRepositoryMock.Verify(s => s.AddAsync(It.IsAny<Student>()), Times.Once);
            erpIntegrationService.Verify(s => s.SyncStudent(It.IsAny<ErpStudent>()), Times.Once);
        }
    }

E com isso terminamos a escrita do teste unitário focado no AddStudentHandler! Claro que em um cenário mais real se testariam mais cenários, mas a intenção foi mostrar a estrutura de um teste unitário utilizando as tecnologias xUnit, Moq, AutoFixture e Shoudly.


Quer alavancar sua carreira como Desenvolvedor(a) .NET?

Opa, aqui é o Luis Felipe (LuisDev), criador do blog LuisDev.

Além de Desenvolvedor .NET Sênior, eu sou instrutor de mais de 700 alunos e também tenho dezenas de mentorados.

Conheça o com mais de 800 video-aulas sobre C# e desenvolvimento de APIs com ASP NET Core, Microsserviços com ASP NET Core, Arquitetura de Software, Computação em Nuvem, SQL, HTML, CSS e JavaScript, JavaScript Intermediário, TypeScript, Desenvolvimento Front-End com Angular, e Desenvolvimento Front-end com React. Diversos mini-cursos disponíveis aos alunos e atualizações gratuitas.

Suporte dedicado, e comunidade de centenas de alunos.

Completo e online, destinado a profissionais que querem dar seu próximo passo em sua carreira como desenvolvedores .NET.

Clique aqui para ter mais informações e garantir sua vaga


Espero que o artigo tenha sido útil. Até o próximo artigo!