Sinon: Разрешение собственных обещаний ES6 не вызывает обратные вызовы при использовании поддельных таймеров.

Созданный на 23 апр. 2015  ·  30Комментарии  ·  Источник: sinonjs/sinon

В следующем тесте я ожидал, что обратный вызов для разрешенного обещания будет вызван внутри теста. По-видимому, нативные промисы не вызывают обратные вызовы синхронно, а планируют их вызов способом, аналогичным setTimeout(callback, 0) . Однако на самом деле он не использует setTimeout, поэтому реализация поддельных таймеров sinon не запускает обратный вызов при вызове tick() .

describe 'Promise', ->
  beforeEach ->
    <strong i="8">@clock</strong> = sinon.useFakeTimers()
  afterEach ->
    @clock.restore()
    console.log 'teardown'

  it "should invoke callback", ->
    p = new Promise (resolve, reject) ->
      console.log 'resolving'
      resolve(42)
      console.log 'resolved'
    p.then ->
      console.log 'callback'
    @clock.tick()
    console.log "test finished"

Я ожидаю этот вывод:

resolved
callback
test finished
teardown

Вместо этого я получаю это:

resolved
test finished
teardown
callback

Обратный вызов вызывается после завершения теста, поэтому утверждения, основанные на том, что происходит внутри обратного вызова, терпят неудачу.

Не имеет значения, разрешено ли обещание до или после вызова then() .

Самый полезный комментарий

Единственное, что вам нужно, это дождаться выполнения микрозадачи обещания.
Таким образом, следующий подход работает идеально:

const tick = async (ms) => {
  clock.tick(ms);
};

it("imitates promise fulfill when called once", async () => {
    new Promise(resolve => setTimeout(resolve, 400)).then(callback);
    expect(callback.callCount).to.eql(0);

    await tick(200);
    expect(callback.callCount).to.eql(0);

    await tick(200);
    expect(callback.callCount).to.eql(1);
  });

Все 30 Комментарий

Я тоже столкнулся с этой проблемой

Я не думаю, что это можно решить, поскольку спецификация Promise не использует setTimeout , а планирует выполнение новой задачи сразу после текущей.

Вы пытались вернуть обещание из теста? Большинство тестовых библиотек теперь поддерживают промисы, и если функция it() возвращает промис, программа запуска тестов будет ждать, пока промис разрешится или отклонится, прежде чем отметить тест как выполненный.

Да, возврат обещания из теста, вероятно, работает. Но при использовании поддельных таймеров я ожидаю, что смогу пройти тест до возврата из тестовой функции. В этом-то и весь смысл.

Я почти уверен, что это можно сделать, заменив объект Promise, точно так же, как заменяется объект Date. Чтобы сделать это, нам, вероятно, нужна полная реализация спецификации обещания, поскольку мы не сможем полагаться на нативную реализацию.

Я вижу проблему, на которую вы, ребята, указываете здесь. Однако я хотел назвать конкретную ситуацию, с которой я столкнулся, поскольку, основываясь как на спецификации обещания, так и на документации sinon, мне показалось, что это «должно» работать. В этом случае я возвращаю обещание в тест, но использование поддельных таймеров, по-видимому, предотвращает выполнение обещания.

Мне казалось, что оба эти теста должны работать, но первый проходит, а второй не проходит. Я использую chai с плагином chai-as-promised с мокко и получаю сообщение об ошибке «Превышено время ожидания 2000 мс. Убедитесь, что в этом тесте вызывается обратный вызов done()».

describe('chai as promised', function() {

    it('should resolve a promise', function() {
        var promise = new Promise(function( resolve ) {
            setTimeout( resolve, 1000 );
        });
        return expect( promise ).to.have.been.fulfilled;
    });

});

describe('sinon.useFakeTimers()', function() {

    before(function() {
        this.clock = sinon.useFakeTimers();
    });

    after(function() {
        this.clock.restore();
    });

    it('should resolve a promise after ticking', function() {

        var promise = new Promise(function( resolve ) {
            setTimeout( resolve, 1000 );
        });

        this.clock.tick( 1001 );

        return expect( promise ).to.have.been.fulfilled;
    });

});

