¿Cómo pruebo adecuadamente las promesas con mocha y chai?

148

La siguiente prueba se comporta de manera extraña:

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

¿Cómo debo manejar adecuadamente una promesa rechazada (y probarla)?

¿Cómo debo manejar adecuadamente una prueba fallida (es decir expect(data.rate).to.have.length(400);:?

Aquí está la implementación que estoy probando:

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
fuente

Respuestas:

233

Lo más fácil sería utilizar el soporte de promesas incorporado que Mocha tiene en las versiones recientes:

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 y así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);
});

Dado que este enfoque promete de principio a fin, es más fácil de probar y no tendrá que pensar en los casos extraños en los que está pensando, como las done()llamadas extrañas en todas partes.

Esta es una ventaja que Mocha tiene sobre otras bibliotecas como Jasmine en este momento. Es posible que también desee verificar Chai As Promised, lo que lo haría aún más fácil (no .then), pero personalmente prefiero la claridad y simplicidad de la versión actual

Benjamin Gruenbaum
fuente
44
¿En qué versión de Mocha comenzó esto? Me da un Ensure the done() callback is being called in this testerror al intentar hacer esto con mocha 2.2.5.
Scott
14
@Scott no toma un doneparámetro en el itque optaría por no participar.
Benjamin Gruenbaum
2
Esto fue muy útil para mí. Eliminarlo doneen mi itdevolución de llamada y llamar explícitamente return(en la promesa) en la devolución de llamada es cómo lo hice funcionar, al igual que en el fragmento de código.
JohnnyCoder
55
Impresionante respuesta, funciona perfecto. Mirando hacia atrás en los documentos, está allí, supongo que es fácil perderse. 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
44
Tener el mismo problema que Scott. No estoy pasando un doneparámetro a la itllamada, y esto todavía está sucediendo ...
43

Como ya se señaló aquí , las versiones más nuevas de Mocha ya son compatibles con Promise. Pero dado que el OP preguntó específicamente sobre Chai, es justo señalar el chai-as-promisedpaquete que proporciona una sintaxis limpia para probar las promesas:

usando chai-como-prometió

Aquí le mostramos cómo puede usar chai-as-prometido para probar ambos resolvey los rejectcasos para una promesa:

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

sin chai-como-prometido

Para dejar realmente claro lo que se está probando, aquí está el mismo ejemplo codificado sin chai-como-prometió:

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'); })
            ;
});
temerario
fuente
55
El problema con el segundo enfoque es que catchse invoca cuando uno de los expect(s)errores. Esto da una impresión equivocada de que la promesa falló aunque no lo hizo. Es solo la expectativa que falló.
TheCrazyProgrammer
2
Santo cielo, gracias por decirme que tengo que llamar Chai.usepara montarlo. Nunca lo habría recogido de la documentación que tenían. | :(
Arcym
3

Aquí está mi opinión:

  • utilizando async/await
  • no necesita módulos chai adicionales
  • evitando el problema de captura, @TheCrazyProgrammer señaló anteriormente

Una función de promesa retrasada, que falla, si se le da un retraso 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')
})

La prueba positiva es bastante simple. La falla inesperada (simulación por 500→0) fallará la prueba automáticamente, a medida que la promesa rechazada se intensifique.

La prueba negativa usa la idea try-catch-idea. Sin embargo: 'quejarse' sobre un pase no deseado ocurre solo después de la cláusula catch (de esa manera, no termina en la cláusula catch (), lo que desencadena más errores pero engañosos.

Para que esta estrategia funcione, uno debe devolver la prueba de la cláusula catch. Si no quieres probar nada más, usa otro bloque it ().

Frank Nocke
fuente
2

Hay una mejor solución. Simplemente devuelva el error con hecho en un bloque 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
  }); 
});

esta prueba fallará con el siguiente mensaje: AssertionError: expected 1 to equal 11

di3
fuente