Sinon: 使用假计时器时,解析原生 ES6 承诺不会触发回调

创建于 2015-04-23  ·  30评论  ·  资料来源: sinonjs/sinon

在下面的测试中,我希望在测试中调用已解决的 Promise 的回调。 显然,原生 Promise 不会同步调用回调,而是安排它们以类似于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()之前或之后解决承诺没有区别。

最有用的评论

您唯一需要做的就是等待 promise microtask 执行。
因此,以下方法非常有效:

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 ,而是安排在当前工作之后立即完成一项新工作。

您是否尝试过从测试中返回承诺? 大多数测试库现在都支持 Promise,如果it()函数返回一个 Promise,则测试运行器将等待 Promise 解决或拒绝,然后再将测试记录为已完成。

是的,从测试中返回一个承诺可能有效。 但是当使用假计时器时,我希望能够在从测试功能返回之前完成测试。 这就是重点。

我很确定这可以通过替换 Promise 对象来完成,就像替换 Date 对象一样。 为了能够做到这一点,我们可能需要一个完整的 Promise 规范实现,因为我们将无法依赖本机实现。

我看到你们在这里指出的问题。 然而,我想指出我遇到的具体情况,因为根据 Promise 规范和 sinon 文档,我认为这“应该”有效。 在这种情况下,我将承诺返回给测试,但使用假计时器似乎会阻止承诺解决。

在我看来,这两个测试都应该起作用,但是第一个通过而第二个失败。 我将 chai 与带有 mocha 的 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 的简单方法替换原生 Promise 实现:

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

在调用 useFakeTimers 时,Sinon.JS 可以简单地做同样的事情。

@JustinLivi在完成之前恢复对我来说:+1:

那么,任何解决方案或解决方法?
我不能修改我的应用程序并停止使用 ES6 Promises,因为我的测试是这样说的 :)

我有问题,不是原生承诺,而是 es6-promise polyfill。
@JustinLiviclock.restore()解决方案解决了这个问题。

不知道为什么这个问题在这里徘徊了这么久,但这不是诗乃的问题。 使用 Promises 本质上是执行异步逻辑。 尽管如此,这里的所有测试都使用同步逻辑(正如 OP 确实提到的那样)。 所以即使你在假装时间,你仍然依赖 Promise 在你的函数完成后执行,这意味着你需要稍微改变你的测试。 这通常包含在大多数测试框架的文档中(这里是 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,那么在使用 Promises 时,如果您只是将 Promise 直接返回给测试框架,则它具有上述相同代码的“快捷方式”版本:

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

    this.clock.tick( 10001 );

    return promise;
});

这会

  1. 同步执行作为参数发送到 Promise 构造函数的executor函数(参见ES2015 规范),有效地设置新的超时。
  2. 返回构造的承诺。
  3. 打勾时间。
  4. 触发超时功能,将 promise 标记为已解决
  5. process (或浏览器)将堆叠一个新的“tick”,解决承诺并调用任何剩余的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 promise polyfill 时? 有没有人尝试过任何替代库,如蓝鸟?

@fatso83这不是return promise问题。
就我而言,这是一个 Phantom(旧的,没有承诺)+ es6-promises polyfill + sinon.js 问题。 在现代浏览器中,promise 已解析并且测试运行良好,但使用 promise polyfill 时,promise 永远不会在计时器被伪造时解析。

@JustinLivi@fpirsch :这个错误报告是关于使用 _native_ 承诺的。 我看到@JustinLivi的测试项目使用es6-promise ,所以我不能保证。 其他 polyfill 也是如此:那将是另一个问题。 我已经用 Node 5 和 6 对此进行了测试,它们具有原生的 Promise 支持。

从上一篇文章中复制粘贴我的示例代码并运行它(预先安装了 Mocha 和 Sinon):

$ pbpaste > test.js

$ mocha test.js

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

  1 passing (12ms)

@fpirsch :我从未说过这是return promise问题。 我刚刚提到他可以用更短的形式重写测试。 提示,而不是修复。 但是您确实提供了一些有价值的信息:它在本机浏览器中工作,但在 promise polyfill 中失败。 那是你的承诺库的错,而不是诗乃。 您的承诺库很可能不会缓存setTimeout和朋友,而是依赖它来实现功能,从而中断。 我曾经在pinkyswear库中对此进行了修复,所以难怪。

检查了es6-promise的来源,它没有缓存对 setTimeout 的引用,所以它会受到 sinon 所做的任何事情的影响。 但是我看不到该调度程序在这里是如何相关的……还有一大堆其他选项,polyfill 将在返回到超时调度程序之前尝试。 任何登陆该后备的浏览器都必须是古老的:)

但无论如何, @fpirsch@JustinLivi提到的是关于 Sinon 和 polyfill 之间互操作性的问题。 这是另一个问题。 没有看到 Sinon 如何在 ATM 上做很多事情(任何 PR 更有可能以es6-polyfill结尾),但如果这是一个 Sinon 问题,请随时打开一个新问题。

PS 我看到@ropez的提示是使用promise-polyfill ,这确实也是我们对最小 polyfill 的建议,但与给定的原因相反。 @mroderick在 1 月份( @ropez发表评论后几个月)修补了该项目以缓存对setTimeout的引用,以便承诺将解决_无论 sinon 在做什么_到全局setTimeout引用。 有关详细信息,请参阅 taylorhakes/promise-polyfill#15。

@fatso83感谢您提供的信息。 不幸的是,像@mroderickpromise-polyfill所做的那样保存对setTimeout的引用在我的情况下不起作用。 也不会用es6-promise promise-polyfill :-(
编辑:它在一个简单的 phantom+mocha 堆栈中,但不是在我们完整的 karma 堆栈中。 太累了。

@fatso83我在这里举了一个例子: faketimers
毕竟问题可能出在 Karma 上……我会尝试更深入地研究这个问题。

@fpirsch :我认为您正在做某事。 你和@JustinLivi都依赖 karma 作为测试运行者。 它们看起来非常相似。 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中的新问题。
我还猜想如果必须考虑这个用例(链接添加计时器的承诺),这将导致破坏lolex API 修改(AFAIK clock.tick必须是异步的)。

@gautaz Ah 现在说得通了。 感谢您的解释!

lolex是一个同步库,我不完全确定如何解决@jasonkuhrt的问题。 随意提供 PR,但修改clock.tick不是很有吸引力。 我宁愿看到一个额外的异步方法。

我从来没有对承诺和时钟滴答有太多问题,但这可能是因为我已经看到它们是同步的,并且在它们之间添加了额外的时钟滴答。

@fatso83没错,添加异步tick会减少干扰(实际上根本不会干扰)。
所以像asyncTick这样的东西可能是合适的。 如果我能抽出时间,我会研究一下。

@gautaz只是好奇你是否有机会为lolex提供补丁?

@fatso83嗨,我去年在我身边开了一个分支,但现在没有时间完成这项工作。
我的同事也被同样的问题绊倒了,所以我只能希望这个问题足够大,给我更多的时间。

与此同时,关于 lolex API 在这个特定点上是否发生了一些变化?

它不应该有。 过去半年代码库的变化非常小。 相当稳定。

有没有机会在这里有简单的解决方法? 有同样的问题。 我有两个承诺,它们是由 setTimeout 解决的。 我需要在任何解决之前检查东西,在第二次解决之前但在第一次解决之后,并且在所有解决之后。

您唯一需要做的就是等待 promise microtask 执行。
因此,以下方法非常有效:

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 等级