Come testare correttamente le promesse con moka e chai?


148

Il seguente test si sta comportando in modo strano:

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);
        })
});

Come devo gestire correttamente una promessa respinta (e testarla)?

Come devo gestire correttamente un test fallito (es .: expect(data.rate).to.have.length(400);?

Ecco l'implementazione che sto 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
    });
};

Risposte:


233

La cosa più semplice da fare sarebbe utilizzare il supporto integrato promesse che Mocha ha nelle ultime versioni:

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
});

O con Nodo moderno e asincrono / attendono:

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);
});

Poiché questo approccio promette da capo a capo, è più facile testarlo e non dovrai pensare agli strani casi a cui stai pensando come alle strane done()chiamate dappertutto.

Questo è un vantaggio che Mocha ha al momento rispetto ad altre biblioteche come Jasmine. Potresti anche voler controllare Chai come promesso che renderebbe ancora più semplice (no .then) ma personalmente preferisco la chiarezza e la semplicità della versione attuale


4
In quale versione di Mocha è iniziata? Ottengo un Ensure the done() callback is being called in this testerrore quando provo a farlo con mocha 2.2.5.
Scott,

14
@Scott non accetta un doneparametro nel itche lo annullerebbe.
Benjamin Gruenbaum,

2
Questo mi è stato molto utile. Rimuovere il donemio itcallback e chiamare esplicitamente return(sulla promessa) nel callback è come l'ho fatto funzionare, proprio come nello snippet di codice.
JohnnyCoder,

5
Risposta eccezionale, funziona perfettamente. Guardando indietro ai documenti, è lì - credo sia facile da perdere. 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
Avere lo stesso problema di Scott. Non sto passando un doneparametro alla itchiamata e questo sta ancora accadendo ...

43

Come già sottolineato qui , le versioni più recenti di Mocha sono già consapevoli di Promise. Ma dal momento che l'OP ha chiesto specificamente di Chai, è giusto sottolineare il chai-as-promisedpacchetto che fornisce una sintassi chiara per le promesse di test:

usando il chai come promesso

Ecco come è possibile utilizzare chai-as-promesso di prova sia resolvee rejectcasi di una 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');
});

senza chai come promesso

Per chiarire davvero cosa viene testato, ecco lo stesso esempio codificato senza chai come promesso:

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'); })
            ;
});

5
Il problema con il secondo approccio è che catchviene invocato quando uno dei expect(s)fallimenti. Questo dà un'impressione sbagliata che la promessa sia fallita, anche se non è stata così. È solo l'aspettativa che abbia fallito.
TheCrazyProgrammer

2
Cavolo, grazie per avermi detto che devo chiamare Chai.useper montarlo. Non l'avrei mai preso dalla documentazione che avevano. | :(
Arcym,

3

Ecco la mia opinione:

  • utilizzando async/await
  • non necessitano di moduli chai extra
  • evitando il problema della cattura, @TheCrazyProgrammer ha sottolineato sopra

Una funzione di promessa ritardata, che fallisce, se viene dato un ritardo di 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')
})

Il test positivo è piuttosto semplice. Il fallimento imprevisto (simulazione da 500→0) fallirà automaticamente il test, poiché la promessa rifiutata aumenta.

Il test negativo utilizza l'idea try-catch. Tuttavia: "lamentarsi" di un passaggio indesiderato si verifica solo dopo la clausola catch (in questo modo, non finisce nella clausola catch (), innescando ulteriori ma fuorvianti errori.

Affinché questa strategia funzioni, è necessario restituire il test dalla clausola catch. Se non vuoi testare nient'altro, usa un altro blocco it () -.


2

C'è una soluzione migliore. Basta restituire l'errore con fatto in un blocco 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
  }); 
});

questo test fallirà con il seguente messaggio: AssertionError: expected 1 to equal 11

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.