Testes melhores e stubs em JavaScript

(publicado originalmente em inglês no Blog da Toggl)

É comum ter de escrever aplicações ou bibliotecas com dependências completamente alheias ao seu código. Seja um banco-de-dados, um web-service, uma ferramenta da linha de comando ou mesmo timers, de repente seus testes passam vários segundos esperando por respostas ou não têm comportamento estritamente definido. Seria ótimo se pudéssemos definir o comportamento de terceiros programaticamente. Ao invés de interagir com eles de fato, apenas testar nossas interações.

A solução para esse problema existe e é muito simples: usar stubs. Saber as usar fará com que seus testes sejam mais fáceis de escrever, previsíveis e muito mais rápidos.

Nesse post, vou explicar o que são stubs por meio de exemplos, uma implementação simples de um stub, o uso do sinon.js com o mocha e meu projeto mocha-make-stub.

O que são stubs? Um exemplo usando web-scraping.

Imagine que estou escrevendo um programa que extrai a principal manchete no site da Folha de S. Paulo. Hoje, a estrutura da manchete de capa é mais ou menos assim:

Há dias em que a manchete de capa está dentro de uma section .main-headline e nós podemos usar o Internet Archive se quiséssemos fazer do código mais confiável. Mas essencialmente, queremos realizar duas tarefas:

Em um projeto de verdade, nós escreveríamos funções separadas para cada uma das tarefas, mas hoje, nós decidimos escrever tudo em uma função. Isso é para fazer o exemplo ilustrativo, mas acontece frequentemente. Mesmo que as tarefas estejam separadas, a orquestração dessas tarefas também devem ser testada.

Na pior das hipóteses, estou mostrando técnicas que você pode aplicar a código que não esteja perfeitamente fatorado.

Nossa função:

O código acima usa duas bibliotecas que devem fazer parte do repertório de muitos programadores que trabalham com Node.js: superagent e cheerio. Primeiro, usamos request.get para baixar a página e em seguida usamos a sintaxe de jQuery do cheerio.load(…) para extrair o trecho desejado da página.

Queremos testar nosso código com o mocha então escrevemos o seguinte teste:

Aqui, nós usamos o módulo built-in assert do Node.js para testar nossa função.

Mas o que acontece se estivermos offline? Ou o site responder com um erro? Nossos testes já são lentos. Esse trecho leva por volta de 100ms consistentemente na minha conexão. Queremos controlar o que é retornado por request.get(…).end(…).

Um stub é uma função que substitui um método em um objeto que queremos controlar. No nosso caso, o objeto é o que request.get(…) retorna: uma instância da classe request.Request. Precisamos de um stub no método request.Request.prototype.end. Assim, quando esse método for chamado, nós teremos controle sobre o que acontece.

Stubs enrolados a mão

Antes de olhar para o sinon.js, um framework escrito especificamente para nos ajudar com esse problema, acho válido mostrar que poderíamos facilmente implementar nossos próprios stubs. Usando os hooks before e after do mocha, podemos substituir o método com uma versão falsa e em seguida o restaurar:

Agora, nossos testes rodam muito mais rápido. Como temos controle da função, podemos testar o que ocorre em um erro:

E assim por diante. Para problemas mais complicados, vai ser interessante saber mais sobre o que aconteceu com o nosso stub. Ele foi chamado? Com quais argumentos? Quantas vezes? Além disso, quando a complexidade crescer, podemos acabar escrevendo código que precisa de muitas stubs e o boilerplate para criar e restaurar elas pode entrar no caminho de escrever testes simples.

Stubs usando Sinon.js

Apesar de que o sinon cobre mais chão do que apenas criar stubs, para nossos propósitos, é a resposta para essas perguntas. Ele expõe helpers para criar e restaurar stubs em objetos e fazer essas asserções relacionadas às chamadas. Aqui está como criar o mesmo stub do primeiro exemplo usando o sinon:

Nesse caso, o primeiro bloco cria o stub em request.Request.prototype com o sinon. O segundo bloco chama .restore no método falso, que o substituí de volta com o método original. Nós podemos usar o objeto retornado pelo sinon.stub ou o método falso request.Request.prototype.end para extrair informações sobre as chamadas.

request.Request.prototype.end.getCalls() por exemplo retorna um Array com informações sobre as chamadas. Podemos ler .thisValue, .args e outros. Eu sugiro "logar" um objeto representando uma chamada e encontrar qual é a informação mais útil para você. Nós também podemos facilmente fazer asserções sobre o método falso ter sido chamado com request.Request.prototype.end.called. Também expostos são: .calledOnce, .calledTwice ou .callCount.

Outra coisa que vale ser mencionada é que métodos para criar funções falsas simples são expostos pelo sinon. Nos podemos encurtar o exemplo acima que só usa um callback e uma String literal com .yields:

Pessoalmente, evito usar esses helpers. Eu não me importo de escrever funções falsas, já que são mais flexíveis e podemos controlar explicitamente o que devem fazer. Ainda assim, o boilerplate de restaurar as funções me incomoda. No contexto do mocha nós poderíamos (por convenção) assumir que os stubs tem o escopo limitado para um único bloco de testes describe e gerar esses hooks before e after.

Eu escrevi um pequeníssimo helper para isso chamado mocha-make-stub. Acho que a motivação principal é mais obrigar o uso dessa convenção do que diminuir o tamanho do código. Aqui está o exemplo usando esse helper:

makeStub chama sinon.stub nos bastidores, mas faz isso dentro de um bloco before. Também adiciona um bloco after para restaurar o método original. Se os testes são escritos usando o helper, você sempre vai ter controle sobre o que os stubs estão fazendo e evita acabar ter uma suite de testes complexa. Um outro pequeno sugar que ele adiciona é .end no objeto de contexto do mocha então as asserções que estávamos escrevendo com request.Request.prototype.end podem ser escritas usando this.end.

O README na sua página do GitHub mostra como nomear esse campo como você quiser usando um parâmetro opcional.

Para mim, tudo isso nos levou a um lugar muito melhor. Os testes rodam rápido, nós estabelecemos uma convenção para criar e destruir stubs e acabamos com todo o boilerplate. Se tudo der certo, você também pode correr para seus testes e começar a remover suas dependências. No pior caso possível, isso vai fazer eles mais rápido. Para mim, há ainda outras possibilidades que stubs abrem.

Quando eu estruturo testes, não me importo sobre dependências no começo; deixo o código interagir com o mundo real. Quando o código e o testes tem estrutura e eu estou feliz com o desenho, eu adiciono stubs para uma dependência de cada vez. Se eu transformar um call de makeStub em um comentário, eu vejo o código rodando exatamente como ele roda em produção, mas em um contexto normal tenho todos os benefícios de stubs.

O helper está no GitHub aqui: https://github.com/yamadapc/mocha-make-stub

@yamadapc