CARACTERÍSTICAS DE TESTES EFICIENTES
- Devem ser relevantes do ponto de vista do produto
- Devem ser reproduzíveis, ou seja, devem retornar sempre o mesmo resultado caso o código não tenha sido modificado.
- Devem ser rápidos já que um dos propósitos dos testes é prover um feedback rápido.
- Devem estar isolados. Se um teste falhar deve ser por que a feature a qual ele verifica está com problemas. Quando um teste está isolado ele permite que seja possível diagnosticar mais facilmente o problema.
TEST DOUBLES
Um dos objetivos do teste unitário é isolar o código (a unidade) que está sendo testado de dependências externas. 'Doubles' são objetos que substituem essas dependências de forma que o teste avalie apenas o código em questão. Doubles podem ser classificados nos seguintes tipos:
- Fake: É uma versão simplificada da dependência.
- Stub: Um objeto que retorna um valor pré-definidos quando um de seus métodos é chamado com um conjunto específico de parâmetros. Podem ser pensados como respostas 'hard-coded'.
- Spy: São objetos que 'gravam' as interações com a unidade (o código sendo testado). Dessa forma pode-se avaliar posteriormente o que aconteceu com o spy durante a execução.
- Mock: É um spy que pode se 'auto-avaliar'. Na sua criação é definido um conjunto de expectativas que são avaliadas na fase de asserção do teste.
MOCHA
- É um framework para criação de testes que pode ser executado em Node ou no browser
- Instale o módulo com npm install --save mocha. O exemplo abaixo demonstra como adicionar um script "test" no package.json que pode ser executado com npm run-script test (ou simplesmente npm test).
- Sobre as opções do comando mocha -u bdd -R spec -t 500 --recursive
- -u: define qual interface de teste que deve ser utilizada. A interface de teste é um conjunto de funções que serão utilizadas para escrever os testes. O valor default é bdd.
- -r: define o tipo de relatório que será gerado
- -t: quantos milissegundo para que o Mocha aguardará até os testes terminarem
- Exemplo de um teste simples (que falha)
- No exemplo abaixo uma função simples (validator) está sendo testada. A palavra-chave describe define uma suite de testes e it define um caso de teste específico. Dentro de um caso de teste são feitas asserções, neste caso com o módulo assert, disponível por default no Node
CHAI
- Um substituto para o módulo default 'assert' que é mais completo e fácil de utilizar.
- O Chai provê 3 tipos de asserção: expect, should e assert.
- Observe que o Chai é apenas uma library de asserções e não um framework de testes como o Mocha. No exemplo abaixo demonstra-se como utilizar uma asserção do Chai em um teste do Mocha:
- O resultado da unidade sendo testada é encapsulada pela função expect, Este função retorna:
- Chains: elementos que não modificam o comportamento das asserções mas que provêm expressividade -- pode-se adicionar um "." para encadear com outros chains.
- Assertions: elementos que executam a avaliação do resultado.
- Flags: permitem que o comportamento da asserção seja modificado.
- No exemplo abaixo utiliza-se diferentes valores de input para um mesma lógica de teste -- esta técnica é chamada de triangulação:
- Testes parametrizados podem executar o mesmo teste com diferentes inputs:
BEFORE, BEFORE EACH, AFTER, AFTER EACH
- A função beforeEach é executada sempre antes de cada it dentro de um determinado escopo.
- A função before será executada uma única vez antes de todos os testes. No exemplo, isto implica que apenas uma única instância do objeto validator será compartilhada por todos os testes. Normalmente se utiliza before quando é necessário alguma preparação custosa como configuração em um banco de dados ou a inicialização de um servidor.
- Analogamente as funções afterEach e after são chamadas, respectivamente, sempre, depois de cada teste e uma única vez depois de todos os testes.
CONTEXT
-
Utilize o context para agrupar testes que utilizam um setup comum e descreva o contexto deles de forma descritiva.
-
Como regra geral context é utilizado para definir cenários, describe para definir features e ações e it para implementar as asserções.
SINON
- Ao escrever um teste desejamos que ele esteja isolado de suas dependências. Por exemplo, suponha que o código sendo testado faça uma chamada Ajax -- se a chamada real precisar ser executada durante o teste então o teste fica dependente da API externa que está sendo chamada. O Sinon facilita a criação de substitutos para as dependências:
- Spies, que possuem informações sobre chamadas a métodos sem que afetem seu comportamento.
- Stubs, são como spies mas substituem completamente a função. Isto permite que uma função seja substituída por um stub que tem um comportamento pré-definido como retornar um valor específico ou uma exceção.
- Mocks, permitem substituir um objeto de forma mais fácil combinando spies e stubs.
SPIES
- A principal funcionalidade de um spy é obter informação sobre a chamada de funções. No exemplo veja que o objeto Spy contém informações sobre quais argumentos forem utilizados em sua primeira chamada (spy.firstCall.args)
var spy = sinon.spy();
// We can call a spy like a function
spy('Hello', 'World');
//Now we can get information about the call
console.log(spy.firstCall.args); //output: ['Hello', 'World']
- No exemplo abaixo 'espia-se' o método setName de user -- veja como é possível verificar quantas vezes ele foi chamado com setNameSpy.callCount
var user = {
...
setName: function(name){
this.name = name;
}
}
//Create a spy for the setName function
var setNameSpy = sinon.spy(user, 'setName');
//Now, any time we call the function, the spy logs information about it
user.setName('Darth Vader');
//Which we can see by looking at the spy object
console.log(setNameSpy.callCount); //output: 1
//Important final step - remove the spy
setNameSpy.restore();
STUBS
- Quando utilizamos um spy a função real ainda é executada -- com um stub ela é completamente substituída:
var stub = sinon.stub();
stub('hello');
console.log(stub.firstCall.args); //output: ['hello']
- No exemplo abaixo, substitui-se o método saveUser (que faz uma chamada Ajax) por um stub -- dessa forma é removida a dependência ao servidor externo:
/* FUNÇÃO A SER TESTADA*/
function saveUser(user, callback) {
$.post('/users', {
first: user.firstname,
last: user.lastname
}, callback);
}
describe('saveUser', function() {
it('should call callback after saving', function() {
//We'll stub $.post so a request is not sent
var post = sinon.stub($, 'post');
post.yields();
//We can use a spy as the callback so it's easy to verify
var callback = sinon.spy();
saveUser({ firstname: 'Han', lastname: 'Solo' }, callback);
post.restore();
sinon.assert.calledOnce(callback);
});
});
- Outro uso comum de stubs é verificar se uma função foi chamada com um conjunto específico de argumentos. No exemplo abaixo o método 'post' do jQuery ($) é avaliado -- é verificado se o método foi chamado com a url e parâmetros definidos anteriormente com sinon.assert.calledWith(post, expectedUrl, expectedParams)
describe('saveUser', function() {
it('should send correct parameters to the expected URL', function() {
//We'll stub $.post same as before
var post = sinon.stub($, 'post');
//We'll set up some variables to contain the expected results
var expectedUrl = '/users';
var expectedParams = {
first: 'Expected first name',
last: 'Expected last name'
};
//We can also set up the user we'll save based on the expected data
var user = {
firstname: expectedParams.first,
lastname: expectedParams.last
}
saveUser(user, function(){} );
post.restore();
sinon.assert.calledWith(post, expectedUrl, expectedParams);
});
});
MOCKS
- Normalmente utilizados quando se precisa fazer o stub de mais de uma função em um único objeto. Se apenas uma função precisa ser substituída um stub é mais fácil de usar.
- Mocks podem fazer suas próprias asserções. Define-se as expectativas no próprio objeto mock que são verificadas ao final do teste.
describe('incrementStoredData', function() {
it('should increment stored value by one', function() {
var storeMock = sinon.mock(store);
storeMock.expects('get').withArgs('data').returns(0);
storeMock.expects('set').once().withArgs('data', 1);
incrementStoredData();
storeMock.restore();
storeMock.verify();
});
});
- Encapsule o teste com o método sinon.test() -- dessa forma não é necessário utilizar o método restore() ao final (que restaura o estado do stub ou mock de forma a não influenciar os demais testes)
it('should do something with stubs', sinon.test(function() {
var stub = this.stub($, 'post');
doSomething();
sinon.assert.calledOnce(stub);
});