DEV Community

Cover image for Cell CMS - Criando testes de maneira prática
Rodolpho Alves
Rodolpho Alves

Posted on • Updated on

Cell CMS - Criando testes de maneira prática

Intro

No último post falamos sobre como utilizar Docker e suas ferramentas de suporte dentro do Visual Studio, quais são suas vantagens e como Containers resolvem vários problemas "clássicos" do deploy.

No post de hoje vamos fazer algo que deveríamos ter feito desde o começo do Cell CMS: Criar nossos projetos de testes unitários.

O branch principal para o post de hoje será o feature/create-tests.

GitHub logo rodolphocastro / cell-cms

CMS leve, self-contained e prático de utilizar! Feito por desenvolvedores e para desenvolvedores!

Cell CMS

Branch Status Descrição
Master Build and Test Ciclo estável, recomendado para produção
Develop Build and Test Ciclo em desenvolvimento, recomendado para entusiastas

Cell CMS é um content management system que visa ser:

  • Leve
  • Auto Contido (self-contained)
  • Prático de Utilizar

Nosso foco é em disponibilizar um CMS que desenvolvedores possam facilmente referenciar em seus aplicativos, sites e sistemas.

📚 Instruções

Utilizando uma Versão publicada

WIP, iremos suportar imagens Docker e executáveis

Compilando

Você precisará ter instalado em seu ambiente o SDK 5.0.101 do Dotnet.

Uma vez configurado basta executar dotnet build .\cell-cms.sln na raiz do repositório.

Testando

Execute dotnet test .\cell-cms.sln na raiz do repositório.

Caso queira capturar informações de cobertura de testes utilize:

dotnet test --no-restore --collect:"XPlat Code Coverage" .\cell-cms.sln

Configurações

Autenticação/Autorização

O CellCMS utiliza o Azure Active Directory como provider de identidade, então você terá de configurar sua instância do AAD conforme explicado neste post.

As seguintes variáveis de ambiente…

Cover do artigo pega lá no unDraw

Últimas alterações do Cell CMS

Faz bastante tempo desde o meu último post. Pois é! Infelizmente acabei de distraindo com outros afazeres e estudos e acabei deixando de lado o bom hábito de escrever 😅.

Mas o Cell CMS teve algumas alterações entre o último post e este! De maneira resumida:

  1. Migramos para .NET 5 (originalmente estávamos no .NET Core 3.1)
  2. Criamos uma pipeline no GitHub Actions para compilar o projeto continuamente
  3. Inúmeras alterações no meu ambiente de desenvolvimento

Eventualmente escreverei um pouco sobre estes processos, provavelmente começando pelo do ambiente (em breve devo reconfigurar mais uma vez). Prometo!

Sem mais delongas, vamos para o conteúdo em si!

Testes Unitários: O que são, de onde vieram e para que servem

De maneira bem resumida podemos dizer que testes unitários (na programação orientada a objetos) são rotinas automatizadas para garantir o funcionamento de uma classe.

Mas o que isso quer dizer, na prática?

Na prática isso quer dizer que você terá um conjunto de classes e métodos especializadas em testar de maneira rápida cada classe que compõe o seu sistema, mas preste atenção pois eles não fazem tudo! Como o próprio nome já diz o escopo deste teste é a unidade (que, eventualmente, compõem o todo do seu sistema 🤷‍♂️).

Outra grande vantagem de ter uma boa cobertura de testes unitários é poder refatorar sem medo, afinal os testes vão garantir que você não quebrou nenhuma API pública (ou se você precisou quebrar pelo menos vai te lembrar de avisar os consumidores dessa API como um [Obsolete] antes de remover de vez!).

Se você quiser saber mais sobre refatorar um amigo meu está fazendo uma série de vídeos especialmente sobre isso, dê uma olhada no canal AspiraPlay no YouTube!

Existem, além dos testes unitários, vários outros tipos de testes (manuais e automatizados) mas para o desenvolvedor o teste mais rápido de fazer e executar será, sempre, o unitário. E por isso vamos falar primeiros deles antes de pensarmos em testes de integração, funcionalidades, aceite, etc.

Uma boa leitura para esse assunto é o próprio site do grande Martin Fowler.

Como escrever um bom teste unitário

Seja qual for o framework ou linguagem de programação que você esteja utilizando sempre encontrará essas características em um bom teste unitário.

