使用 sinon.js 在假实时和实时之间切换

问题描述 投票:0回答:1

总结

在使用Sinon进行测试时,如何干净利落地切换到实时并再次切换回假时间?


详情

考虑到以下

UUT.js
UUT.test.js
timerUtils.js
,我想从假时间转变为实时,例如提前承诺决议,然后在假时间继续测试执行。

有了在存根中生成并在测试中解析的 Promise,Promise 列表中的每个 Promise 在假时间中被解析,然后时钟在 for 循环结束时滴答作响,但代码指针不会从UUT 中下一行的存根,其中显示

Invoked ...
日志消息,除非发生以下任一情况:

  • 存根承诺的等待至少在 1 毫秒内超时(这发生在 for 循环开始时,允许每个解析器基于前一个解析器继续前进),在这种情况下,UUT 中的日期设置为又是实时了。
  • UUT Promise 的等待(发生在第 n 个子函数调用中,当我们完成循环并等待 mainFunction 的 Promise 时,该子函数调用将继续),在这种情况下,UUT 中的日期将设置为假时间。

问题:

  • 是否有更好的方法来切换实时和返回实时?
  • 使用
    timerUtils
    时如何确保测试始终设置回假时间?
  • 奖金,如何让
    enableFurtherWait
    在不阻塞承诺的情况下运行?

代码

// UUT.js
const enableFurtherWait = false;
const callCount = 3;

async function subFunction(index) {
  await new Promise((resolve) => setTimeout(resolve, 1000)); // to be stubbed anyway
}

async function mainFunction() {
  for (let i = 1; i <= callCount; i++) {
    // await subFunction(i); // this won't work!
    console.log(`UUT: Invoking subfunction ${i} at ${new Date().toISOString()}`);
    await UUT.subFunction(i);
    console.log(`UUT: Invoked subfunction ${i} at ${new Date().toISOString()}`);
  }
  if (enableFurtherWait) {
    console.log(`UUT: Waiting a couple of seconds after subfunctions at ${new Date().toISOString()}`);
    await new Promise((resolve) => { console.log("Promise started"); setTimeout(resolve, 2000);});  // Wait for 2 seconds
  }
  console.log(`UUT: mainFunction completed at ${new Date().toISOString()}`);
}

export const UUT = {
  mainFunction,
  subFunction
};
// UUT.test.js
import { timerUtils } from './timerUtils.js';
import { UUT } from './UUT.js';
import sinon from 'sinon';
import { expect } from 'chai';

const promiseResolvers = []; 
const useGlobalClock = true;
let clock;
// should always match with UUT.js, always the last call would show the Invoked log in real time
const callCount = 3;

describe('Main Function Test', function() {
    beforeEach(function() {
        if (useGlobalClock) {
          clock = sinon.useFakeTimers();
        } else {
          timerUtils.useFakeTimer();
        }
        sinon.stub(UUT, 'subFunction').callsFake((index) => {
            console.log(`Stub: subFunction ${index} called, ${new Date().toISOString()}`);
            return new Promise((resolve) => {
                promiseResolvers.push(resolve);
            });
        });
    });

    afterEach(function() {
        if (useGlobalClock) {
          clock.restore();
        } else {
          timerUtils.restoreRealTimer();
        }
        promiseResolvers.length = 0;
        UUT.subFunction.restore();
    });

    it('should complete mainFunction with controlled promise resolution', async function() {
        const mainFunctionPromise = UUT.mainFunction();
        for (let i = 1; i <= callCount; i++) {

            if (useGlobalClock) {
              clock.restore()
            } else {
              timerUtils.pauseFakeTimer();
            }
            console.log(`Test: Restored the real time clock`);

            await new Promise(resolve => setTimeout(resolve, 50));

            console.log(`Test: Use fake timers again`)
            if (useGlobalClock) {
              clock = sinon.useFakeTimers();
            } else {
              timerUtils.resumeFakeTimer();
            }

            let rCount = promiseResolvers.length;
            expect(rCount, `Expected ${i} resolvers but received ${rCount}`).to.equal(i);

            console.log(`Test: Resolving subfunction ${i}`);
            if (typeof promiseResolvers[i - 1] === 'function') {
                // Resolve the i-th subfunction's promise
                promiseResolvers[i - 1]();
                console.log(`Test: Resolved subfunction ${i}`)
            } else {
                throw new Error(`Test: Resolver for subfunction ${i} is not a function`);
            }

            console.log(`Test: Advancing fake timer for subfunction ${i}`);
            if (useGlobalClock) {
              clock.tick(1000)
            } else {
              timerUtils.currentClock.tick(1000);
            }
        }

        console.log(`Test: rCount is ${promiseResolvers.length}`)
        console.log('Test: All subfunctions resolved, advancing time for the final wait');
        if (useGlobalClock) {
          clock.tick(2100)
        } else {
          timerUtils.currentClock.tick(2100);
        }

        console.log('Test: awaiting mainFunction promise');
        await mainFunctionPromise;
        console.log('Test: mainFunction should be completed now');
        expect(UUT.subFunction.callCount).to.equal(callCount);
    });
});
// timerUtils.js
import sinon from 'sinon';

