Pular para o conteúdo
setembro 26, 2010 / cassiomarques

Sobre as razões para testar e porque você pode estar fazendo tudo errado

Há bastante tempo tenho uma certa paranóia com testes automatizados. É uma coisa que foi acontecendo gradativamente. Hoje posso dizer que não consigo mais desenvolver sem testes. Quem acompanha este blog sabe que a nos últimos meses a grande maioria dos posts foram sobre este assunto. É algo que eu gosto de praticar e debater.
Mas conforme o tempo foi passando certos conceitos foram amadurecendo na minha cabeça. Conceitos que anteriormente eu via como “sagrados” passaram a perder parte da importância, assim como aspectos que ainda eram um pouco obscuros começaram a se mostrar mais claros. Como resultado desse processo, eu passei a ver os testes sob uma perspectiva diferente. Talvez diferente do que parte da literatura existente sobre o tema diz, talvez uma visão só minha.

Porque testar?

Todas as idéias sobre as quais tenho pensado giram em torno de uma questão que superficialmente parece simples, mas que não é: Por que testar? As respostas podem variar um pouco, mas boa parte delas vai girar em torno de conceitos já extensivamente debatidos em palestras, livros e blogs. Sobre os motivos já existentes, vou listar as minhas razões para testar.

Segurança durante o desenvolvimento e evolução das aplicações

É importante testar para ter segurança durante o desenvolvimento, tendo garantias um pouco melhores (mas não certeza absoluta, isso é impossível) de que novas alterações no código não vão quebrar o que já está funcionando. Isso é notoriamente importante em equipes grandes, onde os desenvolvedores podem não conhecer a fundo toda a base de código. Por outro lado, mesmo se o projeto seja desenvolvido por um único programador, é dificil prever onde efeitos colaterais de uma alteração vão aparecer.
Outra situação semelhante onde possuir uma boa suíte de testes se mostra uma grande vantagem é no momento de realizar atualizações nas versões das bibliotecas e frameworks utilizados. Como em diversos upgrades não há garantia de compatibilidade com versões anteriores, os testes ajudam a indicar pontos que necessitam de ajustes.

Nos colocar no lugar de quem vai usar o código que estamos escrevendo

Muitas vezes sabemos o que precisamos fazer, mas não temos certeza absoluta de como chegar lá. Um caso clássico é quando precisamos criar alguns componentes no código e disponibilizar isso através de alguma interface pública. Da mesma forma com que gostamos de usar bibliotecas e componentes que expõe interfaces claras, simples e eficientes, assim devem ser as interfaces que criamos. Acontece que é dificil imaginar qual seria a melhor forma de chamar um método ou quais parâmetros esses métodos deveriam receber. Qual é a melhor forma de instanciar objetos? Será que essa parte deve aceitar um bloco? E aqui, deve ir um Hash ou um Array? Não dá para saber quais seriam as melhores opções se não nos colocarmos no lugar de quem vai usar o código. Esse alguém pode vir a ser você mesmo, seus companheiros de equipes ou no caso de OSS, um desconhecido. E é ai que o TDD mostra todo seu poder: Escrevendo testes antes, sem código algum para a implementação dos seus componentes, você pode fazer diversos experimentos sobre como devem ser as APIs. Quando a coisa parecer natural de usar (pois é exatamente isso que você estará fazendo ao escrever os testes), implemente. Então, aos poucos, evolua seu design e refatore tudo.

Segurança para refatorar

Para refatorar o código é imprescindível termos uma boa cobertura de testes automatizados. Se não tivermos testes, o que prevalece é sempre a frase “Pra que mexer no que está funcionando?”. Porque mexer? Porque nem tudo que funciona presta e por que devemos manter a casa em ordem. Precisamos facilitar a evolução do código. Mas para isso precisamos de coragem. Neste caso nossa coragem é a nossa suíte de testes.

Criação de documentação executável

Para mim muitas vezes ajuda muito olhar os testes de um projeto para entender as regras de negócio. Não estou falando de entender como a coisa foi implementada e porque funciona, mas sim de entender o que a aplicação faz. Isso faz muito sentido quando se começa a mexer em um projeto novo, os testes ajudam a compreender melhor as motivações por trás do código. Não acredito 100% na utilização de testes para substituir totalmente documentação escrita, mas os testes evoluem de forma muito mais natural do que documentação escrita (quando esta existe).

Nos certificarmos de que todas as peças se encaixam

Na formas de modelagem de software utilizados atualmente construímos aplicações em camadas. Temos níveis de abstração distintos e todos eles devem funcionar em conjunto, mas podem ser devidamente isolados. Enquanto isolados, utilizamos testes unitários para especificar cada comportamento de forma independente. Porém isso não é o suficiente, nem do ponto de vista técnico e nem do ponto de vista do usuário, pois:

  • O que funciona muito bem de forma separada pode não funcionar tão bem assim em conjunto.
  • Precisamos saber se a interação do usuário com a aplicação atende ao que foi especificado.