Nome do Teste

O primeiro elemento importante é o nome do seu teste. É no nome em que vamos dar aquela resumida em:

  1. O que estamos testando
  2. Sob quais condições/estado estamos testando
  3. O que esperamos

Em alguns frameworks o nome do teste será o nome do método que executa a rotina de teste, em outros são utilizadas anotações, classes, atributos, comentários... Mas a constante é sempre essa:

Você deve identificar o que está testando, sob quais condições e o que você espera de resultado.

Entradas (Inputs)

O segundo elemento importante para seu teste é identificar as entradas para a rotina que você está testando. Note que isso poderá mudar bastante mas você sempre deve pensar em isolar bem o que você quer que entre no seu teste para que garanta que ele é preciso!

Alguns exemplos de testes e inputs:

  1. Testar uma classe que cria um JSON -> Input seriam objetos diferentes
  2. Testar uma classe que utiliza outra para salvar algo -> Input seria a classe que será utilizada via mock e o dado que seria salvo
  3. Testar uma regra de negócio -> Input que consiga disparar esta regra de negócio e inputs que falhem a regra de negócio

Não se preocupe ainda com o que é um mock, falaremos disso depois!

O que está sendo testado (Subject)

O terceiro elemento (que apesar da ordem é o mais importante!) é o que você quer testar de verdade!

A importância de saber quem é este elemento é vital para que seu teste seja unitário de verdade. Se você não consegue identificar um único elemento você provavelmente está escrevendo um teste de integração ou você precisa rever as dependências de sua classe.

O estado do que está sendo testado (State)

Em quarto lugar você precisa pensar qual o estado que você quer testar. Isso pode ser:

  1. Uma dependência falhando
  2. Uma input inválida
  3. Uma referência nula
  4. Um parâmetro sendo omitido

Nunca tente testar todos os possíveis estados! Uma cobertura de 100% de testes pode significar que você está investindo mais tempo em testes do que funcionalidades novas!

Minha preferência pessoal é começar testando sempre dois estados: O válido e o principal inválido.

O que é esperado

Finalmente o último elemento é: o que você espera que aconteça?

Por exemplo:

  1. Retorne null
  2. Retorne um objeto válido
  3. Lance uma Exception
  4. Dê timeout após ... milissegundos

Juntando tudo: AAA

Como lembrar de tudo isso? Uma das práticas mais comuns é sempre lembras dos 3 As:

  1. Arrange: Escolha as inputs, o que será testado e monte o cenário
  2. Act: Realize a ação que você quer que seja testada
  3. Assert: Verifique que a ação fez o que você esperava

Eu normalmente abro meus testes já escrevendo 3 linhas de comentários e então vou preenchendo a lógica:

// Arrange
var subject = new MinhaClasseSendoTestada(null);

// Act
var result = subject.FazAlgumaCoisa();

// Assert
Assert.IsNull(result);
Enter fullscreen mode Exit fullscreen mode

Mocks e Stubs

Não sou um expert no assunto (dê uma pesquisada sobre Kent Beck, TDD Chicago e London para saber mais sobre isso) mas de maneira bem resumida:

Um Mock é um objeto que simula o comportamento de outro objeto, permitindo que você controle o que o objeto real faria e valide quantas vezes ele foi chamado, com quais parâmetros, etc. Você estará testando por comportamento.

Um STUB é um objeto que simula apenas o retorno de outro objeto, entregando respostas fixas para chamadas fixas, controlando o que é retornado quando chamariam o objeto real e apenas isso. Você estará testando por estado, maior parte das vezes, porém também poderia testar por comportamento com algumas alterações.

Unit Tests com .NET

Agora que temos uma ideia do que são testes unitários vamos pensar neles no universo do .NET, de cara já podemos dizer que existem 3 frameworks populares de testes unitários:

  1. nUnit -> Mais clássico mas amplamente utilizado, com diversos plugins e runners
  2. xUnit -> Mais moderno e próximo à ideia de TDD, também é amplamente utilizado
  3. MSTest -> Não recomendado mais atualmente

A principal diferença que nós, como desenvolvedores, vamos notar entre o nUnit e o xUnit é a maneira com que eles lidam com as classes de teste.