Я собрал быстрый тестовый проект: https://github.com/JustinLivi/sinon-promises-test .

Восстановление до завершения теста кажется жизнеспособным обходным путем:

describe('sinon.useFakeTimers()', function() {

    before(function() {
        this.clock = sinon.useFakeTimers();
    });

    it('should resolve a promise after ticking', function() {

        var promise = new Promise(function( resolve ) {
            setTimeout( resolve, 1000 );
        });

        this.clock.tick( 1001 );
        this.clock.restore();

        return expect( promise ).to.have.been.fulfilled;
    });

});

Обходной путь, который мы используем, состоит в том, чтобы заменить нативную реализацию обещания чем-то простым, зависящим от setTimeout, при выполнении тестов:

window.Promise = require('promise-polyfill')

Sinon.JS может просто сделать то же самое при вызове useFakeTimers.

@JustinLivi восстановление до завершения сделало это за меня :+1:

Итак, какое-либо решение или обходной путь?
Я не могу изменить свое приложение и перестать использовать обещания ES6, потому что так говорят мои тесты :)

У меня проблема не с нативными промисами, а с полифиллом es6-promise.
Решение clock.restore() от @JustinLivi устраняет проблему.

Понятия не имею, почему эта проблема так долго висит здесь, но это не проблема Синон. Использование промисов по существу выполняет асинхронную логику. Тем не менее, все тесты здесь используют синхронную логику (как действительно упоминает ОП). Таким образом, даже если вы имитируете время, вы все равно полагаетесь на то, что обещание будет выполнено после завершения вашей функции, а это означает, что вам нужно немного изменить свой тест. Обычно это рассматривается в документах большинства тестовых фреймворков ( вот один из Mocha ), но мы обратимся к этому в некоторых статьях с рецептами тестов на грядущем новом сайте.

Итак, немного изменив пример @JustinLivi , мы получим следующий тест:

var sinon = require('sinon');

describe('sinon.useFakeTimers()', function() {

    before(function() {
        this.clock = sinon.useFakeTimers();
    });

    it('should resolve a promise after ticking', function(done) {

        var promise = new Promise(function( resolve ) {
            setTimeout( resolve, 10000 );
        });

        this.clock.tick( 10001 );

        promise
            .then(done) // call done when the promise completes
            .catch(done); // catch any accidental errors
    });

});

На самом деле, если вы используете Mocha, у него есть «сокращенная» версия того же кода, что и выше, при использовании обещаний, если вы просто возвращаете обещание непосредственно в тестовую среду:

it('should resolve a promise after ticking', function() {
    var promise = new Promise(function( resolve ) {
        setTimeout( resolve, 10000 );
    });

    this.clock.tick( 10001 );

    return promise;
});

Это будет

  1. Синхронно выполните функцию executor , отправленную в качестве параметра конструктору Promise (см . спецификацию ES2015 ), эффективно установив новый тайм-аут.
  2. Верните построенное обещание.
  3. Отметьте время.
  4. Активировать функцию тайм-аута, пометив обещание как выполненное
  5. process (или браузер) установит новую «галочку», выполняя промис и вызывая все оставшиеся Thenables

Закрытие как не ошибка.

@ fatso83 Я не понимаю, как вы проходите тесты так, как вы описали. Я добавил ваши примеры тестов в свой тестовый репозиторий, и я все еще получаю timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.

Смотрите здесь: https://github.com/JustinLivi/sinon-promises-test/blob/master/test.js#L60
Кроме того, насколько я могу судить, этот тест по-прежнему не работает: https://github.com/JustinLivi/Sinon.JS/commit/de106e6db2f5cc076b7e3a78635bd9ae2b6be1c2 .

Редактировать: судя по комментарию @fpirsch , кажется, что это просто при использовании полифилла обещания es6? Кто-нибудь пробовал использовать какие-либо альтернативные библиотеки, такие как bluebird?

@ fatso83 Это НЕ проблема return promise .
В моем случае это Phantom (старый, без промисов) + es6-promises polyfill + проблема sinon.js. В современных браузерах обещание разрешается, и тесты выполняются нормально, но с обещанием полифилла обещание никогда не разрешается, когда таймеры подделаны.

