Allan Larangeiras

Analista de Sistemas




read

Introdução

A primeira vista, projetar qualquer estrutura de testes pode parecer um custo adicional desnecessário no seu projeto. No entanto, com o passar do tempo, tanto projeto como equipe assumem proporções consideráveis aumentando assim o risco de interromper o funcionamento de certas funcionalidades.

Esse risco é muito maior em linguagens interpretadas. Por não ser necessário realizar a construção de todo o projeto antes de executá-lo, é possível que ocorram não apenas inconsistências nas regras de negócio mas até erros de sintaxe.

Outra vantagem dos testes unitários é a adoção do TDD. Durante o desenvolvimento com TDD assume-se um ponto-de-vista externo à funcionalidade construída. Assim, pode-se ter uma noção mais concreta de usabilidade e clareza do seu código.

NodeJS

O NodeJS é um servidor web baseado no motor V8 Runtime do Google Chrome, o qual implementa os conceitos de Non-Blocking IO através de uma única thread encarrecada de monitorar eventos de diversos tipos (Event-Loop).

O conceito de Non-Blocking IO não é novidade e pode ser encontrado em outras linguagens como o Ruby (Event Machine) e Python (Twister e Tornado).

O NodeJS, apesar de implementar um conceito parecido, consegue alcançar números muito mais satisfatórios de desempenho utilizando uma linguagem bem conhecida como o Javascript.

Mas nem tudo são flores, o custo desse desempenho é a necessidade de se aprender um paradigma diferente de programação. Desenvolver em NodeJS se trata basicamente de trabalhar com eventos e callbacks.

O problema de se trabalhar com callbacks e que o desenvolvimento tende a levar ao ‘Callback Hell’, um emaranhado de callbacks aninhados em uma pilha sem fim. Algo similar a este problema já é bem conhecido com as estruturas de decisão e loops.

Esse é mais um motivo para projetar bons testes unitários usando TDD.

Vamos aos testes

Vamos montar uma estrutura de testes no padrão das utilizadas pela maioria dos módulos encontrados no repositório NPM.

Comece criando um diretório chamado exemplo-ut e dentro desse diretório execute o comando abaixo:

npm init

Responda as perguntas do assistente e no final terá um arquivo package.json descrevendo a estrutura inicial do seu projeto. Em seguida criaremos os diretorios lib e test. Dentro do diretório test criaremos o arquivo test.js e dentro do diretório lib criaremos o exemplo.js. Você verá a estrutura abaixo:

exemplo-ut
|-lib/
exemplo.js
|-test/
test.js
package.json

Vamos usar agora o npm para instalar as bibliotecas que usaremos no exemplo. Essas bibliotecas serão usadas apenas em ambiente de desenvolvimento e por esse motivo usaremos o parâmetro –save-dev:

npm install mocha mockery sinon --save-dev

Pra efeitos de praticidade instalaremos o mocha também como dependência global. Assim ficará mais fácil executar os testes:

npm install mocha -g

Iniciando com o Mocha e TDD

Esse artigo não visa ensinar o TDD passo-a-passo, mas basicamente a metodologia propõe o desenvolvimento do teste antes da construção da funcionalidade em um ciclo recorrente de ‘Test Success’ e ‘Test Fail’. Vamos portanto usar essa abordagem nos testes

Abra o arquivo test.js e vamos construir o primeiro teste, na verdade um dummy test, veja abaixo:

// test/test.js

var assert = require('assert');

describe("exemplo-ut", function() {
describe("request", function() {
it('apenas true', function() {
assert.equal(true, true);
});
});
});

Na raiz do seu projeto execute o comando abaixo e observe o resultado:

mocha

Existem alguns parâmetros úteis que podem ser passados ao mocha. Alguns deles são

  • -w que matém o mocha monitorando qualquer alteração nos testes
  • -d para debug
  • -G para exibir notificações growl (muito útil no Linux ou no Mac)

Agora vamos escrever o primeiro teste que faz alguma coisa. Vamos inserir mais um bloco ‘it’ bem abaixo o anterior:

// test/test.js
...
it('chamada ao servico', function() {
exemplo.chamaServico(function(response) {
console.log(response);
});

});
...

Execute o mocha mais uma vez e receberá o seguinte erro:

ReferenceError: exemplo is not defined

Falta importar o módulo para o teste. No começo do arquivo, bem abaixo da definição do módulo assert insira a linha:

var exemplo = require('../lib/exemplo');

Execute o teste novamente.

TypeError: Object #<Object> has no method 'chamaServico'

Muito bem, estamos evoluindo. Agora precisamos criar o método no arquivo exemplo.js:

