
Posted on • Edited 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á ofeature/create-tests
.
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 | Ciclo estável, recomendado para produção | |
Develop | 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 oSDK 5.0.101 do Dotnet.
Uma vez configurado basta executardotnet build .\cell-cms.sln
na raiz do repositório.
Testando
Executedotnet 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 oAzure Active Directory comoprovider de identidade, então você terá de configurar sua instância doAAD conforme explicadoneste 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:
- Migramos para .NET 5 (originalmente estávamos no .NET Core 3.1)
- Criamos uma pipeline no GitHub Actions para compilar o projeto continuamente
- 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 reconfigurarmais 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 quetestes 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 poiseles não fazem tudo! Como o próprio nome já diz oescopo 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 canalAspiraPlay 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 é opró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 é onome do seu teste. É no nome em que vamos dar aquela resumida em:
- O que estamos testando
- Sob quais condições/estado estamos testando
- O que esperamos
Em alguns frameworks onome 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 é identificaras entradas para a rotina que você está testando. Note que isso poderá mudarbastante 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:
- Testar uma classe que cria um JSON -> Input seriam objetos diferentes
- Testar uma classe que utiliza outra para salvar algo -> Input seria a classe que será utilizada viamock e o dado que seria salvo
- 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 é ummock, falaremos disso depois!
O que está sendo testado (Subject)
O terceiro elemento (queapesar 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:
- Uma dependência falhando
- Uma input inválida
- Uma referência nula
- 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ênciapessoal é 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:
- Retorne null
- Retorne um objeto válido
- Lance uma
Exception
- Dê timeout após ... milissegundos
Juntando tudo: AAA
Como lembrar de tudo isso? Uma das práticas mais comuns é sempre lembras dos 3 As:
- Arrange: Escolha as inputs, o que será testado e monte o cenário
- Act: Realize a ação que você quer que seja testada
- 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:
// Arrangevarsubject=newMinhaClasseSendoTestada(null);// Actvarresult=subject.FazAlgumaCoisa();// AssertAssert.IsNull(result);
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 porcomportamento.
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 porestado, maior parte das vezes, porém também poderia testar porcomportamento 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:
- nUnit -> Mais clássico mas amplamente utilizado, com diversos plugins e runners
- xUnit -> Mais moderno e próximo à ideia de TDD, também é amplamente utilizado
- 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ãoo 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 issoo 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:
Métodos()
indicando as rotinas de teste[Atributos]
indicando que um método é um teste com ou sem parâmetros[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 masnã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 umnew ObjetoInput(param1, param2, ...);
. A bibliotecaAutoFixture resolve exatamente isso!
De maneira sucinta:
AutoFixture gera a massa dedados para testes automaticamente para você. Permitindo que vocêgaste menos tempo na fase de Arrange de seu teste.
E melhor ainda: Elefunciona, 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!
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 bibliotecaNSubstitute é mais uma facilitadora para a nossa fase deArrange dos testes. Lembra sobre mocks e stubs? Esse carinha aqui é quem vai criar os mocks pra gente.
De maneira curta:
NSubstitutecria, 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.
nsubstitute / NSubstitute
A friendly substitute for .NET mocking libraries.
Nota: Por muito tempo utilizei uma outra biblioteca chamadaMoq 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:
interfaceIEmailPublisher{stringDriver{get;}TaskSendTo(stringr,CancellationTokenct=default);}varpublisher=Substitute.For<IEmailPublisher>();publisher.SendTo("temp").Returns(Task.CompletedTask);publisher.Driver.Returns("MyDriver123");//publisher.SendTo().ThrowsForAnyArgs<NotImplementedException>(); // Caso queira simular um errovarresult=publisher.Driver;Assert.Equal("MyDriver123",result);// -> True
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 masvai tornar seus testes mais legíveis a longo prazo!
FluentAssertions permite que vocêdescreva o que é esperado do seu testeusando 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 assertionsresultado.Should().NotBeNull().And.BeOfType<X>().And.BeEquivalentTo(Y);// Usando AssertsAssert.NotNull(resultado);Assert.IsType<X>(resultado);Assert.IsEqual(resultado,Y);
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 providerInMemory
é a maneira de simular um acesso ao banco nos nossos testes unitários.
Existe uma discussãobem 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 quevocê pode pecar por usar só implementação concretas você também pode pecar por criar abstrações para tudo!
Abstrair só por abstrair é overengineering.
Mãos à obra: Criando nosso projeto de testes
Vamos à pratica! Fora do Visual Studio vamos criar uma nova pasta na raiz do projeto, chamadatests/
. 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:
- Clique direito na solução e escolha
Add new project
- Pesquise nos templates por
xUnit
e localize um chamadoxUnit Test Project (.NET Core)
- Escolha um nome para seu projeto e o coloque para ser salvo na pasta
tests/
- Opcional, caso o projeto a ser testado seja .NET 5:
- Clique com o direito no projeto e escolha
Properties
- Mude o campo
Target framework
para.NET 5.0
- Salve as alterações e recarregue o projeto
- Clique com o direito no projeto e escolha
Com o projeto criado abra oNuGet Manager
para o projeto de testes e adicione os seguintes pacotes:
AutoFixture
AutoFixture.AutoNSubstitute
AutoFixture.Xunit2
FluentAssertions
Microsoft.EntityFrameworkCore.InMemory
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.
OAutoFixture.Xunit2
nos trás um atributo[AutoData]
que pode ser utilizado para que os parâmetros de um teste sejam criados automaticamente peloAutoFixture
.
A parte mais legal é queherdar este atributo e customizar como o AutoFixture cria as coisas. Isso significa que podemos colocar na "pipeline" do AutoFixture coisas como oEntityFrameworkCore.InMemory
,NSubstitute
e customizar como alguns atributos nossos seriam criados.
Para fazer isso tudo que precisamos é criar uma nova classeCreateDataAttribute
, herdar a classeAutoDataAttribute
e criar um construtor que chama obase(Func<IFixture> factory)
.
Minha sugestão é que você crie um método estático que retorna umaIFixture
e nesse método faça toda a configuração!
Por exemplo, esta é a minha versão deste atributo para o Cell CMS:
usingSystem;usingAutoFixture;usingAutoFixture.AutoNSubstitute;usingAutoFixture.Xunit2;usingAutoMapper;usingCellCms.Api;usingMicrosoft.EntityFrameworkCore;namespaceCellCms.Tests.Unit.Utils{/// <summary>/// Atributo para configurar automaticamente os dados de um test case./// </summary>publicclassCreateDataAttribute:AutoDataAttribute{publicCreateDataAttribute():base(SetupCellCmsFixture){}/// <summary>/// Configura uma fixture com todos os objetos necessários para/// testar o CellCMS./// </summary>/// <returns></returns>privatestaticIFixtureSetupCellCmsFixture(){varfix=newFixture();fix.Customize(newAutoNSubstituteCustomization());SetupRecursionBehaviors(fix);SetupCellContext(fix);SetupAutoMapper(fix);returnfix;}/// <summary>/// Configura e Injeta uma instância do AutoMapper./// </summary>/// <param name="fix"></param>privatestaticvoidSetupAutoMapper(Fixturefix){varautoMapperConfig=newMapperConfiguration(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>privatestaticvoidSetupCellContext(Fixturefix){vardbOptions=newDbContextOptionsBuilder().UseInMemoryDatabase(Guid.NewGuid().ToString());fix.Inject(newCellContext(dbOptions.Options));}/// <summary>/// Configura o comportamento da Fixture durante/// chamadas recursivas./// </summary>/// <param name="fix"></param>privatestaticvoidSetupRecursionBehaviors(Fixturefix){fix.Behaviors.Remove(newThrowingRecursionBehavior());fix.Behaviors.Add(newOmitOnRecursionBehavior());}}}
É bastante verboso na primeira olhada mas lembre-se queusando 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 eadicionar 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 SingletonpublicasyncTaskHandle_ExistingContext_ReturnsList([Frozen]CellContextcontext,IEnumerable<Feed>feeds,ListAllFeedsHandlersubject){// 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 EntityFrameworkcontext.AddRange(feeds);awaitcontext.SaveChangesAsync();// Actvarresult=awaitsubject.Handle(newListAllFeeds(),default);// Assertresult.Should().NotBeNull().And.HaveSameCount(context.Feeds);}
Executando os Testes
Para executar os testes por linha de comando use:dotnet test
na pasta da solução.
Para executar pelo Visual Studio o principal será a janelaTest Explorer
(atalho:Ctrl+E, T
). Porém temos alguns outros atalhos importantes e práticos:
Ctrl+R, A
: Executar todos os testes;Ctrl+R, Ctrl+A
: Executar todos os testes com Debug;Ctrl+R, T
: Executar o teste selecionado (no caso o teste em que seu cursor estiver);Ctrl+R, Ctrl+T
: Executar o teste selecionado com Debug;
Finalizando
O post de hoje vai ficando por aqui! Espero que após ler esse post fique evidenteque podemos escrever testes unitários de maneira rápida e prática graças a diversas bibliotecasOpen Source!
Alguns assuntos que ainda quero abordar, para os próximos posts:
- Analyzers
- Refactoring
- Domain Driven Design
- Clean Architecture
- 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)
For further actions, you may consider blocking this person and/orreporting abuse