Por padrão o xUnit cria uma instância da classe para executar cada método de teste. Você sempre terá uma certa segurança de que o estado da sua classe de teste está limpo.

Enquanto isso o nUnit utiliza a mesma instância da classe para executar todos os métodos de teste. Você terá de tomar cuidado com instâncias que podem ser alteradas pelos testes em si, levando a interdependências, travando a ordem de execução, etc...

Porém, seja xUnit ou nUnit, as classes de teste conterão os seguintes elementos:

  1. Métodos() indicando as rotinas de teste
  2. [Atributos] indicando que um método é um teste com ou sem parâmetros
  3. [OutrosAtributos] indicando métodos de configuração e limpeza dos seus testes

Não vou entrar a fundo nos atributos e setups de nenhum dos dois, porém na seção de codificação mesmo você poderá ver como fica uma classe de teste utilizando o xUnit!

AutoFixture - Criação automática de massa de dados

Comentamos acima sobre a necessidade de sabermos as inputs dos nossos testes, certo? Porém as vezes você sabe o que precisa de input mas não se importa com os detalhes, certo? Nesses cenários pode ser tornar chato você ter de sempre adicionar N parâmetros no seu teste e sempre criar um new ObjetoInput(param1, param2, ...);. A biblioteca AutoFixture resolve exatamente isso!

De maneira sucinta:

AutoFixture gera a massa de dados para testes automaticamente para você. Permitindo que você gaste menos tempo na fase de Arrange de seu teste.

E melhor ainda: Ele funciona, de cara, sem nenhuma configuração em boa parte dos casos, porém você pode configurar ele com regras específicas para casos específicos!

GitHub logo AutoFixture / AutoFixture

AutoFixture is an open source library for .NET designed to minimize the 'Arrange' phase of your unit tests in order to maximize maintainability. Its primary goal is to allow developers to focus on what is being tested rather than how to setup the test scenario, by making it easier to create object graphs containing test data.

NSubstitute - Criação e Configuração de Mocks

A biblioteca NSubstitute é mais uma facilitadora para a nossa fase de Arrange dos testes. Lembra sobre mocks e stubs? Esse carinha aqui é quem vai criar os mocks pra gente.

De maneira curta:

NSubstitute cria, automaticamente, mocks para interfaces e métodos virtuais. Além disso você poderá controlar o que esses mocks retornam, quantas vezes podem ser chamados, etc.

GitHub logo nsubstitute / NSubstitute

A friendly substitute for .NET mocking libraries.

Nota: Por muito tempo utilizei uma outra biblioteca chamada Moq que faz a mesma coisa porém com sintaxe diferente. Ultimamente tenho dado preferência pelo NSubstitute exatamente por parecer legível.

Um breve exemplo do NSubstitute:

interface IEmailPublisher {
    string Driver { get; }
    Task SendTo(string r, CancellationToken ct = default);
}

var publisher = Substitute.For<IEmailPublisher>();
publisher.SendTo("temp").Returns(Task.CompletedTask);
publisher.Driver.Returns("MyDriver123");
//publisher.SendTo().ThrowsForAnyArgs<NotImplementedException>();   // Caso queira simular um erro

var result = publisher.Driver;
Assert.Equal("MyDriver123", result);    // -> True
Enter fullscreen mode Exit fullscreen mode

FluentAssertions - Sintaxe fluent/builder para validar os cenários

FluentAssertions é uma biblioteca que não vai te ajudar a economizar tempo enquanto prepara seus testes mas vai tornar seus testes mais legíveis a longo prazo!

FluentAssertions permite que você descreva o que é esperado do seu teste usando uma linguagem mais próxima à natural.

Por exemplo, se eu quisesse validar em um teste que: "O resultado não é nulo, é do tipo X e é equivalente ao objeto Y"

// Usando fluent assertions
resultado.Should()
    .NotBeNull().And
    .BeOfType<X>().And
    .BeEquivalentTo(Y);

// Usando Asserts
Assert.NotNull(resultado);
Assert.IsType<X>(resultado);
Assert.IsEqual(resultado, Y);
Enter fullscreen mode Exit fullscreen mode

GitHub logo fluentassertions / fluentassertions