// lib/exemplo.js

exports.chamaServico = function(callback) {
callback("Hello World");
}

Problema corrigido, vamos modificar o teste para comparar a string retornada:

// test/test.js

...
it('chamada ao servico', function() {
exemplo.chamaServico(function(response) {
assert.equal('Hello World', response);
});

});
...

Execute o teste novamente e você terá a validação do retorno da string. Se estiver em dúvida basta modificar o valor da string esperada e ver o seu teste quebrar.

Vamos testar algo de verdade

Proposta: vamos testar a chamada a este serviço rest da API Facebook Graph. Abra a URL http://graph.facebook.com/nodejs no seu navegador, o retorno deve ser uma representação JSON de uma página no Facebook.

Agora observe abaixo:

// lib/exemplo.js
var http = require('http');

exports.chamaServico = function(callback) {
http.request('http://graph.facebook.com/nodejs', function(response) {
callback("Hello World");
}).end();
}

Legal! Mas eu não deveria testar o retorno do serviço? Sim, e para isso vamos começar a usar eventos. Como a chamada é assíncrona o NodeJS é capaz de recuperar o response bloco por bloco antes mesmo da requisição ser concluída. Podemos usar este recurso para montar o retorno e quando a requisição for concluída retornar esta string para o callback.

Estamos falando de dois momentos/eventos, chamamos de ‘data’ e ‘end’. Observe abaixo:

// lib/exemplo.js
...
exports.chamaServico = function(callback) {
http.request('http://graph.facebook.com/nodejs', function(response) {
var stringResposta = '';

response.on('data', function(data) {
stringResposta += data;

});

response.on('end', function() {
callback(stringResposta);

});
}).end();
}

Rodamos novamente o mocha e sucesso! O teste continua íntegro… Mas por que? Eu mudei a implementação e isso não quebrou o teste.

Isso aconteceu porque agora estamos trabalhando com eventos, o teste simplesmente finaliza antes mesmo do evento ‘end’ ser lançado.

Corrigir esse comportamento é bem simples. Voltamos ao nosso teste:

// test/test.js
...
it('chamada ao servico', function(done) {
exemplo.chamaServico(function(response) {
assert.equal('Hello World', response);
done();
});

});
...

Adicionamos o parâmetro ‘done’ na função do bloco ‘it’, esse parâmetro é uma função de callback do mocha. Quando o teste realmente estiver encerrado, chamamos ‘done()’. Assim o mocha consegue identificar que o teste foi concluído e pode analisar os asserts.

…e o resultado, teste quebrado.

Vamos corrigir o teste de uma maneira bem prática. Primeiro vamos mudar a forma como o nosso método retorna o JSON. Vamos transformá-lo em um objeto:

// lib/exemplo.js
...
response.on('end', function() {
callback(JSON.parse(stringResposta));
});
...

Vamos agora criar uma variável dentro do nosso teste com o conteúdo retornado pela URL do Facebook. Você pode copiar pelo seu próprio browser da maneira que estiver e colar após o sinal de ‘=’.

Não se preocupe, JSON para javascript é como qualquer outro objeto.

// test/test.js

var assert = require('assert');
var exemplo = require('../lib/exemplo');

var retornoFacebook = {"id": "197262326951664",
"about": "Node.js is a platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications.",
"can_post": false,
"category": "Software",
"checkins": 0,
"description": "Evented I/O for V8 JavaScript.\n\nSome usefull links:\nhttp://howtonode.org/\nhttp://nodetuts.com/\nhttp://www.nodebeginner.org/\nhttp://dailyjs.com/2010/11/01/node-tutorial/\n",
"has_added_app": false,
"is_community_page": false,
"is_published": true,
"likes": 1723,
"link": "https://www.facebook.com/nodejs",
"name": "node.JS",
"parking":
{"lot": 0,
"street": 0,
"valet": 0},
"talking_about_count": 11,
"username": "nodejs",
"website": "http://www.nodejs.org\nhttps://github.com/ry/node/wiki",
"were_here_count": 0}
...

Por fim vamos adaptar nosso teste para ser mais preciso, vamos usar o método ‘deepEqual’ da api de assert e comparar o conteúdo da variável ‘retornoFacebook’ com o response.

// test/test.js
...
it('chamada ao servico', function(done) {
exemplo.chamaServico(function(response) {
assert.deepEqual(retornoFacebook, response);
done();
});

});
...

Rodamos uma última vez o mocha e, bacana, teste corrigido.

Agora que sabemos fazer, vamos fazer certo

Construímos nosso teste primeiro, depois criamos a funcionalidade, obtivemos o retorno e o validamos com a API ‘Assert’.

