Fork me on GitHub

Keep Learning Conhecimento nunca é o bastante

Postado em
24 October 2008 @ 14:23

Tag(s)
Desenvolvimento, Opinião, Rails, Test-Driven Development

Jogue fora os testes quebradiços

É isso mesmo! Jogue fora. Todos eles! Você não precisa de deles. Você precisa de testes que garantam o comportamento do seu sistema. Software não é sobre o código escrito, software é sobre gerar o comportamento esperado a partir de um conjunto de requerimentos.

Bem, não sou um especialista em testes nem um guru da programação, mas tenho algumas opiniões. 🙂

O que é um teste quebradiço (ou “brittle test”)?

Um teste quebradiço é aquele que passa a falhar após uma alteração em código não relacionado ao que ele deveria testar ou quando o código que ele testa é alterado, mas o comportamento do sistema não muda.

Além do custo de manutenção inerente à correção de testes que quebram “sem motivo”, ocorre também um impacto na moral e na motivação dos desenvolvedores. Se um desenvolvedor faz mudanças em um código e, ao integrá-lo, testes começam a falhar por motivos como inconsistência de dados em banco ou alto acoplamento, é praticamente certo que isso irá irritá-lo e, a médio prazo, fazer com que ele abandone os testes, criando verdadeiros “ferros-velhos” de testes sucateados. As quebras se tornarão cada vez mais frequentes e, logo, processos como integração contínua serão completamente abandonados.

Casos comuns

Fixtures

Manter os grafos de objetos através de fixtures torna-se exponencialmente mais complexo à medida que sua aplicação aumenta de tamanho e cria mais relacionamentos entre os objetos. Adicione um atributo ou relacionamento entre modelos e um grande número de testes automaticamente começará a quebrar, exigindo esforço considerável para correção.

Utilize algo como object_daddy e factory_girl que, sim, também precisarão de alguma manutenção após alterações na estrutura de dados, mas exigindo esforço muito menor.

Testes de views em código (usando coisas como assert_tag ou o matcher have_tag)

Esses testes são uma completa e desnecessária perda de tempo. Uma mudança em alguma classe ou nome de campo e, bang!, testes quebrando sem que o comportamento da aplicação tenha sido afetado.

Você pode ser moderado nesses testes e não torná-los muito específicos para evitar isso mas, nesse caso, use algo como o Selenium, também com cuidado, pois é possível cair no mesmo erro com essa ferramenta.

Teste o que influi no comportamento, como o fluxo de redirecionamentos ou elementos com ids utilizados para chamadas ajax.

O Selenium também tem a vantagem de poder ser utilizado por testadores com algum conhecimento de estrutura Html.

Abuso de mocks e stubs

Isto é, para mim, um dos grandes vilões dos testes quebradiços. Esse abuso leva os testes a ficarem altamente acoplados a detalhes de implementação. Por exemplo (o exemplo é bobo, apenas para ilustrar):

class Delivery
  def has_packages?
    self.packages.count > 0
  end
end
 
class DeliveryTest < Test::Unit::TestCase
  context "A scheduled delivery" do
    setup do
      Delivery.stubs(:count).returns(10)
      @delivery = Delivery.new
    end
 
    should "have packages" do
      assert @delivery.has_packages?
    end
  end
end

Se, por um motivo qualquer, a implementação do método has_packages? mudar e passar a utilizar, por exemplo, o método size ao invés de count, o teste passará a quebrar sem que o comportamento tenha sido alterado. O exemplo é simples mas, em casos reais, isso é um grande problema. Note que stubs tendem a deixar os testes mais acoplados do que mocks.

Algumas soluções pra isso são: não utilizar mocks e stubs (o que é um tanto extremista), aumentar o encapsulamento das classes ou aceitar isso e continuar.

Esse tipo de coisa costuma acontecer quando queremos utilizar mocks e stubs para tornarmos os testes independentes de recursos externos, como banco de dados. Nesses casos, o trade-off entre acoplamento do teste ao código e o ganho em velocidade de execução ou tempo de manutenção através do isolamento deve ser avaliado.

Guias gerais

Para evitar testes quebradiços, algumas guias podem ser utilizadas para a maioria dos casos:

  • Especifique apenas o que você quer que aconteça e nada mais
  • Em termos de mocks e stubs: use stubs para queries (tudo o que retorna dados e não modifica estado) e mocks para comandos (que não necessariamente retornam algo, mas modificam estado)
  • Lembre-se de que o que importa é o comportamento do método/classe/sistema. Como isto é implementado, em nível de código, não deve fazer diferença nos seus testes

Como já disse, não sou um especialista. Então, se você percebeu alguma grande besteira nesse post, não exite em discutir, eu tenho a mente aberta. 🙂

Update

Note que mocks e stubs, quando usados corretamente, são uma importante ferramenta de design. Esse tipo de ferramenta, além de isolar os testes de recursos externos, tem o intuito (e talvez até o principal objetivo) de servir como apoio à prática do TDD: enquanto escreve os testes antes do código, mocks e stubs (e dublês em geral) permitem que você especifique o comportamento de partes ainda não existentes da aplicação. Nesse processo, torna-se mais fácil detectar falhas como alto acoplamento e corrigí-las o quanto antes, gerando menos stress.

Leia mais:


5 Comentários

Comentário por
Jony dos Santos Kostetzer
24 October 2008 @ 14:49

Ótimas dicas Lucas, parabéns.

Um dos entraves chatos para testes quebradiços é quando há muito encadeamento de métodos internamente (uma prática a se evitar eu diria), tipo user.documents.pages.count. Fixtures, object factory, mock, tem casos que é dificil saber o que é melhor para isso.

abraço!


Comentário por
Rafael Mueller
24 October 2008 @ 14:54

Lucas, não entendi o porque “stubs tendem a deixar os testes mais acoplados do que mocks.”

Eu sempre pensei ao contrário, quando utilizo stubs, eu somente indico qual o retorno de um método, ao usar mock, informo (posso informar) quais métodos (dentro do método testado) e quantas vezes eles serão executados.

Com stub, posso refatorar completamente um método sem quebrar os testes, mocks podem fazer os testes quebrarem.

Belo post, abraço!


Comentário por
Rafael Mueller
24 October 2008 @ 15:14

@Jony, isso deve ser evitado sim. A lei de Demeter (Law of Demeter) fala exatamente isso.


Comentário por
Lucas Húngaro
24 October 2008 @ 15:22

Rafael, dos testes que eu vi até hoje (ler testes é uma boa para aprender), percebi essa tendência de acoplar os testes a métodos muito específicos através de stubs, como no exemplo que dei.

No caso de mocks, você pode estabelecer expectiativas sobre os métodos a serem chamados, a quantidade de vezes e, dependendo do framework, a ordem de chamada. Isso pode mesmo se tornar muito ruim, caso o desenvolvedor entre numa louca tendência a criar expectativas pra tudo. Nesse caso, caímos no mesmo problema do exemplo que dei com stubs.

No entanto, vou ler mais sobre isso e conversar com algumas pessoas para tentar entender melhor.


Comentário por
Rafael Mueller
24 October 2008 @ 15:30

É verdade, acho que TDD/BDD varia também de acordo com a equipe que está desenvolvendo, nos projetos que participei percebi que com mocks acabava ficando mais acoplado.

No fim acho que o acoplamento maior/menor vai depender mais da equipe desenvolvendo do que o fato de usar mocks/stubs.

Abraço


Deixe um comentário