Mockar ou não mockar, eis a questão
O recente e polêmico debate “TDD Morreu?” entre DHH, Martin Fowler e Kent Beck trouxe à tona o nível de insatisfação relacionado ao uso excessivo de mocks e stubs em testes automatizados. DHH expressou fortemente sua opinião sobre o fundamentalismo a respeito do nível de isolamento das classes sendo testadas e criticou a necessidade demasiada que alguns testes têm de segregar completamente todos os seus colaboradores. Este também foi um dos pontos mais importantes deste post recente de Martin Fowler. Durante o hangout, os 3 afirmaram “quase não usar mocks”.
O teste que te faz dormir tranquilo
Kent Beck enfatizou que, no final do dia, como desenvolvedores/programadores, é nossa responsabilidade ter a certeza de que podemos dormir tranquilos a noite sabendo que não quebramos nada. Uma mentalidade bem diferente de antigamente, quando desenvolvedores simplesmente comitavam o seu código e esperavam por outro grupo de pessoas - os testadores - para ter certeza de que tudo ainda funcionava. Este é um dos principais objetivos de uma suite de testes automatizados, independente destes testes terem sido escritos antes do código (com a prática de "test first") ou depois que o código havia sido escrito.
A finalidade de testes automatizados é verificar, de forma rápida e confiável, que o sistema ainda funciona e que o novo código escrito não afeta negativamente o código já existente. É este tipo de confiança e feedback que não é alcançado quando mocks e stubs são exageradamente utilizados.
Por que mockar demais é perigoso?
A culpa não é dos mocks e stubs, estes dois, assim como qualquer outra ferramenta, são meras vítimas do seu uso indevido. São técnicas extremamente úteis quando precisamos isolar pontos de integração externos, como Web Services e bancos de dados, por exemplo. O perigo existe quando usamos mocks e stubs para isolar classes e métodos que pertencem ao nosso próprio código em camadas que não necessariamente precisariam ser isoladas.
Tautological TDD é um anti-padrão que explica algumas situações onde o uso excessivo de mocks e stubs é perigoso. Durante o hangout foi dito: "se faço TDD, eu posso refatorar meu código". Testes "tautológicos", que são muito caixa branca, checam mais interações do que comportamento, geralmente precisam ser modificados quando refatoramos nosso código. Se você precisa mudar o seu teste pra cada refatoração que fizer no seu código, como saber se a mudança do código não quebrou nada? Já vi muita gente mudar o código e mudar o teste só pra fazer o teste passar.
TTDD já é perigoso em linguagens como Java e C# e a situação se agrava quando passamos para linguagens dinâmicas como Ruby e JavaScript onde pode-se mockar um método que nem mesmo existe. Vou ilustrar abaixo um exemplo real, não o único, que já vi acontecer diversas vezes. Digamos que existe um controller (MyController) cuja responsabilidade é validar um model (MyModel) e exibir os seus erros. O model possui um método "errors". A imagem abaixo ilustra esse exemplo:
Ao testar o controller, mockistas tendem a isolar o model criando um mock ou stub para o método "errors". Ruby, com seu dinamismo, e frameworks de testes como Mocha, nos permitem atingir este nível de isolamento.
Se prestarmos atenção, o método no model é chamado "errors" (plural). Entretanto, o código do controller tem um problema, chama o método no singular, mas o mock/stub faz com que tudo funcione, porque o método mockado também está errado. O teste passa! O que temos aqui? Um falso positivo. Um teste verde dando ao desenvolvedor uma sensação falsa de confiança, quando o código está, na verdade, quebrado. Este é apenas um, entre vários, dos exemplos que mostra o perigo do mal uso de mocks e stubs.
Recentemente, depois de fazer uma atualização de uma dependência, descobrimos que o retorno de um método que estava sendo mockado na maioria dos testes unitários havia mudado: antes retornava nil, agora retorna um array vazio. Mais uma vez, todos os nossos testes passaram, mas o código estava quebrado.
Mais importante do que nomes de métodos errados e valores de retorno é quando o comportamento de uma determinada classe ou entidade é mockada levando em consideração premissas erradas. Quando o propósito dos testes é focado principalmente na verificação da interação entre colaboradores (testes muito caixa branca), essas interações e as expectativas dos mocks nos testes farão todos os testes passarem. Ao ver todos os testes passando, os desenvolvedores irão pra casa dormir tranquilos pensando que nada está quebrado, quando na verdade, alguns destes problemas vão direto pra produção, serem identificados por usuários reais. Já vi isso acontecer várias vezes, problemas que poderíam ter sido identificados por um teste não tão caixa branca, mas acabou indo pra produção. E você, já?
O perigo de ter testes assim é que eles nos dão um falsa sensação de segurança e confiança. Vemos um build passando, com todos os testes verdes e temos a certeza de que não há nada quebrado. Precisamos pensar melhor quando escrevemos um teste, às vezes é melhor escrever um teste que acesse um grupo de classes e não use tanto mock assim para termos a segurança de que tudo funciona. Outra discussão interessante durante o hangout e alguns outros posts foi o que é uma "unidade" em testes unitários?
Como definir uma “unidade”?
A "unidade" a ser testada é um dos grandes pontos de confusão e debate. Martin nos alerta sobre algumas definições de unidade utilizadas: "o design orientado a objetos tende a tratar uma classe como uma unidade, abordagens procedurais e funcionais consideram uma função como sendo uma unidade". Uma unidade deve ser um comportamento (behavior). É mais importante testar O QUE uma entidade faz, do que COMO esta unidade consegue fazê-lo. O critério do que deve ser uma unidade tem que ser definido pelo desenvolvedor escrevendo o código e o teste. Muitas vezes, um grupo de classes pode alcançar um comportamento, portanto, este grupo de classes é a unidade. Pense e defina a profundidade dos seus testes, sem nenhum dogma ou fundamentalismo definido pelo paradigma da linguagem que está usando, de forma que eles garantam a certeza de que o comportamento daquela unidade está funcionando.
A imagem abaixo ilustra o conceito de profundidade de teste:
Uma unidade não é necessariamente uma classe ou um método ou uma função, mas é o que VOCÊ, desenvolvedor escrevendo o teste e o código, decidir que seja, baseado no seu design e nas suas fronteiras. E, obviamente, se depois de definida a profundidade do seu teste você achar sensato utilizar um mock ou um stub para alguns dos seus colaboradores, vá em frente! Não vamos mockar algo simplesmente porque nos sentimos obrigados por alguma definição embasada em um paradigma de uma linguagem. Vamos parar com afirmações do tipo: "um teste unitário da classe A não pode acessar a classe B porque não é um teste unitário". Vamos mockar quando acharmos que devemos, baseados no nosso próprio julgamento de forma que os nossos testes ainda sejam úteis e alcancem o seu objetivo: feedback rápido sobre o funcionamento do nosso código.
Aviso: As afirmações e opiniões expressas neste artigo são de responsabilidade de quem o assina, e não necessariamente refletem as posições da Thoughtworks.