A very extensive set of extension methods that allow you to more naturally specify the expected outcome of a TDD or BDD-style unit tests. Targets .NET Framework 4.7, as well as .NET Core 2.1, .NET Core 3.0, .NET 6, .NET Standard 2.0 and 2.1. Supports the unit test frameworks MSTest2, NUnit3, XUnit2, MSpec, and NSpec3.

EntityFrameworkCore.InMemory - Versão do EFCore para testes unitários

Normalmente utilizaríamos um Mock/Stub para simular o acesso ao banco em nossos testes unitários, porém os objetos e tipos do EntityFrameworkCore, apesar de "mockáveis", requerem um setup bem extenso.

Com isso em mente o próprio EntityFrameworkCore já criou uma biblioteca exatamente para que não precisemos fazer todo esse setup. O provider InMemory é a maneira de simular um acesso ao banco nos nossos testes unitários.

Existe uma discussão bem extensa sobre se usar isso não configuraria seu teste como um teste de integração e se a maneira correta não seria voltar ao velho padrão de Unit Of Work + Repositories (que o próprio EFCore já implementa por si só 🤷‍♂️).

Meu take pessoal nesse assunto é que devemos ser pragmáticos. A biblioteca está ai, pronta. Salvo que eu precise muito por que vou criar mais uma camada? Abstrações são boas (vida longa a interfaces e classe abstratas!) mas da mesma maneira que você pode pecar por usar só implementação concretas você também pode pecar por criar abstrações para tudo!

Abstrair só por abstrair é overengineering.

GitHub logo dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.

Mãos à obra: Criando nosso projeto de testes

Vamos à pratica! Fora do Visual Studio vamos criar uma nova pasta na raiz do projeto, chamada tests/. A ideia aqui é que todos os nossos projetos de testes (Unit, Integration e Feature) ficarão nesta pasta para não bagunçar a hierarquia das outras bibliotecas do projeto.

Criando o Projeto e Instalando dependências

Pelo Visual Studio:

  1. Clique direito na solução e escolha Add new project
  2. Pesquise nos templates por xUnit e localize um chamado xUnit Test Project (.NET Core)
  3. Escolha um nome para seu projeto e o coloque para ser salvo na pasta tests/
  4. Opcional, caso o projeto a ser testado seja .NET 5:
    1. Clique com o direito no projeto e escolha Properties
    2. Mude o campo Target framework para .NET 5.0
    3. Salve as alterações e recarregue o projeto

Com o projeto criado abra o NuGet Manager para o projeto de testes e adicione os seguintes pacotes:

  1. AutoFixture
  2. AutoFixture.AutoNSubstitute
  3. AutoFixture.Xunit2
  4. FluentAssertions
  5. Microsoft.EntityFrameworkCore.InMemory
  6. NSubstitute

Lembre-se de adicionar como referência ao projeto de testes o projeto que será testado.

Configurando nossas Ferramentas

Com tudo instalado podemos passar para a parte de preparação. De certa maneira este passo é opcional mas se quisermos escrever nossos testes da maneira mais prática possível este passo será o grande diferencial de produtividade.

O AutoFixture.Xunit2 nos trás um atributo [AutoData] que pode ser utilizado para que os parâmetros de um teste sejam criados automaticamente pelo AutoFixture.

A parte mais legal é que herdar este atributo e customizar como o AutoFixture cria as coisas. Isso significa que podemos colocar na "pipeline" do AutoFixture coisas como o EntityFrameworkCore.InMemory, NSubstitute e customizar como alguns atributos nossos seriam criados.

Para fazer isso tudo que precisamos é criar uma nova classe CreateDataAttribute, herdar a classe AutoDataAttribute e criar um construtor que chama o base(Func<IFixture> factory).

Minha sugestão é que você crie um método estático que retorna uma IFixture e nesse método faça toda a configuração!

Por exemplo, esta é a minha versão deste atributo para o Cell CMS:

using System;

using AutoFixture;
using AutoFixture.AutoNSubstitute;
using AutoFixture.Xunit2;

using AutoMapper;

using CellCms.Api;

using Microsoft.EntityFrameworkCore;

namespace CellCms.Tests.Unit.Utils
{
    /// <summary>
    /// Atributo para configurar automaticamente os dados de um test case.
    /// </summary>
    public class CreateDataAttribute : AutoDataAttribute
    {
        public CreateDataAttribute() : base(SetupCellCmsFixture)
        {

        }

