在使用Sinon进行测试时,如何干净利落地切换到实时并再次切换回假时间?
考虑到以下
UUT.js
,UUT.test.js
和timerUtils.js
,我想从假时间转变为实时,例如提前承诺决议,然后在假时间继续测试执行。
有了在存根中生成并在测试中解析的 Promise,Promise 列表中的每个 Promise 在假时间中被解析,然后时钟在 for 循环结束时滴答作响,但代码指针不会从UUT 中下一行的存根,其中显示
Invoked ...
日志消息,除非发生以下任一情况:
问题:
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()
的可能性,这将允许在假时间内解决承诺,但我想知道从实时到实时是否可能,当然还有当前行为背后的见解。正如此评论中提到的,
tickAsync
将允许在执行计时器之前进行承诺解析。
通过
await
ing tickAsync
,我们现在可以在存根中切换到实时,并在切换回假时间之前运行任何阻塞代码
欲了解更多见解,此要点包括更新的实现