Melhorando manutenção em testes com AutoFixture

  • Rafael Miceli
  • 6 Abr 2016

Quem nunca precisou refatorar um código que quebrasse os testes que cobria o mesmo?

Ok, dependendo da refatoracão é porque você realmente quebrou algo, mas tem situações em que dia classe precisa de uma nova dependência e isso quebra o Arrange do seu teste, vamos imaginar a seguinte situação.

Problema no Arrange

Digamos que você possui o seguinte teste para garantir que ao criar um aluno o mesmo não possa ser cadastrado com uma matricula já existente (Todo o código inserido aqui pode ser encontrado no meu github):

AlunoServiceTest.cs

passo1

Aluno.cs

passo2

AlunoService.cs

passo3

Excelente, sempre ao criar um aluno garantimos que ele tenha uma matricula única.

Mas, o usuário do sistema disse que quando cadastrar um aluno, também tem que vincular as matérias que ele vai fazer.

Então, vamos criar um teste para garantir que quando o aluno se cadastrar, vincule as matérias que ele selecionou.

AlunoServiceTest.cs

passo4

AlunoService.cs

passo5

Perceba que nessa modificação, no Service de criar aluno inserimos uma nova dependência, como só tínhamos três unitários isso é bem fácil de lidar, mas imagina que tivéssemos dezenas (o que em alguns casos não é difícil), isso seria um bom trabalho, e repetitivo até. Imagina futuramente se tivermos mais uma nova dependência? Vai ficar complicada a coisa. Que tal melhorarmos o Arrange do nosso teste então?

Uma forma que podemos fazer isso é com a design pattern SUTFactory, muito bem explicada por Mark Seemann.

Autofixture

Agora, para um sistema maior imagina ter de criar uma classe SUTFactory para cada classe que você precisa gerenciar as dependências? Bastante código certo?

Com o Autofixture facilitamos muito nosso trabalho.

Para usarmos o Autofixture instalamos o mesmo com nosso package manager no nosso projeto de teste

passo6

Agora, vamos refatorar nosso teste usando o autofixture

passo7

Legal, mas nem tanto ainda. Pois estamos tendo um erro com a interface dos services.

AutoFixture.AutoMoq

Para facilitar mais podemos instalar também um plugin para o Autofixture com moq. Vamos instalar o mesmo no nosso projeto de teste.

passo8

Tendo Instalado, agora vamos refatorar um pouco mais nosso teste.

Preciso adicionar o seguinte trecho para que o Autofixture faça seu trabalho com o moq:

passo9

Se perceber, o Autofixture cria dados randômicos para as propriedades de nossa classe, e com o AutoFixture.AutoMoq ele gera setups com returns randômicos também, o que é um problema para nossos testes de verificar a matricula, precisamos garantir que o retorno do método _alunoRepo.ExisteMatricula seja verdadeiro para o cenário da matricula já existir.

Além disso vamos melhorar nosso teste garantindo que ele não tente buscar as matérias no cenário de matrícula repetida. Pois uma vez indo buscar as matérias, significa que a matricula não era repetida.

Para ajustarmos isso vamos refatorar mais uma vez nosso teste para o seguinte:

[TestClass]
public class AlunoServiceTests
{
    [TestMethod]
    [ExpectedException(typeof(Exception))]        
    public void Dado_Um_Aluno_Quando_Criar_O_Mesmo_Se_A_Matricula_Ja_Existir_Deve_Levantar_Uma_Excecao()
    {
        //Arrange
        var fixture = new Fixture();
        fixture.Customize(new AutoMoqCustomization());

        var materiaServiceMock = fixture.Freeze<Mock<IMateriaService>>();
        var alunoRepoMock = fixture.Freeze<Mock<IAlunoRepo>>();

        alunoRepoMock.Setup(x => x.ExisteMatricula(It.IsAny<string>())).Returns(true);

        var aluno = fixture.Build<Aluno>().Without(a => a.Materias).Create();
        var sut = fixture.Create<AlunoService>();
        
        //Act
        sut.AdcionarAluno(aluno, null);

        //Assert
        materiaServiceMock.Verify(x => x.BuscarMateriaisSelecionados(It.IsAny<List<int>>()), Times.Never);
    }

