Build a mock Timer class and add tests for it

This commit is contained in:
Christopher Willis-Ford 2019-02-07 16:03:45 -08:00
parent d1d5b80d55
commit 7f80fe17b3
4 changed files with 189 additions and 1 deletions

View file

@ -90,6 +90,15 @@ class Timer {
timeElapsed () {
return this.nowObj.now() - this.startTime;
}
/**
* Call a handler function after a specified amount of time has elapsed.
* @param {function} handler - function to call after the timeout
* @param {number} timeout - number of milliseconds to delay before calling the handler
*/
setTimeout (handler, timeout) {
global.setTimeout(handler, timeout);
}
}
module.exports = Timer;

121
test/fixtures/mock-timer.js vendored Normal file
View file

@ -0,0 +1,121 @@
/**
* Mimic the Timer class with external control of the "time" value, allowing tests to run more quickly and
* reliably. Multiple instances of this class operate independently: they may report different time values, and
* advancing one timer will not trigger timeouts set on another.
*/
class MockTimer {
/**
* Creates an instance of MockTimer.
* @param {*} [nowObj=null] - alert the caller that this parameter, supported by Timer, is not supported here.
* @memberof MockTimer
*/
constructor (nowObj = null) {
if (nowObj) {
throw new Error('nowObj is not implemented in MockTimer');
}
/**
* The fake "current time" value, in epoch milliseconds.
* @type {number}
*/
this._mockTime = 0;
/**
* Used to store the start time of a timer action.
* Updated when calling `timer.start`.
* @type {number}
*/
this.startTime = 0;
/**
* Array of pending timeout callbacks
* @type {Array.<Object>}
*/
this._timeouts = [];
}
/**
* Advance this MockTimer's idea of "current time", running timeout handlers if appropriate.
* @param {number} delta - the amount of time to add to the current mock time value, in milliseconds.
* @memberof MockTimer
*/
advanceMockTime (delta) {
if (delta < 0) {
throw new Error('Time may not move backward');
}
this._mockTime += delta;
this._runTimeouts();
}
/**
* @returns {number} - current mock time elapsed since 1 January 1970 00:00:00 UTC.
* @memberof MockTimer
*/
time () {
return this._mockTime;
}
/**
* Returns a time accurate relative to other times produced by this function.
* @returns {number} ms-scale accurate time relative to other relative times.
* @memberof MockTimer
*/
relativeTime () {
return this._mockTime;
}
/**
* Start a timer for measuring elapsed time.
* @memberof MockTimer
*/
start () {
this.startTime = this._mockTime;
}
/**
* @returns {number} - the time elapsed since `start()` was called.
* @memberof MockTimer
*/
timeElapsed () {
return this._mockTime - this.startTime;
}
/**
* Call a handler function after a specified amount of time has elapsed.
* Guaranteed to happen in between "ticks" of JavaScript.
* @param {function} handler - function to call after the timeout
* @param {number} timeout - number of milliseconds to delay before calling the handler
* @memberof MockTimer
*/
setTimeout (handler, timeout) {
this._timeouts.push({
time: this._mockTime + timeout,
handler
});
this._runTimeouts();
}
/**
* Run any timeout handlers whose timeouts have expired.
* @memberof MockTimer
*/
_runTimeouts () {
const ready = [];
const waiting = [];
// partition timeout records by whether or not they're ready to call
this._timeouts.forEach(o => {
const isReady = o.time <= this._mockTime;
(isReady ? ready : waiting).push(o);
});
this._timeouts = waiting;
// next tick, call everything that's ready
global.setTimeout(() => {
ready.forEach(o => o.handler());
}, 0);
}
}
module.exports = MockTimer;

View file

@ -0,0 +1,58 @@
const test = require('tap').test;
const MockTimer = require('../fixtures/mock-timer');
test('spec', t => {
const timer = new MockTimer();
t.type(MockTimer, 'function');
t.type(timer, 'object');
t.type(timer.startTime, 'number');
t.type(timer.time, 'function');
t.type(timer.start, 'function');
t.type(timer.timeElapsed, 'function');
t.end();
});
test('time', t => {
const timer = new MockTimer();
const time1 = timer.time();
const time2 = timer.time();
timer.advanceMockTime(1);
const time3 = timer.time();
t.ok(time1 === time2);
t.ok(time2 < time3);
t.end();
});
test('start / timeElapsed', t => {
const timer = new MockTimer();
const halfDelay = 50;
const fullDelay = halfDelay + halfDelay;
timer.start();
let timeoutCalled = 0;
// Wait and measure timer
timer.setTimeout(() => {
t.ok(timeoutCalled === 0);
++timeoutCalled;
const timeElapsed = timer.timeElapsed();
t.ok(timeElapsed === fullDelay);
t.end();
}, fullDelay);
// this should not call the callback
timer.advanceMockTime(halfDelay);
t.ok(timeoutCalled === 0);
// this should call the callback
timer.advanceMockTime(halfDelay);
});

View file

@ -32,7 +32,7 @@ test('start / timeElapsed', t => {
timer.start();
// Wait and measure timer
setTimeout(() => {
timer.setTimeout(() => {
const timeElapsed = timer.timeElapsed();
t.ok(timeElapsed >= 0);
t.ok(timeElapsed >= (delay - threshold) &&