Demais posts desta série:
Artigo do tipo Tutorial
Recursos especiais neste artigo:
Artigo no estilo Curso Online
Testes Unitários com NUnit
Na última edição, vimos os principais conceitos sobre testes unitários, assim como alguns exemplos de testes utilizando o framework NUnit. Neste artigo, iremos finalizar nossa série abordando outros tópicos importantes como a organização dos testes, uso de mock objects, uso do Selenium para automatização de testes de interface e cobertura de testes.


Em que situação o tema é útil
Este tema é útil na garantia da qualidade dos projetos de software. Com testes unitários bem definidos e implementados, é possível identificar e corrigir erros em tempo de desenvolvimento e além de aumentar a confiança da equipe e do cliente no projeto, consegue-se ainda um aumento na produtividade da equipe, além de identificar quais os pontos de seu sistema que possuem maior risco de incidência de exceções, através da análise da cobertura dos testes.

Na última edição, criamos alguns cenários para aplicação de testes unitários. Na ocasião, criamos os testes sem a preocupação com a organização dos mesmos, porém, este é um aspecto muito importante no qual devemos nos ater em nossos projetos. No nosso caso, uma simples classe gerou cerca de oito testes unitários, imagine em um projeto real quantos testes teríamos? Além disso, devido ao fato dos testes unitários serem códigos de nosso projeto, devemos ter com eles a mesma preocupação de limpeza e clareza que temos com o nosso código, deixando-os fáceis de entender e manter. Para isso, é preciso estar atento à criação de rotinas reutilizáveis nos testes, assim como de hierarquia dos mesmos.

Um ponto importante na organização dos testes é com a relação entre eles e as nossas classes de sistema. Existem algumas estratégias possíveis para isso, sendo dois os principais destes:

· Uma classe de testes para cada classe testada – Com esta abordagem, teremos uma classe de testes para cada classe passível de testes no sistema. Normalmente é usado um padrão de nomenclatura onde a classe de testes possui o mesmo nome da classe testada seguido do sufixo Tests. Por exemplo: Uma classe PedidoRepository teria uma classe de testes chamada PedidoRepositoryTests. Vale ressaltar a pluralidade do nome, ou seja, chamamos a classe de “Tests”, pois ela irá possuir diversos testes referentes a PedidoRepository. Desta forma, qualquer programador que possa dar manutenção nesta classe saberá que ela contém todos os testes referentes a PedidoRepository. Assim, fica fácil identificar as classes de testes ao navegar pelas classes da solution. Um possível efeito colateral desta abordagem é o fato de que, dependendo da classe testada e dos possíveis fluxos da mesma, a classe de testes pode acabar ficando muito grande, com muitos métodos, dificultando a leitura e manutenção da mesma.

· Uma classe de testes por funcionalidade – Uma alternativa à abordagem acima é criar uma classe de testes para uma funcionalidade específica do sistema. Desta forma, seria possível agrupar todos os testes referentes a uma determinada funcionalidade em uma única classe de testes. Por exemplo, poderíamos ter a classe Usuario em nosso sistema e a classe UsuarioTests. Porém, para evitar poluir demais a classe UsuarioTests, poderíamos criar a classe LoginManagerTests que teria os testes relacionados ao processo de gerenciamento de logins como por exemplo ResetarSenhaTest e EnviarSenhaPorEmailTest.

Outro item importante a destacarmos na organização dos testes é com relação à nomenclatura dos mesmos. É importante criarmos testes cujo nome deixe bem claro o que está sendo testado e qual o cenário que está sendo validado. Por exemplo, se tivermos uma classe OperacaoMatematica com a função Somar, poderíamos ter no projeto de testes a classe OperacaoMatematicaTests, com os métodos de teste: “testSomarValoresPositivos” e “testSomarValoresNegativos”. Logo, quem ler o nome do teste saberá qual o cenário que está sendo validado no mesmo, tornando a manutenção mais simples e fácil de ser realizada. Além disso, quando o teste for executado e falhar, basta olhar para o nome do método para saber qual o cenário que está quebrando o teste.

Na medida em que for criando testes unitários nos seus projetos, será natural que sinta a necessidade de refatoração dos mesmos, até porque não deixam de ser código de seu projeto. Durante este processo, poderá identificar abstrações para criar herança em seus testes, ou simplesmente criar métodos e classes utilitárias para facilitar a vida de quem estiver escrevendo os testes.

Outro ponto importante é a separação entre testes unitários e testes de integração. Uma das principais motivações para isso é que os testes de integração levam muito mais tempo para serem executados. Esta separação pode ser em nível de projetos de testes ou simplesmente com a criação de pastas e namespaces. A vantagem de separar os mesmos em projetos diferentes é que fica mais fácil controlar a execução de cada um deles em um cenário automatizado.

Além disso, testes de integração podem envolver configuração de ambiente, sendo que este ambiente representa mais uma possibilidade de falha nos testes. Sendo assim, quando um desenvolvedor executa os testes e obtém uma falha, pode ser que:

· Exista um bug no código que está sendo testado;

· Exista um problema na forma como o teste foi escrito;

· O teste requer alguma configuração para executar;

