DEV Community

Víctor Fructuoso
Víctor Fructuoso

Posted on

Otimizando os testes de uma aplicação .NET em mais de 90%

Recentemente abri uma caixa de perguntas no meu Instagram @fructuoso.dev sobre testes de unidade e me fizeram a seguinte pergunta:

Qual a diferença entre ClassFixture e CollectionFixture?

Para entendermos a diferença entre os dois, primeiro precisamos ter em mente que o xUnit trabalha diferente dos demais frameworks de testes (se você quiser conhecer um pouco mais sobre esses frameworks recentemente escrevi um artigo comparando os três).

Entender o ciclo de vida dele pode impactar significante o tempo de execução dos seus testes, neste artigo veremos um cenário onde conseguimos reduzir o tempo de execução em mais de 90%.

Imaginem que temos as seguintes classes de testes em nosso projeto:

Classes de Testes

Cada uma das duas classes (ClasseA e ClasseB) possuem dois cenários de testes (chamados apenas para fins de explicação de Teste1 e Teste2).

Diferentemente do MSTest e do nUnit, o xUnit irá criar uma instancia da classe para executar cada um dos cenários de teste.

Desta forma teremos algo mais ou menos assim:

Cenários de Testes

Sabendo disso é fácil associar que, para cada cenário de teste temos uma nova chamada ao construtor e um escopo totalmente isolado entre os cenários (que por padrão rodam em paralelo).

Onde é que entram as Fixtures então?

Imaginem que para um determinado conjunto de cenários de testes queremos compartilhar recursos ou estados, o compartilhamento de estado é mais comum para testes integrados pois testes de unidade devem ser totalmente isolados e independentes. Porém em testes de unidade é comum precisar reutilizar algum recurso que possua um alto custo para instanciar como por exemplo uma WebAPI ou um DbContext do EntityFramework.

O ClassFixture é utilizado sempre que queremos compartilhar tais recursos com outros cenários de teste de uma mesma classe.

Já o CollectionFixture é utilizado sempre que queremos compartilhar os recursos com cenários de testes de classes distintas.

Imaginem que temos o seguinte cenário:

O FixtureX é um ClassFixture, enquanto o FixtureZ é um CollectionFixture, teríamos então algo mais ou menos assim:

ClassFixture e CollectionFixture

Enquanto os recursos presentes na FixtureX são compartilhados apenas pelos cenários de teste de uma mesma classe. Os recursos presentes na FixtureZ serão compartilhados por TODOS os cenários de testes nas classes que estiverem associadas a ela.

Para recursos em que é necessário o isolamento e que apenas o próprio cenário de teste o recomendado é colocar o recurso na própria classe que implementa o cenário de teste.

Ao colocar o exemplo apresentado para rodar teremos algo mais ou menos assim (o resultado pode mudar de acordo com a execução, pois lembre que os testes rodam assincronamente)

FixtureZ.Construtor
    FixtureX.Construtor
        ClasseA.Construtor
            ClasseA.Teste1
        ClasseA.Dispose
        ClasseA.Construtor
            ClasseA.Teste2
        ClasseA.Dispose
    FixtureX.Dispose
    FixtureX.Construtor
        ClasseB.Construtor
            ClasseB.Teste1
        ClasseB.Dispose
        ClasseB.Construtor
            ClasseB.Teste2
        ClasseB.Dispose
    FixtureX.Dispose
FixtureZ.Dispose
Enter fullscreen mode Exit fullscreen mode

Entendido o que são e como funcionam as Fixture, vamos avaliar o seguinte cenário:

  • Temos um projeto WebAPI em que temos alguns Controllers;
  • Precisamos testar nossa WebAPI de forma integrada usando o Microsoft.AspNetCore.Mvc.Testing (com ajuda dele nós conseguimos levantar uma instância da nossa WebAPI em tempo de execução para que ela seja chamada via HTTP em nossos cenários de testes)

O comparativo foi feito utilizando a seguinte abordagem.

Em nossa API temos um método que recebe um objeto através de uma chamada POST, passa por todas as camadas da aplicação até chegar em um banco de dados utilizando Entity Framework InMemory.

A quantidade de cenários é representada pela execução do mesmo método passando entradas diferentes.

Então foram explorados dois cenários, o primeiro usando uma instância compartilhada entre os cenários através do conceito de Fixture, já o segundo foi implementado com uma instância dedicada gerada através do construtor da classe.

Ao simular algumas execuções chegamos aos seguintes cenários:

Quantidade Instância Compartilhada Instância Dedicada %
1 1.4 1.3 -8%
5 1.4 2.1 33%
10 1.4 3.3 58%
100 1.4 16.9 92%

Como dito anteriormente as fixtures podem ser utilizadas para compartilhar recursos e estado, dependendo do cenário em que está sendo implementado pode gerar uma grande diferença de performance, porém em outros casos, o compartilhamento de recursos pode gerar efeitos colaterais no resultado dos testes, visto que os testes são executados ao mesmo tempo, logo se uma variável for modificada / consumida por mais de um teste ao mesmo tempo o resultado do teste pode variar a cada execução, ferindo assim um dos princípios básicos dos testes de unidade.

Espero que vocês tenham gostado do conteúdo, não deixem de deixar um like e me seguir para que não percam nenhum conteúdo.

Repositório GitHub

Discussion (0)