Skip to content

Commit

Permalink
add Lolex as alternate implementation of Fake Timers
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Feb 1, 2019
1 parent 2780a77 commit e30e4ac
Show file tree
Hide file tree
Showing 29 changed files with 1,143 additions and 55 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,7 @@
- `[jest-jasmine2]` Will now only execute at most 5 concurrent tests _within the same testsuite_ when using `test.concurrent` ([#7770](https://github.com/facebook/jest/pull/7770))
- `[jest-circus]` Same as `[jest-jasmine2]`, only 5 tests will run concurrently by default ([#7770](https://github.com/facebook/jest/pull/7770))
- `[jest-config]` A new `maxConcurrency` option allows to change the number of tests allowed to run concurrently ([#7770](https://github.com/facebook/jest/pull/7770))
- `[jest-util]` Add possibility to use Lolex as implementation of fake timers ([#7776](https://github.com/facebook/jest/pull/7776))

### Fixes

Expand Down
2 changes: 2 additions & 0 deletions docs/Configuration.md
Expand Up @@ -1002,6 +1002,8 @@ Default: `real`

Setting this value to `fake` allows the use of fake timers for functions such as `setTimeout`. Fake timers are useful when a piece of code sets a long timeout that we don't want to wait for in a test.

If the value is `lolex`, Lolex will be used as implementation instead of Jest's own. This will be the default fake implementation in a future major version of Jest.

### `transform` [object<string, string>]

Default: `undefined`
Expand Down
18 changes: 17 additions & 1 deletion docs/JestObjectAPI.md
Expand Up @@ -408,10 +408,12 @@ Restores all mocks back to their original value. Equivalent to calling [`.mockRe

## Mock timers

### `jest.useFakeTimers()`
### `jest.useFakeTimers(string?)`

Instructs Jest to use fake versions of the standard timer functions (`setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`, `nextTick`, `setImmediate` and `clearImmediate`).

If you pass `'lolex'` as argument, Lolex will be used as implementation instead of Jest's own fake timers.

Returns the `jest` object for chaining.

### `jest.useRealTimers()`
Expand All @@ -438,6 +440,8 @@ This is often useful for synchronously executing setTimeouts during a test in or

Exhausts all tasks queued by `setImmediate()`.

> Note: This function is not available when using Lolex as fake timers implementation
### `jest.advanceTimersByTime(msToRun)`

##### renamed in Jest **22.0.0+**
Expand All @@ -464,6 +468,18 @@ This means, if any timers have been scheduled (but have not yet executed), they

Returns the number of fake timers still left to run.

### `.jest.setSystemTime()`

Set the current system time used by fake timers. Simulates a user changing the system clock while your program is running. It affects the current time but it does not in itself cause e.g. timers to fire; they will fire exactly as they would have done without the call to `jest.setSystemTime()`.

> Note: This function is only available when using Lolex as fake timers implementation
### `.jest.getRealSystemTime()`

When mocking time, `Date.now()` will also be mocked. If you for some reason need access to the real current time, you can invoke this function.

> Note: This function is only available when using Lolex as fake timers implementation
## Misc

### `jest.setTimeout(timeout)`
Expand Down
22 changes: 22 additions & 0 deletions e2e/__tests__/lolex.test.js
@@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import runJest from '../runJest';

describe('Lolex as implementation fo fake timers', () => {
it('should be possible to use Lolex from config', () => {
const result = runJest('lolex/from-config');
expect(result.status).toBe(0);
});

it('should be possible to use Lolex from jest-object', () => {
const result = runJest('lolex/from-jest-object');
expect(result.status).toBe(0);
});
});
18 changes: 18 additions & 0 deletions e2e/lolex/from-config/__tests__/test.js
@@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

test('fake timers', () => {
jest.setSystemTime(0);

expect(Date.now()).toBe(0);

jest.setSystemTime(1000);

expect(Date.now()).toBe(1000);
});
6 changes: 6 additions & 0 deletions e2e/lolex/from-config/package.json
@@ -0,0 +1,6 @@
{
"jest": {
"timers": "lolex",
"testEnvironment": "node"
}
}
20 changes: 20 additions & 0 deletions e2e/lolex/from-jest-object/__tests__/test.js
@@ -0,0 +1,20 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

test('fake timers', () => {
jest.useFakeTimers('lolex');

jest.setSystemTime(0);

expect(Date.now()).toBe(0);

jest.setSystemTime(1000);

expect(Date.now()).toBe(1000);
});
5 changes: 5 additions & 0 deletions e2e/lolex/from-jest-object/package.json
@@ -0,0 +1,5 @@
{
"jest": {
"testEnvironment": "node"
}
}
12 changes: 6 additions & 6 deletions e2e/timer-reset-mocks/after-reset-all-mocks/timerAndMock.test.js
Expand Up @@ -4,17 +4,17 @@ describe('timers', () => {
it('should work before calling resetAllMocks', () => {
jest.useFakeTimers();
const f = jest.fn();
setImmediate(() => f());
jest.runAllImmediates();
expect(f.mock.calls.length).toBe(1);
setTimeout(f, 0);
jest.runAllTimers();
expect(f).toHaveBeenCalledTimes(1);
});

it('should not break after calling resetAllMocks', () => {
jest.resetAllMocks();
jest.useFakeTimers();
const f = jest.fn();
setImmediate(() => f());
jest.runAllImmediates();
expect(f.mock.calls.length).toBe(1);
setTimeout(f, 0);
jest.runAllTimers();
expect(f).toHaveBeenCalledTimes(1);
});
});
6 changes: 3 additions & 3 deletions e2e/timer-reset-mocks/with-reset-mocks/timerWithMock.test.js
Expand Up @@ -4,8 +4,8 @@ describe('timers', () => {
it('should work before calling resetAllMocks', () => {
const f = jest.fn();
jest.useFakeTimers();
setImmediate(() => f());
jest.runAllImmediates();
expect(f.mock.calls.length).toBe(1);
setTimeout(f, 0);
jest.runAllTimers();
expect(f).toHaveBeenCalledTimes(1);
});
});
9 changes: 5 additions & 4 deletions examples/timer/__tests__/infinite_timer_game.test.js
Expand Up @@ -5,15 +5,16 @@
jest.useFakeTimers();

it('schedules a 10-second timer after 1 second', () => {
jest.spyOn(global, 'setTimeout');
const infiniteTimerGame = require('../infiniteTimerGame');
const callback = jest.fn();

infiniteTimerGame(callback);

// At this point in time, there should have been a single call to
// setTimeout to schedule the end of the game in 1 second.
expect(setTimeout.mock.calls.length).toBe(1);
expect(setTimeout.mock.calls[0][1]).toBe(1000);
expect(setTimeout).toBeCalledTimes(1);
expect(setTimeout).toHaveBeenNthCalledWith(1, expect.any(Function), 1000);

// Fast forward and exhaust only currently pending timers
// (but not any new timers that get created during that process)
Expand All @@ -24,6 +25,6 @@ it('schedules a 10-second timer after 1 second', () => {

// And it should have created a new timer to start the game over in
// 10 seconds
expect(setTimeout.mock.calls.length).toBe(2);
expect(setTimeout.mock.calls[1][1]).toBe(10000);
expect(setTimeout).toBeCalledTimes(2);
expect(setTimeout).toHaveBeenNthCalledWith(2, expect.any(Function), 10000);
});
11 changes: 7 additions & 4 deletions examples/timer/__tests__/timer_game.test.js
Expand Up @@ -5,12 +5,15 @@
jest.useFakeTimers();

describe('timerGame', () => {
beforeEach(() => {
jest.spyOn(global, 'setTimeout');
});
it('waits 1 second before ending the game', () => {
const timerGame = require('../timerGame');
timerGame();

expect(setTimeout.mock.calls.length).toBe(1);
expect(setTimeout.mock.calls[0][1]).toBe(1000);
expect(setTimeout).toBeCalledTimes(1);
expect(setTimeout).toBeCalledWith(expect.any(Function), 1000);
});

it('calls the callback after 1 second via runAllTimers', () => {
Expand All @@ -27,7 +30,7 @@ describe('timerGame', () => {

// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback.mock.calls.length).toBe(1);
expect(callback).toBeCalledTimes(1);
});

it('calls the callback after 1 second via advanceTimersByTime', () => {
Expand All @@ -44,6 +47,6 @@ describe('timerGame', () => {

// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback.mock.calls.length).toBe(1);
expect(callback).toBeCalledTimes(1);
});
});
50 changes: 50 additions & 0 deletions flow-typed/npm/lolex_v2.x.x.js
@@ -0,0 +1,50 @@
// flow-typed signature: a865cf1b7ee719c2a40b85dc8dccf56c
// flow-typed version: fc3f3a2e99/lolex_v2.x.x/flow_>=v0.64.x

// @flow
declare module 'lolex' {
declare opaque type ImmediateID;
declare type installConfig = {
target?: Object,
now?: number | Date,
toFake?: string[],
loopLimit?: number,
shouldAdvanceTime?: boolean,
advanceTimeDelta?: number,
};
declare type lolex = {
createClock(now?: number, loopLimit?: number): Clock,
install(config?: installConfig): Clock,
timers: Object,
withGlobal(global: Object): lolex,
};
declare type Clock = {
setTimeout: typeof setTimeout;
clearTimeout: typeof clearTimeout;
setInterval: typeof setInterval;
clearInterval: typeof clearInterval;
setImmediate: typeof setImmediate;
clearImmediate: typeof clearImmediate;
requestAnimationFrame: typeof requestAnimationFrame;
cancelAnimationFrame: typeof cancelAnimationFrame;
hrtime: typeof process.hrtime;
nextTick: typeof process.nextTick;
now: number;
performance?: {
now: typeof performance.now,
};
tick(time: number | string): void;
next(): void;
reset(): void;
runAll(): void;
runMicrotasks(): void;
runToFrame(): void;
runToLast(): void;
setSystemTime(now?: number | Date): void;
uninstall(): Object[];
Date: typeof Date;
Performance: typeof Performance;
countTimers(): number;
}
declare module.exports: lolex;
}
Expand Up @@ -51,6 +51,10 @@ const jestAdapter = async (
environment.fakeTimers.useFakeTimers();
}

if (config.timers === 'lolex') {
environment.fakeTimersLolex.useFakeTimers();
}

globals.beforeEach(() => {
if (config.resetModules) {
runtime.resetModules();
Expand Down
12 changes: 11 additions & 1 deletion packages/jest-environment-jsdom/src/index.js
Expand Up @@ -12,13 +12,14 @@ import type {EnvironmentContext} from 'types/Environment';
import type {Global} from 'types/Global';
import type {ModuleMocker} from 'jest-mock';

import {FakeTimers, installCommonGlobals} from 'jest-util';
import {FakeTimers, FakeTimersLolex, installCommonGlobals} from 'jest-util';
import mock from 'jest-mock';
import {JSDOM, VirtualConsole} from 'jsdom';

class JSDOMEnvironment {
dom: ?Object;
fakeTimers: ?FakeTimers<number>;
fakeTimersLolex: ?FakeTimersLolex;
global: ?Global;
errorEventListener: ?Function;
moduleMocker: ?ModuleMocker;
Expand Down Expand Up @@ -76,6 +77,11 @@ class JSDOMEnvironment {
moduleMocker: this.moduleMocker,
timerConfig,
});

this.fakeTimersLolex = new FakeTimersLolex({
config,
global,
});
}

setup(): Promise<void> {
Expand All @@ -86,6 +92,9 @@ class JSDOMEnvironment {
if (this.fakeTimers) {
this.fakeTimers.dispose();
}
if (this.fakeTimersLolex) {
this.fakeTimersLolex.dispose();
}
if (this.global) {
if (this.errorEventListener) {
this.global.removeEventListener('error', this.errorEventListener);
Expand All @@ -98,6 +107,7 @@ class JSDOMEnvironment {
this.global = null;
this.dom = null;
this.fakeTimers = null;
this.fakeTimersLolex = null;
return Promise.resolve();
}

Expand Down
12 changes: 11 additions & 1 deletion packages/jest-environment-node/src/index.js
Expand Up @@ -13,7 +13,7 @@ import type {Global} from 'types/Global';
import type {ModuleMocker} from 'jest-mock';

import vm from 'vm';
import {FakeTimers, installCommonGlobals} from 'jest-util';
import {FakeTimers, FakeTimersLolex, installCommonGlobals} from 'jest-util';
import mock from 'jest-mock';

type Timer = {|
Expand All @@ -25,6 +25,7 @@ type Timer = {|
class NodeEnvironment {
context: ?vm$Context;
fakeTimers: ?FakeTimers<Timer>;
fakeTimersLolex: ?FakeTimersLolex;
global: ?Global;
moduleMocker: ?ModuleMocker;

Expand Down Expand Up @@ -72,6 +73,11 @@ class NodeEnvironment {
moduleMocker: this.moduleMocker,
timerConfig,
});

this.fakeTimersLolex = new FakeTimersLolex({
config,
global,
});
}

setup(): Promise<void> {
Expand All @@ -82,8 +88,12 @@ class NodeEnvironment {
if (this.fakeTimers) {
this.fakeTimers.dispose();
}
if (this.fakeTimersLolex) {
this.fakeTimersLolex.dispose();
}
this.context = null;
this.fakeTimers = null;
this.fakeTimersLolex = null;
return Promise.resolve();
}

Expand Down
5 changes: 5 additions & 0 deletions packages/jest-jasmine2/src/__tests__/pTimeout.test.js
Expand Up @@ -13,6 +13,11 @@ jest.useFakeTimers();
import pTimeout from '../pTimeout';

describe('pTimeout', () => {
beforeEach(() => {
jest.spyOn(global, 'setTimeout');
jest.spyOn(global, 'clearTimeout');
});

it('calls `clearTimeout` and resolves when `promise` resolves.', async () => {
const onTimeout = jest.fn();
const promise = Promise.resolve();
Expand Down

0 comments on commit e30e4ac

Please sign in to comment.