Pergunta: esse teste é realmente unitário? Não.

A função do teste unitário e validar um componente do seu sistema, isoladamente e livre de qualquer influência externa. Nosso teste está sujeito a vários problemas, alguns deles estão listados abaixo:

  • Queda do serviço (HTTP 500)
  • Variação de massa de dados (O serviço é dinâmico, portanto algum dado diferente do esperado já quebrará o teste)
  • Uma atualização ainda não comtemplada na interface do serviço (sabemos que o Facebook não trabalha assim, mas pode acontecer com qualquer outro…)

Nenhum dos motivos acima significa erro de código ou até problema de design. Portanto, seu teste não deveria quebrar em nenhuma dessas situações.

Usando o Sinon para criar Stubs

Agora a coisa complica um pouco, precisamos começar a investir em algumas refatorações na nossa aplicação e em uma estrutura de testes mais robusta.

O primeiro passo é criar um web server que responda o conteúdo que desejo. Dentro do nosso arquivo de testes posso fazer uso de duas funções do mocha ‘before()’ e ‘after()’.

Observe abaixo:

// test/test.js
...
var http = require('http');
...
var server;

before(function() {
server = http.createServer(function(req, res) {
});

server.listen(3000, function() {
console.log('Iniciando HTTP Server.');
});
});

after(function() {
server.close(function() {
console.log('Finalizando HTTP Server');
});
});
...

Vamos fazer agora o servidor http retornar o JSON que já está dentro do teste.

// test/test.js
...
server = http.createServer(function(req, res) {
res.writeHead(200, { 'content-type': 'application/json' });
res.write(JSON.stringify(retornoFacebook));
res.end();
});
...

Sempre execute o mocha após esses ajustes para verificar se o teste ainda funciona.

Observe que a latência do teste está aumentando. Vamos ficar de olho nisso!

Vamos agora refatorar nossa chamada ao serviço. Vamos criar mais um método que se encarregará de retornar a URL.

// lib/exemplo.js
...
exports.chamaServico = function(callback) {
http.request(this.getUrl(), function(response) {
var stringResposta = '';

response.on('data', function(data) {
stringResposta += data;

});

response.on('end', function() {
callback(JSON.parse(stringResposta));

});
}).end();
}

exports.getUrl = function() {
return 'http://graph.facebook.com/nodejs';
}
...

O teste ainda está ok? Vamos adiante, agora vamos usar o Sinon para criar um Stub.

// test/test.js
...
var sinon = require('sinon');
...
before(function() {
server = http.createServer(function(req, res) {
res.writeHead(200, { 'content-type': 'application/json' });
res.write(JSON.stringify(retornoFacebook));
res.end();
});
server.listen(3000, function() {
console.log('Iniciando HTTP Server.');
});

sinon.stub(exemplo, 'getUrl', function() {
return 'http://localhost:3000/';
});
});
...

Execute o teste novamente e o resultando ainda é um sucesso. A diferença vai ser observada na latência do teste que agora está aceitável pois a chamada é local.

E o Mockery?

Eu ia chegar lá… Observe que o método ‘getUrl’ que criamos se trata de um método utilitário. Por se tratar de um contexto diferente, o ideal é mover este método para um arquivo próprio. Até porque sua aplicação vai crescer, então que cresça de forma saudável.

Observe a refatoração abaixo:

// lib/exemplo.js
var http = require('http');
var util = require('./util');

exports.chamaServico = function(cb) {
http.request(util.getUrl(), function(res) {
var response = '';
res.on('data', function(data) {
response += data;
});
res.on('end', function() {
cb(JSON.parse(response));
});
}).end();

}

// lib/util.js
exports.getUrl = function() {
return 'http://graph.facebook.com/nodejs';
}

Agora a função ‘getUrl’ está no arquivo util.js e este é importado via require no arquivo exemplo.js.

Vamos ajustar nosso teste

// test/test.js
...
var mockery = require('mockery');
var util = require('../lib/util');
...
before(function() {
server = http.createServer(function(req, res) {
res.writeHead(200, { 'content-type': 'application/json' });
res.write(JSON.stringify(retornoFacebook));
res.end();
});
server.listen(3000, function() {
console.log('Iniciando HTTP Server.');
});

var utilMock = sinon.stub(util, 'getUrl', function() {
return 'http://localhost:3000/';
});

mockery.registerMock('../lib/util', utilMock);
});
...

Execute novamente o mocha e observe o resultado.

Referências

NodeJS

NPM

Mockery

Sinon

Mocha

BrazilianHolidays

Blog Logo

Allan Larangeiras


Publicado em

Image
Continuar lendo