    [TestMethod]
    public void Dado_Um_Aluno_Quando_Criar_O_Mesmo_Se_A_Matricula_Nao_Existir_Deve_Criar_Com_Sucesso()
    {
        //Arrange
        var fixture = new Fixture();
        fixture.Customize(new AutoMoqCustomization());

        var materiaServiceMock = fixture.Freeze<Mock<IMateriaService>>();
        var alunoRepoMock = fixture.Freeze<Mock<IAlunoRepo>>();

        alunoRepoMock.Setup(x => x.ExisteMatricula(It.IsAny<string>())).Returns(false);

        var aluno = fixture.Build<Aluno>().Without(a => a.Materias).Create();
        var materiasIdsSelecionadas = fixture.CreateMany<int>();
        var sut = fixture.Create<AlunoService>();
        
        //Act
        sut.AdcionarAluno(aluno, materiasIdsSelecionadas.ToList());

        //Assert
        materiaServiceMock.Verify(x => x.BuscarMateriaisSelecionados(It.IsAny<List<int>>()), Times.Once);
        alunoRepoMock.Verify(x => x.CriarAluno(aluno), Times.Once);
    }

    [TestMethod]    
    public void Dado_Um_Aluno_Quando_Criar_Se_O_Mesmo_Selecionou_Materia_Deve_Criar_Com_Sucesso()
    {
        //Arrange
        var fixture = new Fixture();
        fixture.Customize(new AutoMoqCustomization());

        var materiaServiceMock = fixture.Freeze<Mock<IMateriaService>>();
        var alunoRepoMock = fixture.Freeze<Mock<IAlunoRepo>>();

        alunoRepoMock.Setup(x => x.ExisteMatricula(It.IsAny<string>())).Returns(false);
        materiaServiceMock.Setup(x => x.BuscarMateriaisSelecionados(It.IsAny<List<int>>())).Returns(new List<Materia>
        {
            new Materia()
        });

        var aluno = fixture.Build<Aluno>().Without(a => a.Materias).Create();
        var materiasIdsSelecionadas = fixture.CreateMany<int>();
        var sut = fixture.Create<AlunoService>();
        
        //Act
        sut.AdcionarAluno(aluno, materiasIdsSelecionadas.ToList());

        //Assert
        Assert.IsNotNull(aluno.Materias);
        Assert.IsTrue(aluno.Materias.Count > 0);
        materiaServiceMock.Verify(x => x.BuscarMateriaisSelecionados(It.IsAny<List<int>>()), Times.Once);
        alunoRepoMock.Verify(x => x.CriarAluno(aluno), Times.Once);
    }

}

Agora vamos triangularizar nosso cenário de aluno selecionar matérias criando um teste para a situação que o aluno não escolha nenhuma matéria:

[TestClass]
public class AlunoServiceTests
{
    [TestMethod]
    [ExpectedException(typeof(Exception))]        
    public void Dado_Um_Aluno_Quando_Criar_O_Mesmo_Se_A_Matricula_Ja_Existir_Deve_Levantar_Uma_Excecao()
    {
        //Arrange
        var fixture = new Fixture();
        fixture.Customize(new AutoMoqCustomization());

        var materiaServiceMock = fixture.Freeze<Mock<IMateriaService>>();
        var alunoRepoMock = fixture.Freeze<Mock<IAlunoRepo>>();

        alunoRepoMock.Setup(x => x.ExisteMatricula(It.IsAny<string>())).Returns(true);

        var aluno = fixture.Build<Aluno>().Without(a => a.Materias).Create();
        var sut = fixture.Create<AlunoService>();
        
        //Act
        sut.AdcionarAluno(aluno, null);

        //Assert
        materiaServiceMock.Verify(x => x.BuscarMateriaisSelecionados(It.IsAny<List<int>>()), Times.Never);
    }

