Skip to content

Commit

Permalink
Add mocks to NodeJS "timers" module (#467)
Browse files Browse the repository at this point in the history
* add tests

* implement mocking timers module

* improve tests

* move tests to fake-timers-test.js

* rename timersModuleMocks to timersModuleMethods

* Add notes about touching NodeJS timers module to README.md
  • Loading branch information
swenzel-arc committed May 18, 2023
1 parent dbdf02a commit a111a71
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 2 deletions.
5 changes: 4 additions & 1 deletion README.md
Expand Up @@ -67,6 +67,7 @@ clock instance, not the browser's internals.

Calling `install` with no arguments achieves this. You can call `uninstall`
later to restore things as they were again.
Note that in NodeJS also the [timers](https://nodejs.org/api/timers.html) module will receive fake timers when using global scope.

```js
// In the browser distribution, a global `FakeTimers` is already available
Expand Down Expand Up @@ -146,7 +147,9 @@ The `loopLimit` argument sets the maximum number of timers that will be run when

### `var clock = FakeTimers.install([config])`

Installs FakeTimers using the specified config (otherwise with epoch `0` on the global scope). The following configuration options are available
Installs FakeTimers using the specified config (otherwise with epoch `0` on the global scope).
Note that in NodeJS also the [timers](https://nodejs.org/api/timers.html) module will receive fake timers when using global scope.
The following configuration options are available

| Parameter | Type | Default | Description |
| -------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Expand Down
31 changes: 30 additions & 1 deletion src/fake-timers-src.js
@@ -1,6 +1,14 @@
"use strict";

const globalObject = require("@sinonjs/commons").global;
let timersModule;
if (typeof require === "function" && typeof module === "object") {
try {
timersModule = require("timers");
} catch (e) {
// ignored
}
}

/**
* @typedef {object} IdleDeadline
Expand Down Expand Up @@ -85,6 +93,7 @@ const globalObject = require("@sinonjs/commons").global;
* @property {function(): void} uninstall Uninstall the clock.
* @property {Function[]} methods - the methods that are faked
* @property {boolean} [shouldClearNativeTimers] inherited from config
* @property {{methodName:string, original:any}[] | undefined} timersModuleMethods
*/
/* eslint-enable jsdoc/require-property-description */

Expand Down Expand Up @@ -886,6 +895,12 @@ function withGlobal(_global) {
}
}
}
if (clock.timersModuleMethods !== undefined) {
for (let j = 0; j < clock.timersModuleMethods.length; j++) {
const entry = clock.timersModuleMethods[j];
timersModule[entry.methodName] = entry.original;
}
}
}

if (config.shouldAdvanceTime === true) {
Expand Down Expand Up @@ -1733,7 +1748,9 @@ function withGlobal(_global) {
);
}
}

if (_global === globalObject && timersModule) {
clock.timersModuleMethods = [];
}
for (i = 0, l = clock.methods.length; i < l; i++) {
const nameOfMethodToReplace = clock.methods[i];
if (nameOfMethodToReplace === "hrtime") {
Expand All @@ -1753,6 +1770,18 @@ function withGlobal(_global) {
} else {
hijackMethod(_global, nameOfMethodToReplace, clock);
}
if (
clock.timersModuleMethods !== undefined &&
timersModule[nameOfMethodToReplace]
) {
const original = timersModule[nameOfMethodToReplace];
clock.timersModuleMethods.push({
methodName: nameOfMethodToReplace,
original: original,
});
timersModule[nameOfMethodToReplace] =
_global[nameOfMethodToReplace];
}
}

return clock;
Expand Down
141 changes: 141 additions & 0 deletions test/fake-timers-test.js
Expand Up @@ -21,6 +21,16 @@ const {
utilPromisifyAvailable,
} = require("./helpers/setup-tests");

var timersModule;

if (typeof require === "function" && typeof module === "object") {
try {
timersModule = require("timers");
} catch (e) {
// ignored
}
}

describe("FakeTimers", function () {
describe("setTimeout", function () {
beforeEach(function () {
Expand Down Expand Up @@ -4664,6 +4674,137 @@ describe("FakeTimers", function () {
assert.isFalse(stub.called);
});
});
describe("Node timers module", function () {
before(function () {
if (!timersModule) {
this.skip();
}
});

/**
* Returns elements that are present in both lists.
*
* @function
* @template E
* @param {E[]} [list1]
* @param {E[]} [list2]
* @return {E[]}
*/
function getIntersection(list1, list2) {
return list1.filter((value) => list2.indexOf(value) !== -1);
}

/**
* Get property names and original values from timers module.
*
* @function
* @param {string[]} [toFake]
* @return {{propertyName: string, originalValue: any}[]}
*/
function getOriginals(toFake) {
return toFake.map((propertyName) => ({
propertyName,
originalValue: timersModule[propertyName],
}));
}

afterEach(function () {
if (this.clock) {
this.clock.uninstall();
delete this.clock;
}
});

it("should install all timers", function () {
const toFake = getIntersection(
Object.getOwnPropertyNames(timersModule),
Object.getOwnPropertyNames(FakeTimers.timers)
);
const originals = getOriginals(toFake);

this.clock = FakeTimers.install();

for (const { propertyName, originalValue } of originals) {
refute.same(timersModule[propertyName], originalValue);
}
});

it("should uninstall all timers", function () {
const toFake = getIntersection(
Object.getOwnPropertyNames(timersModule),
Object.getOwnPropertyNames(FakeTimers.timers)
);
const originals = getOriginals(toFake);

this.clock = FakeTimers.install();
this.clock.uninstall();

for (const { propertyName, originalValue } of originals) {
assert.same(timersModule[propertyName], originalValue);
}
});

it("should have synchronized clock with globalObject", function () {
this.clock = FakeTimers.install();

const globalStub = sinon.stub();
const timersStub = sinon.stub();

timersModule.setTimeout(timersStub, 5);
setTimeout(globalStub, 5);
this.clock.tick(5);
assert(globalStub.calledOnce);
assert(timersStub.calledOnce);
});

it("fakes and resets provided methods", function () {
const toFake = ["setTimeout", "Date"];
const originals = getOriginals(toFake);
this.clock = FakeTimers.install({ toFake });

for (const { propertyName, originalValue } of originals) {
if (originalValue === undefined) {
assert.same(timersModule[propertyName], originalValue);
} else {
refute.same(timersModule[propertyName], originalValue);
}
}
});

it("resets faked methods", function () {
const toFake = ["setTimeout", "Date"];
const originals = getOriginals(toFake);

this.clock = FakeTimers.install({ toFake });
this.clock.uninstall();

for (const { propertyName, originalValue } of originals) {
assert.same(timersModule[propertyName], originalValue);
}
});

it("does not fake methods not provided", function () {
const toFake = ["setTimeout", "Date"];
const notToFake = ["clearTimeout", "setInterval", "clearInterval"];
const originals = getOriginals(notToFake);

this.clock = FakeTimers.install({ toFake });

for (const { propertyName, originalValue } of originals) {
assert.same(timersModule[propertyName], originalValue);
}
});

it("does not fake when installing on custom global object", function () {
const original = timersModule.setTimeout;
this.clock = FakeTimers.withGlobal({
Date: Date,
setTimeout: sinon.fake(),
clearTimeout: sinon.fake(),
}).install();
assert.same(timersModule.setTimeout, original);
});
});
});

describe("loop limit stack trace", function () {
Expand Down

0 comments on commit a111a71

Please sign in to comment.