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:
- Does stubbing make your tests brittle?
- Sorry, you’re just not my type
- Brittle tests
- Mocks aren’t stubs
5 Comentários