Como faço para testar adequadamente as promessas com mocha e chai?

148

O seguinte teste está se comportando de maneira estranha:

it('Should return the exchange rates for btc_ltc', function(done) {
    var pair = 'btc_ltc';

    shapeshift.getRate(pair)
        .then(function(data){
            expect(data.pair).to.equal(pair);
            expect(data.rate).to.have.length(400);
            done();
        })
        .catch(function(err){
            //this should really be `.catch` for a failed request, but
            //instead it looks like chai is picking this up when a test fails
            done(err);
        })
});

Como devo lidar adequadamente com uma promessa rejeitada (e testá-la)?

Como devo lidar adequadamente com um teste que falhou (ou seja expect(data.rate).to.have.length(400);:?

Aqui está a implementação que estou testando:

var requestp = require('request-promise');
var shapeshift = module.exports = {};
var url = 'http://shapeshift.io';

shapeshift.getRate = function(pair){
    return requestp({
        url: url + '/rate/' + pair,
        json: true
    });
};
chovy
fonte

Respostas:

233

A coisa mais fácil a se fazer seria usar o suporte incorporado às promessas do Mocha nas versões recentes:

it('Should return the exchange rates for btc_ltc', function() { // no done
    var pair = 'btc_ltc';
    // note the return
    return shapeshift.getRate(pair).then(function(data){
        expect(data.pair).to.equal(pair);
        expect(data.rate).to.have.length(400);
    });// no catch, it'll figure it out since the promise is rejected
});

Ou com o Node moderno e assíncrono / espera:

it('Should return the exchange rates for btc_ltc', async () => { // no done
    const pair = 'btc_ltc';
    const data = await shapeshift.getRate(pair);
    expect(data.pair).to.equal(pair);
    expect(data.rate).to.have.length(400);
});

Como essa abordagem tem promessas de ponta a ponta, é mais fácil testar e você não terá que pensar nos casos estranhos em que está pensando, como as done()chamadas estranhas em todos os lugares.

Essa é uma vantagem do Mocha sobre outras bibliotecas como o Jasmine no momento. Você também pode verificar o Chai As Promised, o que tornaria ainda mais fácil (não .then), mas pessoalmente eu prefiro a clareza e simplicidade da versão atual

Benjamin Gruenbaum
fonte
4
Em que versão do Mocha isso começou? Eu recebo um Ensure the done() callback is being called in this testerro ao tentar fazer isso com o mocha 2.2.5.
Scott
14
O @Scott não aceita um doneparâmetro no itqual seria desativado.
Benjamin Gruenbaum
2
Isso foi muito útil para mim. Removendo o doneno meu itretorno, e explicitamente chamar return(a promessa) na chamada de retorno é como eu tenho que trabalhar, assim como no trecho de código.
JohnnyCoder
5
Resposta incrível, funciona perfeito. Olhando para os documentos, está lá - é fácil perder, eu acho. Alternately, instead of using the done() callback, you may return a Promise. This is useful if the APIs you are testing return promises instead of taking callbacks:
Federico
4
Tendo o mesmo problema que Scott. Eu não estou passando um doneparâmetro para a itchamada, e isso ainda está acontecendo ...
43

Como já apontado aqui , as versões mais recentes do Mocha já têm reconhecimento de promessa. Mas como o OP perguntou especificamente sobre Chai, é justo apontar o chai-as-promisedpacote que fornece uma sintaxe limpa para as promessas de teste:

usando chai como prometido

Veja como você pode usar chai-as-prometeu teste tanto resolvee rejectcasos de uma promessa:

var chai = require('chai');
var expect = chai.expect;
var chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);

...

it('resolves as promised', function() {
    return expect(Promise.resolve('woof')).to.eventually.equal('woof');
});

it('rejects as promised', function() {
    return expect(Promise.reject('caw')).to.be.rejectedWith('caw');
});

sem chai como prometido

Para deixar bem claro o que está sendo testado, eis o mesmo exemplo codificado sem o chai-conforme-prometido:

it('resolves as promised', function() {
    return Promise.resolve("woof")
        .then(function(m) { expect(m).to.equal('woof'); })
        .catch(function(m) { throw new Error('was not supposed to fail'); })
            ;
});

it('rejects as promised', function() {
    return Promise.reject("caw")
        .then(function(m) { throw new Error('was not supposed to succeed'); })
        .catch(function(m) { expect(m).to.equal('caw'); })
            ;
});
fearless_fool
fonte
5
O problema com a segunda abordagem é que catché invocado quando um dos expect(s)falha. Isso dá uma impressão errada de que a promessa falhou, embora não tenha cumprido. É apenas a expectativa que falhou.
TheCrazyProgrammer
2
Caramba, obrigado por me dizer que tenho que ligar Chai.usepara montá-lo. Eu nunca teria pegado isso na documentação que eles tinham. | :(
Arcym
3

Aqui está a minha opinião:

  • usando async/await
  • não precisando de módulos chai extras
  • evitando o problema de captura, @TheCrazyProgrammer apontado acima

Uma função de promessa atrasada, que falha, se receber um atraso de 0:

const timeoutPromise = (time) => {
    return new Promise((resolve, reject) => {
        if (time === 0)
            reject({ 'message': 'invalid time 0' })
        setTimeout(() => resolve('done', time))
    })
}

//                     ↓ ↓ ↓
it('promise selftest', async () => {

    // positive test
    let r = await timeoutPromise(500)
    assert.equal(r, 'done')

    // negative test
    try {
        await timeoutPromise(0)
        // a failing assert here is a bad idea, since it would lead into the catch clause…
    } catch (err) {
        // optional, check for specific error (or error.type, error. message to contain …)
        assert.deepEqual(err, { 'message': 'invalid time 0' })
        return  // this is important
    }
    assert.isOk(false, 'timeOut must throw')
    log('last')
})

Teste positivo é bastante simples. Falha inesperada (simulação por 500→0) falhará no teste automaticamente, à medida que a promessa rejeitada aumenta.

O teste negativo usa a ideia de tentar pegar. No entanto: 'reclamar' de um passe indesejado ocorre somente após a cláusula catch (dessa forma, ele não termina na cláusula catch (), desencadeando erros adicionais, porém enganosos.

Para que essa estratégia funcione, é preciso retornar o teste da cláusula catch. Se você não quiser testar mais nada, use outro bloco it () -.

Frank Nocke
fonte
2

Há uma solução melhor. Basta retornar o erro com done em um bloco catch.

// ...

it('fail', (done) => {
  // any async call that will return a Promise 
  ajaxJson({})
  .then((req) => {
    expect(1).to.equal(11); //this will throw a error
    done(); //this will resove the test if there is no error
  }).catch((e) => {
    done(e); //this will catch the thrown error
  }); 
});

este teste falhará com a seguinte mensagem: AssertionError: expected 1 to equal 11

di3
fonte