        /// <summary>
        /// Configura uma fixture com todos os objetos necessários para
        /// testar o CellCMS.
        /// </summary>
        /// <returns></returns>
        private static IFixture SetupCellCmsFixture()
        {
            var fix = new Fixture();
            fix.Customize(new AutoNSubstituteCustomization());
            SetupRecursionBehaviors(fix);
            SetupCellContext(fix);
            SetupAutoMapper(fix);
            return fix;
        }

        /// <summary>
        /// Configura e Injeta uma instância do AutoMapper.
        /// </summary>
        /// <param name="fix"></param>
        private static void SetupAutoMapper(Fixture fix)
        {
            var autoMapperConfig = new MapperConfiguration(cfg =>
            {
                cfg.AddMaps(typeof(Startup));
            });
            fix.Inject(autoMapperConfig);
            fix.Inject(autoMapperConfig.CreateMapper());
        }

        /// <summary>
        /// Configura e Injeta uma instância do CellContext.
        /// </summary>
        /// <param name="fix"></param>
        private static void SetupCellContext(Fixture fix)
        {
            var dbOptions = new DbContextOptionsBuilder()
                            .UseInMemoryDatabase(Guid.NewGuid().ToString());
            fix.Inject(new CellContext(dbOptions.Options));
        }

        /// <summary>
        /// Configura o comportamento da Fixture durante
        /// chamadas recursivas.
        /// </summary>
        /// <param name="fix"></param>
        private static void SetupRecursionBehaviors(Fixture fix)
        {
            fix.Behaviors.Remove(new ThrowingRecursionBehavior());
            fix.Behaviors.Add(new OmitOnRecursionBehavior());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

É bastante verboso na primeira olhada mas lembre-se que usando esse atributo você terá muito mais produtividade para escrever seus testes!

Escrevendo nossos Testes

Finalmente podemos começar a escrever nossos testes!

Tudo que você precisa fazer agora é criar uma nova classe, criar um método para o seu teste e adicionar os atributos.

Por exemplo um dos testes do Cell CMS:

[Theory]        // Indica ao xUnit que este teste tem parâmetros
[CreateData]    // Indica que os parâmetros serão criados através do atributo que criamos na seção anterior
// O [Frozen] indica que o AutoFixture deve retornar sempre esta mesma instância para todos que precisarem dentro deste método! É como se fosse um Singleton
public async Task Handle_ExistingContext_ReturnsList([Frozen] CellContext context,
    IEnumerable<Feed> feeds,
    ListAllFeedsHandler subject)
{
    // Note que deixamos que o próprio "objeto a ser testado" seja criado pelo AutoFixture, dessa maneira o mesmo context que ele passou aqui para este método será passado para o subject!

    // Arrange
    // Aqui estou garantindo que os dados criados pelo AutoFixture estão salvos no Context do EntityFramework
    context.AddRange(feeds);
    await context.SaveChangesAsync();

    // Act
    var result = await subject.Handle(new ListAllFeeds(), default);

    // Assert
    result
        .Should()
        .NotBeNull().And
        .HaveSameCount(context.Feeds);
}
Enter fullscreen mode Exit fullscreen mode

Executando os Testes

Para executar os testes por linha de comando use: dotnet test na pasta da solução.

Executando testes pelo terminal

Para executar pelo Visual Studio o principal será a janela Test Explorer (atalho: Ctrl+E, T). Porém temos alguns outros atalhos importantes e práticos:

  1. Ctrl+R, A: Executar todos os testes;
  2. Ctrl+R, Ctrl+A: Executar todos os testes com Debug;
  3. Ctrl+R, T: Executar o teste selecionado (no caso o teste em que seu cursor estiver);
  4. Ctrl+R, Ctrl+T: Executar o teste selecionado com Debug;

Test Explorer do Visual Studio

Finalizando

O post de hoje vai ficando por aqui! Espero que após ler esse post fique evidente que podemos escrever testes unitários de maneira rápida e prática graças a diversas bibliotecas Open Source!

Alguns assuntos que ainda quero abordar, para os próximos posts:

  1. Analyzers
  2. Refactoring
  3. Domain Driven Design
  4. Clean Architecture
  5. Configuração de um ambiente de desenvolvimento

Fiquem ligados para o próximo post, obrigado por lerem e até a próxima!

Top comments (0)