Comment tester correctement les promesses avec du moka et du chai?

148

Le test suivant se comporte bizarrement:

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

Comment dois-je gérer correctement une promesse rejetée (et la tester)?

Comment dois-je gérer correctement un test échoué (par exemple expect(data.rate).to.have.length(400);:?

Voici l'implémentation que je teste:

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
la source

Réponses:

233

La chose la plus simple à faire serait d'utiliser les promesses intégrées prises en charge par Mocha dans les versions récentes:

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 avec Node moderne et async / await:

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

Puisque cette approche est prometteuse de bout en bout, elle est plus facile à tester et vous n'aurez pas à penser aux cas étranges auxquels vous pensez comme les done()appels impairs partout.

C'est un avantage que Mocha a sur d'autres bibliothèques comme Jasmine pour le moment. Vous voudrez peut-être également vérifier Chai As Promised, ce qui le rendrait encore plus facile (non .then) mais personnellement, je préfère la clarté et la simplicité de la version actuelle

Benjamin Gruenbaum
la source
4
Dans quelle version de Mocha cela a-t-il commencé? J'obtiens une Ensure the done() callback is being called in this testerreur en essayant de faire cela avec mocha 2.2.5.
Scott
14
@Scott ne prend pas de doneparamètre dans le itqui le désirerait.
Benjamin Gruenbaum
2
Cela m'a été très utile. Supprimer le donedans mon itrappel et appeler explicitement return(sur la promesse) dans le rappel est la façon dont je l'ai fait fonctionner, tout comme dans l'extrait de code.
JohnnyCoder
5
Réponse géniale, fonctionne parfaitement. En regardant la documentation, il est là - il est facile de le rater, je suppose. 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
Ayant le même problème que Scott. Je ne passe pas un doneparamètre à l' itappel, et cela se produit toujours ...
43

Comme déjà souligné ici , les nouvelles versions de Mocha sont déjà compatibles avec Promise. Mais comme l'OP a posé des questions spécifiquement sur Chai, il est juste de souligner le chai-as-promisedpaquet qui fournit une syntaxe propre pour tester les promesses:

en utilisant chai-comme-promis

Voici comment vous pouvez utiliser chai-as-promise pour tester les deux resolveet les rejectcas pour une promesse:

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

sans chai-comme-promis

Pour être vraiment clair sur ce qui est testé, voici le même exemple codé sans chai-as-promise:

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'); })
            ;
});
sans peur
la source
5
Le problème avec la deuxième approche est qu'elle catchest invoquée lorsque l'un des expect(s)échecs. Cela donne une fausse impression que la promesse a échoué même si ce n'est pas le cas. Ce n'est que l'attente qui a échoué.
TheCrazyProgrammer
2
Merci de m'avoir dit que je devais appeler Chai.usepour le monter. Je n'avais jamais repris cela dans la documentation dont ils disposaient. | :(
Arcym
3

Voici ma prise:

  • en utilisant async/await
  • pas besoin de modules chai supplémentaires
  • éviter le problème de capture, @TheCrazyProgrammer a souligné ci-dessus

Une fonction de promesse retardée, qui échoue, si un délai de 0 est donné:

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

Le test positif est assez simple. Un échec inattendu (simuler par 500→0) échouera automatiquement au test, car la promesse rejetée s'intensifie.

Le test négatif utilise l'idée try-catch. Cependant: «se plaindre» d'une passe indésirable ne se produit qu'après la clause catch (de cette façon, cela ne finit pas dans la clause catch (), déclenchant d'autres erreurs mais trompeuses.

Pour que cette stratégie fonctionne, il faut renvoyer le test à partir de la clause catch. Si vous ne voulez rien tester d'autre, utilisez un autre bloc it () -.

Frank Nocke
la source
2

Thre est une meilleure solution. Renvoyez simplement l'erreur avec done dans un bloc 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
  }); 
});

ce test échouera avec le message suivant: AssertionError: expected 1 to equal 11

di3
la source