    [TestMethod]
    public void Dado_Um_Aluno_Quando_Criar_O_Mesmo_Se_A_Matricula_Nao_Existir_Deve_Criar_Com_Sucesso()
    {
        //Arrange
        var fixture = new Fixture();
        fixture.Customize(new AutoMoqCustomization());

        var materiaServiceMock = fixture.Freeze<Mock<IMateriaService>>();
        var alunoRepoMock = fixture.Freeze<Mock<IAlunoRepo>>();

        alunoRepoMock.Setup(x => x.ExisteMatricula(It.IsAny<string>())).Returns(false);

        var aluno = fixture.Build<Aluno>().Without(a => a.Materias).Create();
        var materiasIdsSelecionadas = fixture.CreateMany<int>();
        var sut = fixture.Create<AlunoService>();
        
        //Act
        sut.AdcionarAluno(aluno, materiasIdsSelecionadas.ToList());

        //Assert
        materiaServiceMock.Verify(x => x.BuscarMateriaisSelecionados(It.IsAny<List<int>>()), Times.Once);
        alunoRepoMock.Verify(x => x.CriarAluno(aluno), Times.Once);
    }

    [TestMethod]    
    public void Dado_Um_Aluno_Quando_Criar_Se_O_Mesmo_Selecionou_Materia_Deve_Criar_Com_Sucesso()
    {
        //Arrange
        var fixture = new Fixture();
        fixture.Customize(new AutoMoqCustomization());

        var materiaServiceMock = fixture.Freeze<Mock<IMateriaService>>();
        var alunoRepoMock = fixture.Freeze<Mock<IAlunoRepo>>();

        alunoRepoMock.Setup(x => x.ExisteMatricula(It.IsAny<string>())).Returns(false);
        materiaServiceMock.Setup(x => x.BuscarMateriaisSelecionados(It.IsAny<List<int>>())).Returns(new List<Materia>
        {
            new Materia()
        });

        var aluno = fixture.Build<Aluno>().Without(a => a.Materias).Create();
        var materiasIdsSelecionadas = fixture.CreateMany<int>();
        var sut = fixture.Create<AlunoService>();
        
        //Act
        sut.AdcionarAluno(aluno, materiasIdsSelecionadas.ToList());

        //Assert
        Assert.IsNotNull(aluno.Materias);
        Assert.IsTrue(aluno.Materias.Count > 0);
        materiaServiceMock.Verify(x => x.BuscarMateriaisSelecionados(It.IsAny<List<int>>()), Times.Once);
        alunoRepoMock.Verify(x => x.CriarAluno(aluno), Times.Once);
    }

    [TestMethod]
    [ExpectedException(typeof(Exception))]
    public void Dado_Um_Aluno_Quando_Criar_Se_O_Mesmo_Nao_Selecionou_Materia_Deve_Levantar_Uma_Excecao()
    {
        //Arrange
        var fixture = new Fixture();
        fixture.Customize(new AutoMoqCustomization());

        var materiaServiceMock = fixture.Freeze<Mock<IMateriaService>>();
        var alunoRepoMock = fixture.Freeze<Mock<IAlunoRepo>>();

        alunoRepoMock.Setup(x => x.ExisteMatricula(It.IsAny<string>())).Returns(false);

        var aluno = fixture.Build<Aluno>().Without(a => a.Materias).Create();
        var sut = fixture.Create<AlunoService>();

        //Act
        sut.AdcionarAluno(aluno, new List<int>());

        //Assert
        materiaServiceMock.Verify(x => x.BuscarMateriaisSelecionados(It.IsAny<List<int>>()), Times.Never);
        alunoRepoMock.Verify(x => x.CriarAluno(aluno), Times.Never);
    }
}

Com isso podemos se quiser adicionar mais uma dependência que não vamos quebrar nossos testes. Digamos que o usuário nos pediu para que após cadastrar o aluno em suas matérias, envie um e-mail para o mesmo. Fazemos a seguinte modificação:

passo10

Adicionando a dependência IEmailService não quebramos os testes anteriores, nem em compile time.

Fica ai um sample de Autofixture para situações que geralmente passamos no dia a dia.

Lembrando que todo o código para conferir (a versão sem o Autofixture e com o Autofixture) estão em meu github

comentarios com Disqus Disqus