Por vezes, testes unitários que deveriam ser simples acabam consumindo mais tempo do que o esperado, o que faz com que muitas vezes sejam deixados para trás. Você sabia que estes simples testes podem ser um indicativo de qualidade de código? Através dessa talk vamos discutir as principais falhas de design que são facilmente identificadas através de testes unitários.
Primeiramente, por que pensar em Design de Código? Gostaria de iniciar esta palestra com uma história:
Prazos de entrega de software são curtos. Essa é uma realidade vivida por muitos, senão por todos nós. Então a primeira coisa que aprendemos no mundo da programação profissional é entregar código que funciona dentro do prazo.
Esse é um belo exemplo de código que funciona. E está em produção! Vencemos! Yay!
Esse código é o típico gerador de bugs. Ele é o Código Mogwai: aos olhos do cliente parece fofo, corresponde às especificações, mas basta uma iteração do usuário, geralmente próximo às 18:00hrs, para que ele se multiplique em diversos Gremlins. E então todo o tempo que foi economizado para que a entrega fosse realizada, é disperdiçado na caça aos bugs.
Aquele código é reflexo da imaturidade técnica da equipe (a qual fiz parte na época, e não tenho nenhuma vergonha de assumir. Quem nunca foi iniciante antes?). E apesar disto ser um panorâma comum quando se trata em desenvolvimento de software, isto não é aceitável. [enfase]Código de entregável que gere bugs não é aceitável.[enfase] Então, o que podemos fazer para evitar isso?
[Testes são o primeiro caminho para a salvação da equipe]
A função dos testes unitários é verificar o funcionamento de um fluxo garantindo que, a partir de uma entrada, uma saída esperada seja reproduzida. Testes unitários são a maneira mais simples, rápida e eficiente de validar uma funcionalidade. Existem algumas libs Python para se trabalhar com testes unitários, como o unittest que estou usando no exemplo, e o py.test que tem ganhado bastante espaço pela simplicidade.
Então através de testes, se quero garantir a consistência do funcionamento de uma nova feature, faço testes unitários que cubram todos os fluxos possíveis. Antes de colocar uma nova feature no ar, executo todos os testes do projeto para saber se alguma parte foi afetada pela nova funcionalidade. Basta rodar todos os testes e verificar se algum teste quebrou. Tranquilidade.
E esse é a classe de teste do código anterior
E esse...
E esse...
E continua
A medida que novos bugs foram sendo encontrados, em fluxos imprevistos no momento da criação dos testes, novos testes foram criados para a garantia desses fluxos. A cada alteração na especificação da feature, testes e mais testes eram atualizados, para garantir a mudança de comportamento dos fluxos antes mapeados. Manter essa classe de teste tornou-se tão dispendioso até o momento em que a entrega teve que ser priorisada em detrimento da qualidade. E na hora que a entrega canta, quem é o primeiro a rodar? Os testes, é claro.
O que acontece a partir de então é um círculo vicioso: você faz testes para cobrir fluxo de um código Gremlim, você deixa de fazer os testes porque a manutenção dos mesmos tornou-se inviável, e então você cria brechas para que novos Gremlins surjam. A verdade é que este teste estava esfregando na nossa cara todo o bad smell do nosso código, que por nossa limitação técnica éramos incapazes de ver. Testes devem ser de fácil manutenção, legíveis e simples. E se os seus testes não são assim, existe algo de errado com o seu código.
Estamos caminhando para os 60 anos de existência da Engenharia de Software. Nesses zilhões de projetos de software planejados e executados desde então, muitos problemas puderam ser identificados e mapeados. Em relação ao desenvolvimento de software em si, na década de 80 começou-se a documentar quais problemas geralmente acometiam projetos de software e quais as possíveis soluções para estes problemas. Criou-se então modelos de resoluções, baseados em melhores práticas, para que qualquer programador pudesse resolver problemas comuns ao projetar um sistema. Ficaram conhecidos como Padrões de Projeto.
Padrões de Projeto é um tema muito extenso, o qual não vou me aprofundar aqui, mas gostaria de citá-los somente para justificar o bed smell do nosso código anterior. Se você tem dificuldades em dar manutenção, criar novas features, desacoplar apps do seu projeto, testar, sem dúvidas você está violando [com sorte] um ou mais padrões de projeto. Eles são assunto de estudo obrigatório para qualquer programador, mas podem ser extremamente difíceis de identificar e aplicar quando não se tem muita experiência.
Deixo então aqui algumas recomendações de estudo, antes de seguir com a apresentação.
[Padrões de Projeto - Soluções Reutilizaveis de Software Orientado a Objetos]
[Utilizando UML e Padrões]
[Clean Code]
[Python Patterns]
Como eu disse antes, padrões de projeto exigem algum tempo de estudo e de experiência antes de que você possa fazer bom uso deles. Já testes, nem tanto assim. Além de identificadores de bad smells, eles podem te ajudar a aplicar e identificar alguns padrões de projetos que possam estar sendo violados no seu código. Para isso, podemos adotar algumas pequenas regras:
procure testar unitariamente cada etapa do seu código.
Cada etapa do fluxo pode conter subfluxos que podem ficar obscurecidos quando você trata tudo como uma só função.
Já que você está tratando-os unitariamente, verifique no seu código se é viável criar uma função específica para cada etapa da função anterior.
Se a sua função pode gerar um fluxo de erro, teste-o unitariamente também. Um programador novato ficará feliz de saber que você criou um teste que documenta este cenário.
Não seja econômico, testes também ajudam na documentação do sistema.
Se a sua função faz chamada a outra função, api, etc, não é responsabilidade do teste dela garantir o funcionamento de fluxos externos. Imagine ter que mecher em 299 testes por causa da mudança de comportamento em uma função? A menos que a alteração seja de mudança de referência, somente os testes relacionados a função/fluxo devem quebrar. Para isso use e abuse de mocks sempre que for necessário.
[Uma verdade sobre funções-testes, elas devem ser pequenas. Outra verdade, elas devem ser menores ainda.] A menos que você esteja testando a construção de uma classe, onde você pode ter uma asserção de atributo-valor por linha em favor da legibilidade, você deve ser capaz de testar o fluxo esperado em 2~3 linhas. Se não, pare e verifique se existem subfluxos encapsulados, no seu teste e volte ao passo 1 e 2.
Depois de esmiuçar os testes em sua TestCase, verifique se os testes abordam cenários muito distintos. Será que a classe testada não está com responsabilidades demais?
Até então tenho falado em testes unitários no contexto de testes de regressão: estamos criando testes para funcionalidades que foram previamente codificadas. No TDD, temos a abordagem inversa: os testes são criados antes da funcionalidade ser codificada.
Quando abordamos o TDD com testes unitários que aplicam as regras anteriormente citadas, muitas falhas de design podem ser evitadas.