我正在使用 Sinon 假计时器来测试一些包含计时器的函数。问题是测试的函数在调用计时器之前使用了几个异步调用,我发现很难同步测试用例,以便保证在测试函数调用setTimeout()
后调用clock.tickAsync()
。 我见过其他有类似问题的问题,但它们更多的是关于从测试中设置的计时器以及如何将它们与测试中调用的即时报价同步。 但是如果我们认为我们的函数其他测试是一个黑匣子,我们只知道在执行一些异步调用后,它会调用setTimeout
,我们必须在其上前进时钟以防止计时器挂起,那会是什么好方法?
例: 在这里,foo
是我们想要测试的函数。我们所知道的是,它执行一些异步任务,无论是 I/O 还是其他什么,然后等待 10 秒(我的真实代码实际上有一个循环,每秒检查一次条件,超时为 10 秒)。测试调用foo
,然后将时钟提前 20 秒以触发计时器,但大多数情况下它不起作用,因为对tickAsync()
的调用将在foo
调用setTimeout()
之前:
function foo() {
await something();
await something();
await something();
await something();
await something();
return new Promise(resolve => setTimeout(resolve, 10000));
}
let clock;
describe('Test with unpredictable timer', () => {
beforeEach(() => {
clock = FakeTimers.install();
});
afterEach(() => {
clock.uninstall();
});
it('should advance timer', () => {
const p = foo();
// advance timer
await clock.tickAsync(20000);
return p;
});
});
因此,此测试通常会失败,因为计时器永远不会触发。
我意识到最可靠的方法可能是在调用setTimeout
时通知我的测试,因此我考虑添加一个发送"等待"事件的事件发射器,但这确实使我的生产代码混乱。我还考虑过猴子补丁设置超时(在 Sinon 替换它之后)也发出一个事件。但这也太乱了。 最后,我求助于tickAsync()
调用的循环,确保每个await clock.tickAsync()
都为事件循环提供新的运行,这是有效的,但本质上当然是不可靠的,因为它仍然依赖于任意选择的循环数量等:
function tickSteps(clock, duration, steps) {
for (let i = 0; i < steps; i++) {
await clock.tickAsync(duration / steps);
}
}
例如,这适用于tickSteps(clock, 20000, 100)
,但可能不适用于50
的steps
值,并且显然不是一个可靠的解决方案。
我还考虑在我的测试代码中添加一个"真实"计时器,让它在滴答假时钟之前实际等待 50 "真实"毫秒,但这也被证明是混乱的。
除了重新设计我的代码以将其分解为更小的部分或添加回调/事件之外,Sinon 本身有什么方法可以帮助解决这个问题吗?从本质上讲,我需要的是告诉 Sinon 滴答时钟,但将其推迟到下一个设置的计时器(但我想如果运行时等正在使用计时器,这也可能有问题)。或者,如果有一种方法可以做到"滴答假时钟,但只有在真正的时钟延迟之后"。
是否有解决此问题的首选解决方案,或者我应该放弃并重新设计经过测试的代码?
据我了解的问题(我仍然无法掌握用例,给定已完成的异步函数,将 setTimeout 设置为某个固定时间范围很有用)我会尝试在foo()
之后运行clock.runAll()
。 如文档中所述:
这将运行所有挂起的计时器,直到没有剩余的计时器。如果在执行时添加了新的计时器,它们也将运行。
在您的情况下,稍后安排clock.runAll()
以确保已调用setTimeout
可能会有所帮助。
我想到的另一个解决方案是:
- 向在超时之前调用的
foo
函数添加回调 - 在
foo
的最后一个异步函数上添加一个间谍,以通知测试何时调用超时