Posts Tagged ‘tdd’

A doce ilusão da alta cobertura em testes

Há tempos atrás - quando eu trabalhava em grandes empresas de desenvolvimento de software - tinhamos em nosso workflow a prática de enviar nossos códigos para a equipe de QA realizar uma “extensa” bateria de testes. O código que passava nos testes era considerado de qualidade. Para os que não passavam, os programadores responsáveis eram alertados sobre a vulnerabilidade e tinham a missão de corrigí-los.

Lembro-me de participar de reuniões por motivos de falha encontrada no sistema após a entrada em produção. Os motivos de existir a falha eram estudados e na grande maioria das vezes, os testers acabavam sendo apontados como culpados por “não terem realizados testes suficientes”. Não me lembro de presenciar nada sobre “fazer testes ruins”, testes que em si, pouco testavam.

Após a expansão de TDD e dos frameworks que permitiram automatizar testes, medir a cobertura em testes de um determinado projeto foi uma evolução natural para quem utiliza testes automatizados. Diversas ferramentas, para diversas linguagens/plataformas, começaram a aparecer e a facilitar a monitoração deste indicador.

Porém, tenho visto em alguns projetos, que o índice de cobertura em testes tem sido encarado como um fim, ao invés de uma consequência. Valores para o índice de cobertura em testes tem sido estipulados no início do projeto, e o código - consequentemente a equipe - passa a ser encarada como “de qualidade”, caso mantenha o valor inicialmente estipulado.

Coincidentemente - ou não - alguns códigos de testes desses projetos não são tão bem escritos. Testam pouca coisa, ou absolutamente nada. Mas mesmo assim, a avaliação que o código recebe é de “auto grau de qualidade”, já que apresenta um elevado índice de cobertura em testes.

Resolvi então, escrever alguns exemplos para ilustrar esse post. No trecho abaixo, temos uma classe NumberUtil, que possui 2 métodos: 1) humanize_array - onde dado um array é retornado um outro array, porém com os números em forma de string - por extenso; 2) humanize - onde dado um número retorna a sua string - por extenso;

Olhando atentamente ao trecho acima, percebemos que existe um bug na linha 10. Isso faz com que dado o número 1, a string retornada seja “zero”.

Logo em seguida temos a sua classe de teste, chamada NumberUtilTest. Esperamos que essa classe possua uma boa cobertura em testes para que possa identificar o problema no trecho anterior.

Os dois métodos de NumberUtil são testados, a cobertura está boa, mas se executarmos os testes, nenhuma falha é apresentada. Exatamente o contrário do que imaginávamos, já que existe um bug na classe.

Isso acontece porque o método test_should_humanize_array não está bem escrito. Existe uma falha básica nele, que faz com que não identifique o bug. Ele deve esperar o valor “one” ao invés de “zero”, já que a primeira posição do array dado, tem valor 1.

Temos aqui um bom exemplo de alta cobertura em testes, mas com pouca qualidade em código, já que os testes não identificam um blug relativamente simples.

Outros tipos de falha em sistema, ocorrem por falta de implementação, falta de código. Isso é realmente mais difícil de ser capturado em testes automatizados. Vamos dar uma olhada no trecho a seguir:

Acima temos uma classe chamada Person, onde o programador esqueceu de retirar o comentário em uma validação para o atributo username. Isso faz com que, mesmo que nenhum valor para username seja informado, o objeto será salvo no banco de dados.

Vamos então verificar a sua classe de teste, chamada PersonTest.

Podemos verificar que o método test_should_create_person realiza um teste simples para a criação de um objeto Person, sem informar valor para o atributo username. Esse teste será executado com sucesso, mesmo que não seja desejável ter um objeto Person sem username preenchido.

Para isso, o mais interessante seria termos uma classe de teste um pouco mais robusta, conforme podemos visualizar abaixo:

Acima, testes são realizados para identificar falhas em código existente e em códigos não existente.

Com base nesses exemplos simples, podemos verificar que ter um alto índice em cobertura em testes não é razão para ter um código de qualidade escrito.

Fixar valores para esse indicador, não fará com que código de qualidade seja escrito. É preferencial que a escrita do bom código seja estimulada, assim como bons testes. Com isso, a alta cobertura em testes se apresentará como consequência e não como fim.

Tenho certeza que você já se deparou com código de teste mal escrito. Código que no final das contas, nada ou pouco testava. Caso tenha esse exemplo, mande para mim :) Envie para o meu e-mail (fmesquitacunha @ gmail), cole o código no Gist ou Pastie e me mande a URL. Ou ainda, faça um post no seu blog e link para cá. Quem sabe consigo fazer uma série sobre *Testes que nada testam* :)

Sobre testes e TDD

Muito tem se falado e se escrito sobre a realização de testes no processo de desenvolvimento de software. Bastante coisa interessante vem acontecendo neste cenário e isso é muito interessante, pois testes são de extrema importância para a qualidade de software.

