Jak prawidłowo przetestować obietnice z mokką i herbatą?

148

Poniższy test zachowuje się dziwnie:

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

Jak mam odpowiednio obsłużyć odrzuconą obietnicę (i przetestować ją)?

Jak mam poprawnie obsłużyć nieudany test (np .: expect(data.rate).to.have.length(400);?

Oto implementacja, którą testuję:

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
źródło

Odpowiedzi:

233

Najłatwiej byłoby skorzystać z wbudowanej obsługi obietnic, którą Mocha ma w najnowszych wersjach:

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

Lub z nowoczesnym Node i 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);
});

Ponieważ takie podejście obiecuje koniec, jest łatwiejsze do przetestowania i nie będziesz musiał myśleć o dziwnych przypadkach, o których myślisz, takich jak dziwne done()rozmowy wszędzie.

Jest to obecnie przewaga Mocha nad innymi bibliotekami, takimi jak Jasmine. Możesz również sprawdzić Chai zgodnie z obietnicą, co jeszcze bardziej ułatwiłoby (nie .then), ale osobiście wolę przejrzystość i prostotę obecnej wersji

Benjamin Gruenbaum
źródło
4
W jakiej wersji Mocha to się zaczęło? Pojawia się Ensure the done() callback is being called in this testbłąd, gdy próbuje to zrobić z mocha 2.2.5.
Scott
14
@Scott nie przyjmuje doneparametru w, itktóry by z niego zrezygnował.
Benjamin Gruenbaum
2
To było dla mnie bardzo pomocne. Usunięcie donew moim itwywołaniu zwrotnym i jawne wywołanie return(zgodnie z obietnicą) w wywołaniu zwrotnym jest sposobem, w jaki to działa, podobnie jak we fragmencie kodu.
JohnnyCoder,
5
Świetna odpowiedź, działa idealnie. Patrząc wstecz na dokumenty, jest tam - po prostu łatwo go przeoczyć. 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
Mając ten sam problem co Scott. Nie przekazuję doneparametru do itwywołania, a to nadal się dzieje ...
43

Jak już wspomniano tutaj , nowsze wersje są już Mocha Obietnica świadomy. Ale ponieważ OP zapytał konkretnie o Chai, uczciwe jest wskazanie chai-as-promisedpakietu, który zapewnia czystą składnię do testowania obietnic:

używając chai-zgodnie z obietnicą

Oto, jak możesz użyć chai-zgodnie z obietnicą, aby przetestować obie resolvei rejectprzypadki dla Obietnicy:

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

bez herbaty - zgodnie z obietnicą

Aby naprawdę jasno określić, co jest testowane, oto ten sam przykład zakodowany bez chai-zgodnie z obietnicą:

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'); })
            ;
});
nieustraszony głupiec
źródło
5
Problem z drugim podejściem polega na tym, że catchjest wywoływany, gdy jedno z expect(s)niepowodzeń. Daje to mylne wrażenie, że obietnica zawiodła, chociaż tak się nie stało. Tylko oczekiwanie zawiodło.
TheCrazyProgrammer
2
Do diabła, dzięki, że powiedziałeś mi, że muszę zadzwonić, Chai.useżeby go zamontować. Nigdy bym tego nie wyłowił z dokumentacji, którą mieli. | :(
Arcym
3

Oto moja opinia:

  • za pomocą async/await
  • nie potrzebuje dodatkowych modułów chai
  • Aby uniknąć problemu ze złapaniem, @TheCrazyProgrammer wskazał powyżej

Funkcja opóźnionej obietnicy, która kończy się niepowodzeniem, jeśli zostanie podane opóźnienie równe 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')
})

Pozytywny test jest raczej prosty. Niespodziewana awaria (zasymulowana przez 500→0) automatycznie nie przejdzie testu, ponieważ odrzucona obietnica eskaluje.

Test negatywny wykorzystuje ideę try-catch. Jednak: „narzekanie” na niepożądany przebieg ma miejsce dopiero po klauzuli catch (w ten sposób nie kończy się w klauzuli catch (), wywołując dalsze, ale wprowadzające w błąd błędy.

Aby ta strategia zadziałała, należy zwrócić test z klauzuli catch. Jeśli nie chcesz testować niczego innego, użyj innego it () - block.

Frank Nocke
źródło
2

To lepsze rozwiązanie. Po prostu zwróć błąd z wykonanym w bloku 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
  }); 
});

ten test zakończy się niepowodzeniem i zostanie wyświetlony następujący komunikat: AssertionError: expected 1 to equal 11

di3
źródło