Nas duas primeiras, o desenvolvedor tem certa motivação para investigar o problema, porém a terceira poderá fazer com que o desenvolvedor ignore o erro, visto que não representa uma falha de codificação. Desta forma, quando colocamos testes unitários e testes de integração no mesmo pacote, corremos um risco maior de este cenário ocorrer, visto que a probabilidade de problemas de ambiente e configuração impactarem no resultado dos testes é muito maior no cenário de testes de integração do que no de testes unitários.

Outra importante característica na organização dos testes é com relação ao controle da evolução dos mesmos. É fundamental que os códigos de teste estejam sob nosso controle de versões, ou seja, é importante tratarmos nosso código de teste como tratamos nosso código de produção, até porque ele passa a ser um ativo do projeto. Além disso, a versão o código de teste precisa sempre acompanhar a versão do código testado, uma vez que o mesmo evolui naturalmente ao longo do tempo e pode demandar refatorações no seu respectivo teste.

Bons testes podem ser classificados de acordo com algumas características. As principais características que devemos nos ater para criar bons testes são:

· Confiáveis – Os desenvolvedores precisam confiar no resultado dos testes. Se o projeto de testes é executado e reporta que está tudo bem, o desenvolvedor precisa confiar na consistência dos testes.

· Fáceis de manter – Criar códigos difíceis de serem mantidos pode acarretar em problemas sérios de performance e prazo do projeto. Uma simples alteração nestes casos pode se tornar uma grande dor de cabeça. Desta forma, precisamos escrever testes que possam ser facilmente mantidos pela equipe de desenvolvimento, caso contrário, o que deveria ser uma solução, passa a ser um novo problema para a equipe.

· Legíveis – A nomenclatura dos testes precisa ser a mais clara possível e através de seu nome temos que saber exatamente o que está sendo validado naquele teste.

Existem algumas formas de validar as características acima. A primeira delas, com relação à confiabilidade dos testes, é a reação do desenvolvedor após a execução do teste. Se um teste é executado com sucesso o desenvolvedor não pode pensar “Vou realizar o debug apenas para garantir que está tudo OK”, caso fizer, temos um forte indício de que o teste não está confiável. Da mesma maneira que, se o teste falha o desenvolvedor não pode pensar “Pode ser que tenhamos um problema”, porque ele sabe que existe um erro no código, pois o teste precisa ser confiável.

Outro item importante a destacarmos é com relação ao uso de lógica nos testes. Precisamos sempre evitar criar lógicas em nossos testes, pois aumenta consideravelmente as chances de termos bugs nos testes, visto que os mesmos se tornam mais complexos e acabam tratando mais de um cenário. Estruturas como ifs, elses, switchs e loops são indícios que algo está errado no seu teste.

Logo, outro erro comum é com relação aos assertions dos testes. É importante que um teste tenha apenas uma assertion, fazendo assim com que o teste seja mais simples de ser lido e mantido e garantindo que tenhamos apenas uma forma para validar a execução de uma determinada rotina.

Anti-Patterns em testes unitários

Temos diversos patterns que nos orientam sobre o que devemos fazer, porém, existem os anti-patterns, que nos mostram justamente o contrário: O que não devemos fazer. No caso dos testes unitários, temos alguns anti-patterns importantes de serem mencionados.

· Restrição de ordem dos testes – Este anti-pattern está presente quando escrevemos um determinado teste unitário, assumindo como verdade que um determinado teste fora executado anteriormente e que por isso determinada característica do ambiente já foi pré-configurada. Acontece que este teste ficará dependente do outro, não podendo ser executado isoladamente, pois poderia apresentar alguma falha. Um teste unitário precisa ser independente dos outros. Se o mesmo possuir alguma dependência, esta deverá ser inicializada no próprio teste ou no Setup do mesmo.

· Chamadas entre testes – Este anti-pattern está presente quando um teste unitário executa outro teste unitário fazendo com que exista uma dependência entre os mesmos. Algumas vezes, o desenvolvedor comete este erro na tentativa de evitar repetição de código. Neste caso, deve-se abstrair determinado trecho de código, a ser reutilizado, em um método utilitário, que será executado pelos dois métodos de teste.

· Corrupção de estado compartilhado – Este anti-pattern está presente quando não temos o controle sobre determinado meio de manutenção de estado como banco de dados, memória ou arquivos físicos, fazendo com que um determinado recurso fique “sujo” após a execução do teste, impactando na execução de outros testes ou até mesmo na re-execução do mesmo teste. Uma boa forma de evitar este problema é no início do teste garantir que o estado dos recursos que o teste irá manipular está da forma esperada. Isso pode ser feito através de helpers executados diretamente no início do teste ou no método Setup da classe de teste.

Trabalhando com Mock Objects

Algumas rotinas de nossos sistemas dependem da execução de funções de componentes externos, sejam funções de outras classes, ou até mesmo interação com sistema de arquivos, servidor de email ou web services. Nestes casos, precisamos isolar a execução de nossa rotina para que estes componentes externos não interfiram no resultado do nosso teste unitário. Para isso, podemos usar o conceito de mock objects, onde definimos objetos com falsas implementações de determinados métodos para forçar um input esperado e consistente para o método a ser testado.

...
Quer ler esse conteúdo completo? Tenha acesso completo