Automatizar testes (sejam eles unitários, de integração ou aceitação) é uma tarefa que tem se tornado cada vez mais simples devido aos frameworks de testes e ferramentas que oferecem suporte para os mesmos. As IDEs estão cada vez mais completas e possuem suporte para integração com os frameworks, o que facilita e agiliza o ato de testar software. Também temos as ferramentas de integração contínua, que podem ser configuradas para trabalhar em conjunto com os frameworks de teste, o que torna a automação bastante completa.

Uma sigla já bem conhecida e divulgada no cenário de desenvolvimento de software é TDD. Test Driven Development (TDD) significa guiar o desenvolvimento de software através de testes. Na prática, primeiro é escrito o teste para que posteriormente seja escrito o código que vai atender a determinada funcionalidade. O teste valida se o código está em conformidade com a funcionalidade que se propõe implementar, caso esteja, diz-se que o código passou no teste, caso contrário, não. Ou seja, após definir qual funcionalidade será implementada, escreve-se um teste onde o código que passe neste teste estará, consequentemente, implementando a funcionalidade. Diversos autores já escreveram sobre TDD, dando sua importante colaboração para o cenário de desenvolvimento de software.

Porém, em minha opinião, vejo na TDD uma “técnica” muito mais importante para o design de software do que propriamente para testes. Tendo como objetivo escrever código que passe em um teste, muitas vezes o desenvolvedor “se vê obrigado” a utilizar técnicas de programação que diminuem o acoplamento entre classes e que cada classe e seus métodos tenham seus papéis muito bem definidos, fazendo com que cada teste específico possa ser realizado sem a preocupação de testar outras tantas funcionalidades. Com isso técnicas como refactoring e dependency injection acabam sendo amplamente aplicadas, o que torna o design limpo e coeso.

Mas com todo esse movimento em torno de testes de software e TDD, algumas coisas tem se confundido. Testar software é executar os diversos testes em cima de código escrito. Ou seja, depois de escrito uma determinada funcionalidade, executa-se os testes e checa-se o resultado. Caso o código escrito tenha passado nos testes diz-se que está em conformidade com a funcionalidade que se propõe a implementar, livre de bugs, e apto à produção. TDD é desenvolvimento guiado por testes, ou seja, a partir de testes se escreve código.

Quando utilizamos TDD consequentemente testamos software, e isso é bom! Aplicando TDD garantimos um design limpo, coeso, e ao executar os testes efetivamente testamos o software, logo temos as duas abordagens sendo utilizadas. Mas não necessariamente quem testa software faz TDD. Nem mesmo quem automatiza teste necessariamente faz TDD. Um exemplo que tenho visto bastante é de equipes que escrevem código e depois os testes para atestar a qualidade do código anteriormente escrito. Isso não é TDD. Pode até parecer que não existe diferença entre escrever o código e depois o teste ou o teste depois o código, já que o importante é o teste existir, mas é como eu disse anteriormente, TDD não está relacionado a teste está relacionado a design. E, em minha opinião, está muito mais relacionado a design do que a teste. Quando se escreve um teste antes do código, a intenção no momento em que vai se escrever o código é de passar no teste, e a intenção do teste é aplicar regras para que a funcionalidade seja implementada e que esteja correta.

Hoje pela manhã li um bom post escrito por Uncle Bob, onde ele comenta sobre testes gerados automaticamente e TDD. Testes que são gerados automaticamente, são gerados a partir de código escrito! E se o código foi escrito antes do teste, não foi feito TDD. Isso não invalida o que foi feito, afinal, foi aplicado um teste, porém não foi feito TDD.

Concluindo, TDD para mim está muito mais relacionado a design do que a testes, já que o design é feito de maneira diferente quando se aplica TDD do que quando simplesmente se realiza testes depois do código escrito.

Abaixo um pequeno trecho do post que mencionei do Uncle Bob.

In TDD, programmers state their intent twice; once in the test code, and again in the production code. These two statements of intent verify each other. The tests, test the intent of the code, and the code tests the intent of the tests. This works because it is a human that makes both entries! The human must state the intent twice, but in two complementary forms. This vastly reduces many kinds of errors; as well as providing significant insight into improved design.

Using a test generator breaks this concept because the generator writes the test using the production code as input. The generated test is not a human restatement, it is an automatic translation. The human states intent only once, and therefore does not gain insights from restatement, nor does the generated test check that the intent of the code was achieved. It is true that the human must verify the observations, but compared to TDD that is a far more passive action, providing far less insight into defects, design and intent.

É basicamente isso, espero que tenham gostado do meu primeiro post ;)

About me

Felipe Mesquita is a software developer based in Rio de Janeiro, Brazil. He works as Ruby on Rails freelancer, building web applications and running his pet projects.
Read more.
Veja o meu perfil no LinkedIn
Subscribe to RSS

Ruby Onda