quinta-feira, 26 de fevereiro de 2009

Duck typing e os testes de aceitação automáticos

Uma discussão frequente em minha equipe é sobre maneiras de garantir que nossos sistemas estejam funcionando adequadamente por meio de testes automáticos. Em nossos ultimos projetos temos conseguido alcançar 100% de cobertura de testes unitários, e nos sistemas legados temos aumentado a cobertura aos poucos.

No entanto, estamos cientes de que nem 100% de cobertura de testes unitários garantem a ausência de bugs. Isso nos remete aos testes de aceitação automáticos, não só por causa do funcionamento das telas e fluxos de navegação, mas também dos possíveis problemas que um refactoring com um pouco menos de atenção pode causar em uma linguagem baseada em duck typing.

Darei um exemplo em ruby para ilustrar um bug que pode ser introduzido sem que seus testes unitários percebam. Digamos que você tenha as seguintes classes:

class Filme < ActiveRecord::Base do
def alugar
self.estoque.remover self
end
end
class Estoque < ActiveRecord::Base do
def remover(filme)
# ...
end
end

No seu spec que verifica o método alugar da classe Filme você colocaria um mock mais ou menos como mostrado abaixo:

@estoque = mock_model(Estoque)
@estoque.should_receive(:remover).with(@filme).once

Um dia você percebe que o nome do método remover não está explicando muito bem o significado, e resolve fazer um refactoring renomeando-o para remover_fita_do_filme.

Você altera os testes da classe Estoque, renomeia o método e logo depois recebe uma ligação de uma empresa de telefonia te oferecendo serviços excepcionais que você nunca precisou. Puto da vida você desliga o telefone, roda os testes e todos passam! Serviço feito!

Perceberam o problema? Sem um teste de aceitação automático ou no mínimo um teste manual, você não perceberia que a classe Filme continua referenciando o método antigo, remover, da classe Estoque. Esse tipo de problema não passaria em uma linguagem que não segue duck typing, pois a etapa de verificação de código da compilação serviria como uma espécie de teste unitário de tipos e nomes.

Um problema mais evidente seria um método que recebe um objeto que precisa ter determinado método, ou seja implementar determinada interface. Veja o exemplo abaixo, que mostra que ao alugar um item, no caso um Filme, adiciona-se todas as tags dele às tags preferenciais do cliente:

class Cliente < ActiveRecord::Base do
def alugar(item)
#...
adiciona_tags_preferenciais item.tags
end
end
class Filme < ActiveRecord::Base do
def tags
# ...
end
end

Se mudarmos o nome do método tags em Filme e também corrigirmos o uso de dele no método alugar da classe Cliente tudo funcionará bem. Mas, como usamos duck typing não temos como garantir que outros objetos, digamos alugáveis, tenham sido alterados. Se por exemplo esta locadora também alugas livros, teríamos um erro evidente:

class Cliente < ActiveRecord::Base do
def alugar(item)
#...
adiciona_tags_preferenciais item.tags_principais
end
end
class Filme < ActiveRecord::Base do
def tags_principais
# ...
end
end
class Livro < ActiveRecord::Base do
def tags
# ...
end
end

Esse tipo de erro só seria pego num teste manual se lembrássemos de testar o aluguel de livros também. Por isso é tão importante o teste de aceitação automático em uma linguagem com duck typing. Em Java por exemplo, que a interface precisa ser explícita, o erro seria pego no que poderíamos considerar o teste unitário que a compilação executa. As classes Filme e Livro implementariam a interface Alugavel por exemplo:

public interface Alugavel {
List tagsPrincipais();
}

Mas é claro que o uso de uma liguagem menos dinâmica não remove a necessidade de testes de aceitação automáticos. O valor desse tipo de testes é independente da linguagem, e sua implementação é importantíssima para garantir fluxos de navegação e integração entre os diversos componentes do sistema.

5 comentários:

  1. Olá Tiago,

    Além de ferramentas como o Rational Functional Tester e Selenium, vocês utilizam mais alguma ferramenta para criação de testes de aceitação automáticos?

    ResponderExcluir
  2. Estamos utilizando o Cucumber e o Selenium, mas tem um pessoal por aqui que está pesquisando também o Watir.

    ResponderExcluir
  3. Ola, parabens pelo post.
    Desculpa a ignorancia, não tenho experiencia com ruby, mas quando vc faz: mock_model(Estoque), ele provavelmente cria um mock para Estoque, e no comando seguinte vc diz que esse mock deve chamar o método remover uma vez, passando como parametro um filme (não corri atras para ter certeza dessa afirmação, mas seria assim em java e não deve variar muito para ruby).
    Mas enfim, esse problema não podia ser resolvido acrescentando um pouco mais de funcionalidade na lib (lib?) que faz o mock para vc? algo que quando vc faz: @estoque.should_receive(:remover) fizesse uma analise para garantir se realmente existe o método e gerasse nem que seja um alerta dizendo: "oh, vc esta me dizendo que estoque tem um método remover, mas eu não achei nenhum método remover, da uma checada". Para não dizer em falhar o teste, pois na minha opinião não faz sentido criar um mock para a chamada de um método que no sistema de verdade não existe.
    No segundo caso do Filme e Livro com as tags, acho que naturalmente vc teria um teste unitário testando um Cliente alugando um Livro e um Filme, sendo Filme e Livro mocks, cairia no mesmo caso que falei acima.
    Esse meu pensamento faz algum sentido? to viajando no conceito?! acho que isso poderia seila, quebrar um pouco esse conceito de duck typing, mas na minha opinião, se tratando de execução de testes, seria uma vantagem a mais e com certeza não seria ruim.

    Valeu

    ResponderExcluir
  4. Faz muito sentido o que você falou Gustavo. Mas em Ruby é meio complicado implementar isso pois não há como saber se em tempo de execução a classe que você está mocando foi alterada. Muitas vezes mocamos arrays com métodos que não existem na classe Array por exemplo, apenas para tratar métodos que são inseridos por outros plugins.

    Quanto a implementar um teste com livro, também faz sentido, mas o primeiro problema persistiria. Se ele alterasse o teste do uso da classe Livro, ainda sim o bug continuaria, para corrigir o que o desenvolvedor teria de lembrar de fazer é alterar o teste de todos os objetos "alugaveis" ou seja, aqueles que possuirem o método "tags" e verificar se não está quebrando nenhuma outra parte do sistema ao fazer essa alteração.

    Sem dúvida um trabalho que pode ser bastante arriscado em sistemas mais complexos.

    ResponderExcluir
  5. Hmm, acho que entendi, ruby, groovy e tal vc pode acrescentar uma funcionalidade em tempo de execução ao objeto certo? ai ninguem te garante que Estoque (que a principio não tem mais o método remover) não terá esse método em uma determinada execução, pois o método pode ter sido adicionado dinamicamente. Isso?!
    É, realmente dificil de resolver, apenas com processos no desenvolvimento para garantir a certeza da não inclusão de bug.

    ps: tenho que deixar de ser vadio e estudar um pouco mais essas linguagens dinamicas hehe

    obrigado.

    ResponderExcluir