Aqui entram os testes de integração. Coisas como o nosso velho conhecido Cucumber, que testam nossa aplicação de ponta à ponta, interagindo com nossas views (ou até mesmo com nossas CLIs se estivermos desenvolvendo aplicações para linha de comando) e passando por todas as camadas que a compõe. Só assim para termos uma garantia um pouco melhor de que todas as peças funcionam em conjunto.

Você está testando pelas razões corretas? Qual o impacto dos testes no seu dia a dia? É positivo?

E então chegamos ao cerne deste post: Você testa porque enxerga em tal prática algo que realmente traz benecífios ao projeto? Ou você testa somente porque os fodões também testam e você tem que dançar conforma a música?

Escrever os testes depois de ter escrito a implementação

Esse é o sintoma mais claro de que você escreve testes por obrigação e não porque eles te ajudam de alguma forma. Claro que em alguns casos escrever os testes depois fazem sentido, como quando você quer entender melhor como uma parte do código que você não conhece (e que não possui testes) funciona ou antes de tentar refatorar alguma parte do código que não possui cobertura de testes. Mas se você sempre escreve os testes depois, você está testando pelas razões erradas.
Quando você escreve o teste após ter feito a implementação você não está especificando o comportamento esperado, você está apenas forçando o teste a passar. Isso faz com que você vá sempre pelo caminho mais simples, o que na grande maioria das vezes não prevê as condições de contorno do seu problema. Em outras palavras, você só vai testar as condições onde tudo dá certo naturalmente. Você vai achar que está coberto, mas na verdade bugs vão te pegar pelo caminho, tal qual teriam pego caso você não houvesse escrito teste algum.

Escrever grandes blocos de setup para seus testes e achar que isso é natural

Quando você escreve testes, seus blocos setup ou before são muito longos? Saiba que isso pode indicar que seu design está errado. Confesso que passei por isso diveras vezes e fui relapso. Paguei o preço mais tarde, com designs que apresentavam alto acoplamento e dificil manutenção. Quando você precisa escrever linhas e mais linhas de código apenas para montar um cenário no qual uma característica de um objeto possa ser testada, isso indica que seu objeto pode possuir alguns dos seguintes problemas:

  • Alta dependência de objetos de outras classes.
  • Conhecimento excessivo de detalhes de implementação dos objetos de outras classes.
  • Interfaces frágeis, onde pequenas alterações de código fazem com que grandes porções da aplicação precisem ser reescritas.

O problema se encontra em ver isso acontecer e acreditar que é normal, que é apenas um efeito colateral da prática de escrever os testes. Não é. Isso indica que seu design deve ser melhorado e se você ignora esse sinal, está testando pelos motivos errados. Os testes não servem apenas para que você fique feliz quando tudo está verde e se sinta preocupado quando algo fica vermelho. Eles estão ali como guias. Testes são poderosos, eles podem prever o futuro. Eles mostrm que seu código está embolado e que as coisas poderiam estar melhor organizadas. Não ignore esse sinal, reescreva, refatore.
Algo que colabora para que isso seja interpretado como um resultado natural da escrita de testes é a lenda da correlação entre alto índice de LOC de teste versus LOC de implementação e uma provável alta cobertura dos testes. Indices assim não são necessariamente sinais de boa cobertura de testes, porque você pode estar escrevendo muito código de setup, quando na verdade seus testes deveriam ser longos por testarem diversos cenários e condições diferentes, exercitando sua implementação.

Ser bonzinho com o código

Quando estiver escrevendo testes, você deve ser um carrasc@. Deve preferencialmente esquecer que quem vai implementar aquilo é você mesm@. Somente assim você vai pensar em escrever testes para todos (ou o mais próximo disso) casos possíveis. Só assim, usando o chapéu de Q&A e massacrando seu código com as possibilidades mais absurdas e maldosas você vai ter certeza de que tem uma boa cobertura e de que sua implemetação é suficientemente robusta. Não existe algo como “isso nunca vai acontecer, é pouquissímo provável!”. Vai acontecer sim e vai estourar no seu colo. Por isso, teste cada caso, não tenha pena de si mesmo ou do seu código.

Não ser crítico

Atualmente ferramentas para testes automatizados são extremamente numerosas. É dificil escolher uma para usar. Mas ainda mais dificil é ser capaz de enxergar as limitações de uma determinada ferramenta e sair da zona de conforto e aprender algo novo quando aquilo que se conhece não atende nossas necessidades. Sou testemunha disso e nos últimos meses quebrei a cara por pelo menos duas vezes por ser assim, mais especificamente em Ruby.

Primeiro, finalmente percebi que o Rspec não é a solução para tudo, apesar de ser extremamente poderoso, bem documentado e amplamente utilizado. O Rspec é mais uma dependência e o bom senso tem me mostrado que quanto menos dependências em seus projetos você tiver, mais feliz você será a longo prazo. O que estou começando a fazer é usar o Rspec apenas para aplicações. Gems e plugins estou começando a testar com as ferramentas nativas do Ruby (Test::Unit e agora no Ruby 1.9 o Minitest). Uma gem a menos para baixar, menos dependência externa e maior facilidade para quem quiser ajudar e implementar novas funcionalidades.