@JustinLivi и @fpirsch : этот отчет об ошибке был об использовании _native_ promises. Я вижу тестовый проект @JustinLivi с использованием es6-promise , поэтому не могу за это поручиться. То же самое касается и других полифилов: это другая проблема. Я тестировал это с Node 5 и 6, которые имеют встроенную поддержку промисов.

Скопируйте и вставьте мой код примера из предыдущего поста и запустите его (с предустановленными Mocha и Sinon):

$ pbpaste > test.js

$ mocha test.js

  sinon.useFakeTimers()
    ✓ should resolve a promise after ticking

  1 passing (12ms)

@fpirsch : я никогда не говорил, что это проблема return promise . Я только что упомянул, что он может переписать тест в более короткой форме. Совет, а не исправление. Но вы предоставили некоторую ценную информацию: это работает в нативных браузерах, но не работает в промис-полифиле. Это ошибка вашей обещанной библиотеки, а не Синон. Ваша обещанная библиотека, скорее всего, не кэширует setTimeout и друга и полагается на нее для работы, что нарушает ее. Однажды я реализовал исправление именно для этого в библиотеке pinkyswear , так что неудивительно.

Проверен источник es6-promise , и он не кэширует ссылки на setTimeout , поэтому на него повлияет все, что делает sinon. Но я не понимаю, насколько этот планировщик здесь уместен... Существует также огромный список других опций , которые полифилл попробует, прежде чем вернуться к планировщику тайм-аута. Любой браузер, использующий этот запасной вариант, должен быть древним :)

Но в любом случае, @fpirsch и @JustinLivi упоминают проблемы, связанные с функциональной совместимостью между Sinon и polyfill. И это другой вопрос. Не видя, как Sinon может многое сделать с этим банкоматом (любой PR, скорее всего, закончится в es6-polyfill ), но если это проблема Sinon, не стесняйтесь открывать новую проблему.

PS Я вижу, что совет от @ropez заключался в том, чтобы использовать promise-polyfill , и это действительно наша рекомендация для минимального полифилла, но по причине, противоположной указанной. @mroderick исправил этот проект в январе (через несколько месяцев после комментария @ropez ), чтобы кэшировать ссылку на setTimeout , чтобы обещание разрешалось _независимо от того, что делает Синон_, в глобальную ссылку setTimeout . Подробности смотрите в taylorhakes/promise-polyfill#15.

