Agile

28 dez, 2011

Mantenha a mente aberta. TDD nem sempre é o melhor!

Publicidade

Às vezes defendemos tanto alguma ideia, repetindo seus benefícios, que diminuímos nosso olhar crítico sobre ela e deixamos de perceber que nem sempre ela se aplica tão bem quanto defendemos. Em casos mais extremos, entramos em um modo de defesa do tipo “not listening”, e simplesmente não ouvimos mais de verdade nenhum argumento. Você já passou por isso? Na verdade, é mais comum do que parece.

Um dos mais antigos colaboradores da Qualidata, o Ricardo (http://www.laedevolta.com.br/blog), atualmente afastado por estar fazendo doutorado na Dinamarca, há algum tempo me encaminhou por e-mail um artigo do Mike Tayler que questionava algumas questões sobre testes. Sou um defensor de testes automatizados e de TDD (test-driven development), e o e-mail do Ricardo levantava um ponto de discussão muito interessante sobre a qualidade de algoritmos construídos com TDD. Gosto disso! O artigo Testing is not a substitute for thinking, no contexto da minha discussão com Ricardo sobre o assunto, estava sendo utilizado para questionar a proposta do TDD de construir as funcionalidades de forma incremental, através de testes unitários e sucessivas refatorações, deixando emergir a solução à medida que os testes são satisfeitos e a funcionalidade desenvolvida. Para Mike Tayler, “construir testes não substitui o pensar na solução”.

Conhecendo bem o Ricardo, seu equilíbrio e sua visão crítica, procurei dar todo o crédito ao questionamento, e ler o artigo com bastante atenção. Mas, mesmo assim, analisando o fato em retrospectiva, percebo que num primeiro momento fui influenciado por uma visão preconcebida. Logo de cara, eu assumi que o Mike Tayler provavelmente não tinha experiência com TDD. Mas por quê? Porque ele discorda daquilo que eu penso ser o certo? E, a propósito, nós realmente discordamos? (não perca o final do post, hehehe)

Pois é, ser humano é ser complicado. Mesmo querendo ser abertos a mudanças, de alguma forma tendemos a desenvolver visões preconcebidas e darmos respostas prontas aos questionamentos que, de alguma forma, colidem com nossas convicções. Logo, não podemos simplesmente assumir que “somos abertos” porque o queremos ser. Manter de fato a mente aberta às mudanças é um exercício que precisa ser praticado todos os dias, em cada nova situação.

Manter de fato a mente aberta às mudanças é um exercício que precisa ser praticado todos os dias, em cada nova situação

Mas o que dizia o artigo? Em resumo, o artigo explorava os comentários de um outro artigo relativo a um exercício que pedia que os desenvolvedores escrevessem e submetessem uma busca binária sem testá-la. O ponto do autor é que escrever testes automatizados está muito em moda, e de alguma forma para o Taylor isso pode limitar o desenvolvedor, pois testar não é a única arma em nosso arsenal. Ele destaca três limitações dos testes:

  • Testes conseguem apenas mostrar a presença de erros, e não sua ausência;
  • Testes podem ter erros, da mesma forma que o código testado;
  • Testes aumentam a confiança, e não o entendimento;

Na verdade, o autor diz que os dois primeiros pontos são apenas para esquentar, e que o terceiro é de fato o mais importante. E de fato o autor desenvolve bem o último ponto, procurando mostrar que escrever testes não seria uma boa técnica para desenvolver algoritmos. Que antes de sair escrevendo testes unitários com refactoring, é necessário pensar e analisar o problema, partindo para implementar o algoritmo (e seus testes) depois de compreender bem a solução que será dada. Ao final, ele aborda a questão TDD mais diretamente, argumentando que o código final com TDD seria mas confuso do que um projetado previamente. Vale a pena a leitura de pelo menos esses tópicos do artigo original.

Voltando à história, sou um defensor do uso de TDD no desenvolvimento, mas não sou extremista. Mas estava eu lendo o artigo que Ricardo me encaminhou, e meio aborrecido com o fato de alguém, em meio a algumas verdades, soltar um monte de “baboseira” sobre o desenvolvimento com TDD produzir código ruim. Pelo menos era isso o que eu sentia na hora.

Segue uma transcrição da minha primeira resposta ao Ricardo. Note que os trechos destacados são temperados com uma boa dose de preconceito.

Fala Ricardo,

Veja, Dikstra (citado no texto) é conhecido por até certa altura de sua vida não utilizar computador e escrever seus algoritmos à caneta tinteiro, em papel, sem rasuras. Logo, Dijkstra não estava muito preocupado com a implementação e manutenção de código-fonte de sistemas. Ele estava preocupado em resolver matematicamente problemas computacionais de forma elegante e eficiente, com garantia de correção. Neste contexto não cabe falar de TDD, pois não estamos falando de construir programas, mas de projetar algoritmos.

Veja, propor um exercício de programação proibindo teste é uma estratégia interessante. Inclusive, eu iria além: propor uma solução em uma linguagem abstrata, que não pode ser compilada nem executada, pois o que estaria em foco é a capacidade de alguém pensar em um problema, compreendê-lo, para então propor uma solução de forma abstrata, e não partir para uma implementação baseada em tentativa e erro.

Mas se eu fosse implementar em uma linguagem como C# o famoso algoritmo de Dijkstra (menor caminho de um vértice a todos os outros em um grafo), especificado em alguma notação matemática de alto nível, certamente começaria escrevendo testes unitários, pela simples razão de que ao final vou querer testar o que escrevi, e a sistemática do TDD funciona bem pra isso, além de garantir que eventuais alterações posteriores não irão quebrar o algoritmo. Contudo, se eu estiver propondo um novo algoritmo para solucionar o problema, de forma alguma iria utilizar TDD. Na verdade, de forma alguma iria utilizar uma linguagem de programação para pensar na solução.

Não estou defendendo que TDD valha para tudo. Porém tenho muita dificuldade de encontrar exemplos em que TDD não se aplique na construção de sistemas. A célebre frase de Dikstra “Testing can show the presence of bugs, but not their absence” é perfeita, mas o que isso tem a ver com TDD? Ao falar isso, Dijkstra provavelmente estava se referindo a um processo de análise e projeto de algoritmos chamado de “derivação formal”, que ele tanto defendia. Isso significa definir propriedades como invariantes, pré e pós-condições, e utilizar técnicas como indução matemática para provar a correção de um algoritmo. O que Dijstra está dizendo é que testes não provam correção. Utilizei na minha dissertação uma extensão da linguagem de comandos guardados do Dijkstra [Dijkstra, EdsgerW. Guarded commands, non-determinacy and formal derivation of programs. Comm. ACM 18(8):453-457, 1975] que define matematicamente a semântica de cada comando e nos permite desenvolver provas matemáticas da correção dos algoritmos. Enfim, estamos caminhando aqui no campo dos métodos formais, que pelo elevado custo ficam frequentemente restritos à pesquisa científica e a demandas muito específicas. Gostaria muito de ver isso presente no nosso dia-a-dia, mas temo que estejamos bem longe. Contudo, ainda assim, ao mapearmos especificações formais para implementações em linguagens de programação, podemos introduzir erros, e teremos de tratar questões como problemas de alocação de memória, erros de comunicação, etc.. Neste contexto testes unitários me parecem ainda muito interessantes para conduzir a implementação, mesmo quando o comportamento tenha sido definido através de uma especificação formal. Logo, para mim, TDD e métodos formais atuam em camadas de abstração muito distintas, resolvendo problemas distintos.

A afirmação “Tests increase confidence, not understanding” e todo o argumento que um código escrito com TDD fica confuso revela pra mim falta de experiência pessoal com TDD. Mais uma vez, se ele está comparando testes unitários com especificações formais de sistemas, aí não há o que discutir, pois seria como perguntar, o que é melhor, suco de laranja ou Jazz? Mas se ele está falando de desenvolvimento de software em geral, provavelmente ele deve estar tomando como base códigos ruins escritos por desenvolvedores ruins utilizando TDD. E obviamente o problema não é o TDD. E não há método que resolva o problema de pessoas não qualificadas. Além do mais, no TDD o “refactor” é exatamente a oportunidade de lustrar o seu código até onde o tempo/dinheiro permitam. Poderíamos experimentar implementações totalmente diferentes para o mesmo problema, e ficarmos com a mais elegante. Admito que em algumas situações precisamos criar algumas inversões de controle para viabilizar os testes, e isso parece não agregar muito ao sistema em si, mas tudo tem seus pros e contras.

Enfim, alguns pensam que TDD e “processo ágil” é começar escrevendo código e deixar o pau quebrar, pois no final a gente sai refatorando. TDD não defende isso, e de fato não pode ser assim. A verdade é que, na prática, com TDD, preciso pensar muito mais antes de sair programando do que quando programava sem TDD. Concordo que não é bala de prata, mas no contexto de desenvolvimento de sistemas que nós estamos acostumados, a aplicabilidade é imensa.

PS: Gostaria muito de ver essas ferramentas de testes unitários evoluírem para provadores de teoremas, e o mundo da derivação formal alcançar o mundo da programação, mas se 90% dos desenvolvedores não conseguem escrever uma simples busca binária, temo que 99.999% não tenham base matemática suficiente para trabalhar com isso.

Abraços,
Fabricio.

TDD e métodos formais atuam em camadas de abstração muito distintas, resolvendo problemas distintos.

Pois é, apesar de ainda concordar com boa parte dos elementos da minha resposta, hoje tomaria um caminho bem diferente. É muito fácil nos afastarmos dos pontos que realmente estão em discussão, e partirmos para ataques pouco coerentes.

Como esperado, depois disso, a resposta do Ricardo foi ponderada, concordando em geral com minha argumentação, mas destacando a falta de embasamento para as minhas pressuposições sobre o senhor Taylor não ter experiência real com TDD.

Como disse, procuro manter a mente aberta. Mas, só após receber a resposta do Ricardo apontando minha incoerência, pude fazer uma nova leitura do artigo e enxergar o ponto do autor. De fato, percebi que concordava totalmente com ele.

Por exemplo, no final de 2009 se não me engano, estava cooperando em um projeto aqui na Qualidata, e antes de partir para implementar uma certa restrição de negócio que não era muito trivial, fui para o quadro, fiz uns desenhos e uns pequenos cálculos, e ao final cheguei a uma expressão que representava a restrição (figura abaixo). Criei um caso de teste, implementei a expressão e o teste passou. Depois disso, escrevi mais vários testes, cobrindo os diferentes cenários, e todos passaram sem qualquer alteração no método em questão. Na verdade, isso, de certa forma, foge bastante da proposta TDD de dar passos pequenos. Ou seja, ao pensar antes na fórmula e codificá-la de uma vez, eu codifiquei mais do que o mínimo necessário para fazer o meu primeiro teste passar. Mas, nesse caso, pensar antes na solução foi muito melhor do que sair programando e refatorando, e realmente acredito que o resultado com  “TDD puro” seria inferior.

Foto do quadro branco com as restrições (alfa e beta) analisadas

Enfim, ao final, não só eu já concordava com a questão levantada pelo Mike Tayler, mas também a colocava em prática no meu dia-a-dia. Contudo, incrivelmente, durante a primeira leitura do artigo, de alguma forma a sua crítica ao TDD acionou em mim um modo de defesa que fez com que eu analisasse a questão de forma nada isenta.

Então é isso aí, pessoal. Cuidado com análises rasas e defesas muito apaixonadas. Mantenham a mente aberta!