Percebi também que nada é perfeito. Após praticamente dois anos escrevendo cenários do Cucumber diariamente, notei que essa prática não é nem um pouco produtiva e é necessário algo que justifique todo esse trabalho. A idéia do Cucumber é simplesmemte fantástica, mas ela espera um cenário que em muitas vezes é praticamente utópico: A participação ativa do cliente durante o desenvolvimento. Não vou discutir aqui sobre as implicações disso nas práticas ágeis de desenvolvimento, mas a questão é que o mundo real nem sempre é belo como os livros contam. Muitas vezes você vai estar tendo um trabalho enorme para criar algo que poderia agregar valor para o cliente mas que ele nunca vai olhar.
Após assistir à palestra do Daniel Lopes sobre o Steak, a coisa acabou por fazer sentido para mim de uma vez por todas. Ainda precisamos escrever testes de integração (e na minha opinião esses são os testes mais importantes no fim das contas), mas se esses cenários não forem escritos pelo cliente e revistos por ele posteriormente, passar por todas as etapas necessárias para testar tal cenário com o Cucumber pode não ser vantajoso. Nestes casos, o testes devem voltar a ter seu papel inicial: ajudar o desenvolvedor. Não precisamos de mais uma DSL, já sabemos Ruby e isso já é o suficiente para que possamos ler nossos testes.

A lição é que devemos ser críticos e todos os dias analisarmos o que estamos fazendo. Será que essa é a melhor forma de fazer isso? Você pode perceber que não e quanto mais cedo isso acontecer, melhor.

9 Comentários

Deixe um comentário
  1. Felipe Coury / set 26 2010 11:19 pm

    Eu descobri a maioria dos benefícios e técnicas que você descreve, após penar bastante.

    De todos os conselhos, o que mais eu me pego forçando a fazer é ser carrasco, colocar mesmo o chapéu de Tester e Q&A e imaginar-se como o cliente da aplicação.

    No final, isso se torna essencial. Essa técnica te obriga a enfrentar problemas e decisões de design que acabam impactando em outras áreas do sistema.

    Eu me pego frequentemente colocando um test case em pending porque preciso criar algum componente ou adicionar uma feature nova a um componente já existente para atender um requisito.

    A melhor sensação se dá quando a necessidade acontece novamente e você já tem isso escrito e testado. Você pode generalizar o componente com confiança, refatorar e quando tudo volta ao estado “verdin”, a sensação é de “full circle”, ou seja, de que tudo se encaixa.

    Excelente post!

    Abração,

    Felipe.

  2. Felipe Coury / set 26 2010 11:22 pm

    … uma nota: o link para a palestra do Daniel está apontando para seu próprio blog :-)

    • cassiomarques / set 26 2010 11:29 pm

      Obrigado, tinha esquecido de colocar o endereço dentro do href do link, ficou vazio. Corrigido!

  3. Renata Eliza / set 27 2010 12:56 am

    Muito interessante a sua perspectiva Cássio. Está aqui um post que eu compartilharei com alguns desenvolvedores.

    Desenvolvedores esses que em sua maioria pensam que a função de testar efetivamente é de exclusiva responsabilidade do Teste.

    O seu post quebra paradigmas e desmistifica um pouco isso. Bacana!

  4. Lucas Catón / set 27 2010 4:25 pm

    Excelente post Cássio, muito bom mesmo.

    Me fez repensar algumas coisas e notar que em alguns aspéctos estou realmente testando de forma errada.

    Obrigado pelo post. E parabéns!

  5. Roger Leite / set 27 2010 4:26 pm

    Concordo plenamente com o post. Lembrando que tive que ser “paranóico” com o time para todos começarem a fazer testes.

    Nota importante: Faz mais de um ano que estou trabalhando com ruby e rails, e hoje se não fosse isso, acho que teria desistido dos testes em geral. Trabalhava com Java antes.

    Depois de um bom tempo apanhando, chegamos no seguinte modelo:
    – model com Rspec e Remarkable
    – integração com Rspec e Cucumber.

    Está sendo o suficiente !

  6. Bruno Andrade / set 27 2010 7:08 pm

    Grande Post Cassio, parabéns

    eu sempre vejo tdd como uma tecnica bem mais de design, do que de testes em si,gostei muito do seu pensamento,inclusive estou usando o steak para testes.Recentemente eu passei pelo caso de que para griar um teste eu precisava criar muitos objetos e relacionamentos… não refactorei esta parte do codigo ainda mas me parece um mal cheiro no codigo o que achas?

    • cassiomarques / set 27 2010 7:26 pm

      Depende.

      Se for um teste de integração, talvez não haja problemas, acho que cada caso é único e seria o caso de analisar mesmo. Já em testes unitários eu acho que pode ser um mal cheiro.

      No caso de testes de integração talvez seja o caso de verificar se sua feature não está complexa demais e se ela não poderia ser quebrada em cenários/views diferentes. Mas nem sempre isso é possível…

Trackbacks

  1. O melhor da semana 26/09 a 02/10 « QualidadeBR

Deixar mensagem para Felipe Coury Cancelar resposta