@fatso83 спасибо за информацию. К сожалению, сохранение ссылки на setTimeout , как это сделал @mroderick для promise-polyfill , в моем случае не работает. Не работает и замена es6-promise на promise-polyfill :-(
РЕДАКТИРОВАТЬ: это происходит в простом стеке фантом + мокко, но не в нашем полном стеке с кармой. Так устал от этого.

@ fatso83 Я привел пример здесь: faketimers
В конце концов, проблема может быть в Карме... Постараюсь углубиться в это.

@fpirsch : я думаю, ты что-то задумал. И вы, и @JustinLivi полагаетесь на карму в качестве тестировщика. Они кажутся очень похожими. PS Я удалил свой комментарий, так как подумал, что он совершенно излишен, поскольку Джастин уже предоставил тестовый пример, который показал проблему, но ваш пример проекта подтверждает гипотезу о том, что это проблема Кармы, так что спасибо!

Это может быть связано, но я не уверен. Почему третий тест ниже не работает? Конкретный сбой - это типичный тайм-аут теста:

     Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.
describe.only('working', () => {
  let clock;
  beforeEach(() => { clock = Sinon.useFakeTimers(); })
  afterEach(() => { clock.restore() })

  const delay = (ms) => (
    new Promise((resolve/* , reject */) => {
      setTimeout(resolve, ms)
    })
  );

  it('1 OK', () => {
    const promise = new Promise((resolve) => {
      setTimeout(resolve, 100)
    })
    clock.tick(200)
    return promise
  })

  it('2 OK', () => {
    const promise = delay(100)
    clock.tick(200)
    return promise
  })

  it('3 FAIL', () => {
    const promise = delay(50).then(() => delay(50))
    clock.tick(200)
    return promise
  })
})

@jasonkuhrt Третий тест терпит неудачу, потому что clock.tick обрабатывает исполнителей синхронно, и поэтому .then(...) не выполняется внутри из-за его асинхронного характера :

onFulfilled или onRejected не должны вызываться до тех пор, пока стек контекста выполнения не будет содержать только код платформы.

@ fatso83 Я не знаю позицию разработчиков lolex в отношении связанных обещаний, которые добавляют таймеры. Текущее ограничение должно быть, по крайней мере, подробно описано в документации.

В зависимости от того, должно ли это поддерживаться или нет, эта проблема может быть открыта повторно (или может быть открыта новая) и зависеть от новой проблемы в lolex .
Я также предполагаю, что если этот вариант использования (цепочка промисов, которые добавляют таймеры) должен быть принят во внимание, это приведет к нарушению модификации API lolex (насколько мне известно, clock.tick должен быть асинхронным).

@gautaz Ах, теперь это имеет смысл. Спасибо за объяснение!

lolex — это библиотека синхронизации, и я не совсем уверен, как решить проблему @jasonkuhrt . Не стесняйтесь предоставлять PR, но модификация clock.tick не очень привлекательна. Я бы предпочел увидеть дополнительный асинхронный метод.

У меня никогда не было особых проблем с промисами и тиканием часов, но это было, вероятно, потому, что я видел, что они были синхронными, и добавлял дополнительные тики часов между ними.

@ fatso83 Вы правы, добавление асинхронного tick было бы менее разрушительным (на самом деле вообще не мешало бы).
Так что что-то вроде asyncTick может подойти. Я посмотрю на это, если я могу найти время.

@gautaz просто любопытно, у вас когда-нибудь получалось поставить патч для lolex ?

@ fatso83 Привет, в прошлом году я открыл ветку на своей стороне, но пока не нашел времени закончить работу.
У меня есть коллеги, которые также столкнулись с той же проблемой, поэтому я могу только надеяться, что проблема станет достаточно серьезной, чтобы предоставить мне дополнительное время.

Изменилось ли за это время что-то в отношении lolex API в этом конкретном вопросе?

Этого не должно быть. Очень мало изменений в кодовой базе за последние полгода. Довольно стабильно.

Есть ли шансы найти здесь простой обходной путь? Есть такая же проблема. У меня есть два обещания, и они разрешаются setTimeout. Мне нужно проверять материал перед любым разрешением, перед вторым, но после первого разрешения и после всех разрешений.

Единственное, что вам нужно, это дождаться выполнения микрозадачи обещания.
Таким образом, следующий подход работает идеально:

const tick = async (ms) => {
  clock.tick(ms);
};

it("imitates promise fulfill when called once", async () => {
    new Promise(resolve => setTimeout(resolve, 400)).then(callback);
    expect(callback.callCount).to.eql(0);

    await tick(200);
    expect(callback.callCount).to.eql(0);

    await tick(200);
    expect(callback.callCount).to.eql(1);
  });

Уловка @jakwuh сработала для меня (узел 8, без транспиляции, собственные промисы), за исключением того, что обратные вызовы then были отложены на тик.

Обновленный комментарий : решение в моем исходном комментарии (ниже) имеет проблемы. Иногда мне нужно было несколько последовательных вызовов await Promise.resolve() , чтобы действительно сбросить все. Вот что-то, что, кажется, работает немного лучше:

beforeEach(function() {
    const originalSetImmediate = setImmediate;
    this.clock = sinon.useFakeTimers();
    this.tickAsync = async ms => {
        this.clock.tick(ms);
        await new Promise(resolve => originalSetImmediate(resolve));
    }
});

afterEach(function() {
    this.clock.restore();
});

Исходный комментарий [ВНИМАНИЕ: код работает не так, как приведенный выше.]

Добавление дополнительного асинхронного пробела к помощнику tick решило проблему для меня:

```js
постоянный тик = асинхронный (мс) => {
часы.тик (мс);
ждать Promise.resolve();
};

Для людей, заинтересованных в работе по улучшению Sinon в этой области (на самом деле, его родственного проекта lolex ), ознакомьтесь с обсуждениями здесь:

Была ли эта страница полезной?
0 / 5 - 0 рейтинги