export const timerUtils = {
    currentClock: null,
    elapsedFakeTime: 0,

    useFakeTimer: function() {
        console.log('Starting fake timer');
        this.currentClock = sinon.useFakeTimers();
        this.elapsedFakeTime = 0;
    },

    pauseFakeTimer: function() {
        if (this.currentClock) {
            this.elapsedFakeTime = this.currentClock.now;
            console.log('Pausing fake timer at:', this.elapsedFakeTime);
            this.currentClock.restore();
        }
    },

    resumeFakeTimer: function() {
        console.log('Resuming fake timer from:', this.elapsedFakeTime);
        this.currentClock = sinon.useFakeTimers({ now: this.elapsedFakeTime });
    },

    restoreRealTimer: function() {
        if (this.currentClock) {
            console.log('Restoring real timer');
            this.currentClock.restore();
            this.currentClock = null;
        }
    }
};
// output
  Main Function Test
UUT: Invoking subfunction 1 at 1970-01-01T00:00:00.000Z
Stub: subFunction 1 called, 1970-01-01T00:00:00.000Z
Test: Restored the real time clock
Test: Use fake timers again
Test: Resolving subfunction 1
Test: Resolved subfunction 1
Test: Advancing fake timer for subfunction 1
Test: Restored the real time clock
UUT: Invoked subfunction 1 at 2023-12-31T15:36:44.323Z
UUT: Invoking subfunction 2 at 2023-12-31T15:36:44.323Z
Stub: subFunction 2 called, 2023-12-31T15:36:44.323Z
Test: Use fake timers again
Test: Resolving subfunction 2
Test: Resolved subfunction 2
Test: Advancing fake timer for subfunction 2
Test: Restored the real time clock
UUT: Invoked subfunction 2 at 2023-12-31T15:36:44.376Z
UUT: Invoking subfunction 3 at 2023-12-31T15:36:44.376Z
Stub: subFunction 3 called, 2023-12-31T15:36:44.377Z
Test: Use fake timers again
Test: Resolving subfunction 3
Test: Resolved subfunction 3
Test: Advancing fake timer for subfunction 3
Test: rCount is 3
Test: All subfunctions resolved, advancing time for the final wait
Test: awaiting mainFunction promise
UUT: Invoked subfunction 3 at 1970-01-01T00:00:05.000Z
UUT: mainFunction completed at 1970-01-01T00:00:05.000Z
Test: mainFunction should be completed now
    ✔ should complete mainFunction with controlled promise resolution (162ms)

注释

  • 有时我注意到直接使用全局时钟(导致使用假时间的测试)与使用正常的
    timerUtils
    时钟(导致使用真实时间的测试)存在差异。但结果并不总是一致的。因此,
    useGlobalClock
  • 的旗帜
  • 我知道使用
    await Promise.resolve()
    的可能性,这将允许在假时间内解决承诺,但我想知道从实时到实时是否可能,当然还有当前行为背后的见解。
  • 该代码是展示该问题的演示,原始代码库要大得多。
javascript asynchronous testing promise sinon
1个回答
0
投票

正如此评论中提到的,

tickAsync
将允许在执行计时器之前进行承诺解析。

通过

await
ing
tickAsync
,我们现在可以在存根中切换到实时,并在切换回假时间之前运行任何阻塞代码

欲了解更多见解,此要点包括更新的实现

© www.soinside.com 2019 - 2024. All rights reserved.