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 Apr 18, 2019
1 parent 5a66bbb commit aa2ecd6
Show file tree
Hide file tree
Showing 28 changed files with 1,119 additions and 38 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -56,6 +56,7 @@

- `[expect]`: Improve report when matcher fails, part 13 ([#8077](https://github.com/facebook/jest/pull/8077))
- `[@jest/core]` Filter API pre-filter setup hook ([#8142](https://github.com/facebook/jest/pull/8142))
- `[@jest/fake-timers]` Add possibility to use Lolex as implementation of fake timers ([#7776](https://github.com/facebook/jest/pull/7776))
- `[jest-snapshot]` Improve report when matcher fails, part 14 ([#8132](https://github.com/facebook/jest/pull/8132))
- `[@jest/reporter]` Display todo and skip test descriptions when verbose is true ([#8038](https://github.com/facebook/jest/pull/8038))
- `[jest-runner]` Support default exports for test environments ([#8163](https://github.com/facebook/jest/pull/8163))
Expand Down
2 changes: 2 additions & 0 deletions docs/Configuration.md
Expand Up @@ -1063,6 +1063,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 @@ -501,10 +501,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 @@ -531,6 +533,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 @@ -557,6 +561,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
20 changes: 20 additions & 0 deletions e2e/__tests__/lolex.test.ts
@@ -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.
*/

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);
});
});
Expand Up @@ -52,6 +52,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.ts
Expand Up @@ -9,7 +9,7 @@ import {Script} from 'vm';
import {Global, Config} from '@jest/types';
import {installCommonGlobals} from 'jest-util';
import mock, {ModuleMocker} from 'jest-mock';
import {JestFakeTimers as FakeTimers} from '@jest/fake-timers';
import {JestFakeTimers as FakeTimers, LolexFakeTimers} from '@jest/fake-timers';
import {JestEnvironment, EnvironmentContext} from '@jest/environment';
import {JSDOM, VirtualConsole} from 'jsdom';

Expand All @@ -31,6 +31,7 @@ function isWin(globals: Win | Global.Global): globals is Win {
class JSDOMEnvironment implements JestEnvironment {
dom: JSDOM | null;
fakeTimers: FakeTimers<number> | null;
fakeTimersLolex: LolexFakeTimers | null;
// @ts-ignore
global: Global.Global | Win | null;
errorEventListener: ((event: Event & {error: Error}) => void) | null;
Expand Down Expand Up @@ -98,6 +99,11 @@ class JSDOMEnvironment implements JestEnvironment {
moduleMocker: this.moduleMocker,
timerConfig,
});

this.fakeTimersLolex = new LolexFakeTimers({
config,
global: (global as unknown) as NodeJS.Global,
});
}

setup() {
Expand All @@ -108,6 +114,9 @@ class JSDOMEnvironment implements JestEnvironment {
if (this.fakeTimers) {
this.fakeTimers.dispose();
}
if (this.fakeTimersLolex) {
this.fakeTimersLolex.dispose();
}
if (this.global) {
if (this.errorEventListener && isWin(this.global)) {
this.global.removeEventListener('error', this.errorEventListener);
Expand All @@ -122,6 +131,7 @@ class JSDOMEnvironment implements JestEnvironment {
this.global = null;
this.dom = null;
this.fakeTimers = null;
this.fakeTimersLolex = null;
return Promise.resolve();
}

Expand Down
9 changes: 8 additions & 1 deletion packages/jest-environment-node/src/index.ts
Expand Up @@ -9,7 +9,7 @@ import vm, {Script, Context} from 'vm';
import {Global, Config} from '@jest/types';
import {ModuleMocker} from 'jest-mock';
import {installCommonGlobals} from 'jest-util';
import {JestFakeTimers as FakeTimers} from '@jest/fake-timers';
import {JestFakeTimers as FakeTimers, LolexFakeTimers} from '@jest/fake-timers';
import {JestEnvironment} from '@jest/environment';

type Timer = {
Expand All @@ -21,6 +21,7 @@ type Timer = {
class NodeEnvironment implements JestEnvironment {
context: Context | null;
fakeTimers: FakeTimers<Timer> | null;
fakeTimersLolex: LolexFakeTimers | null;
global: Global.Global;
moduleMocker: ModuleMocker | null;

Expand Down Expand Up @@ -78,6 +79,8 @@ class NodeEnvironment implements JestEnvironment {
moduleMocker: this.moduleMocker,
timerConfig,
});

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

setup() {
Expand All @@ -88,8 +91,12 @@ class NodeEnvironment implements JestEnvironment {
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
19 changes: 18 additions & 1 deletion packages/jest-environment/src/index.ts
Expand Up @@ -9,7 +9,7 @@ import {Script} from 'vm';
import {Config, Global} from '@jest/types';
import jestMock, {ModuleMocker} from 'jest-mock';
import {ScriptTransformer} from '@jest/transform';
import {JestFakeTimers as FakeTimers} from '@jest/fake-timers';
import {JestFakeTimers as FakeTimers, LolexFakeTimers} from '@jest/fake-timers';

type JestMockFn = typeof jestMock.fn;
type JestMockSpyOn = typeof jestMock.spyOn;
Expand All @@ -29,6 +29,7 @@ export declare class JestEnvironment {
constructor(config: Config.ProjectConfig, context?: EnvironmentContext);
global: Global.Global;
fakeTimers: FakeTimers<unknown> | null;
fakeTimersLolex: LolexFakeTimers | null;
moduleMocker: ModuleMocker | null;
runScript(
script: Script,
Expand Down Expand Up @@ -166,6 +167,8 @@ export interface Jest {
retryTimes(numRetries: number): Jest;
/**
* Exhausts tasks queued by setImmediate().
*
* > Note: This function is not available when using Lolex as fake timers implementation
*/
runAllImmediates(): void;
/**
Expand Down Expand Up @@ -248,4 +251,18 @@ export interface Jest {
* every test so that local module state doesn't conflict between tests.
*/
isolateModules(fn: () => void): Jest;

/**
* 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
*/
getRealSystemTime(): number;

/**
* 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
*/
setSystemTime(now?: number): void;
}
6 changes: 5 additions & 1 deletion packages/jest-fake-timers/package.json
Expand Up @@ -12,7 +12,11 @@
"dependencies": {
"@jest/types": "^24.7.0",
"jest-message-util": "^24.7.1",
"jest-mock": "^24.7.0"
"jest-mock": "^24.7.0",
"lolex": "^4.0.0"
},
"devDependencies": {
"@types/lolex": "^3.1.1"
},
"engines": {
"node": ">= 6"
Expand Down

0 comments on commit aa2ecd6

Please sign in to comment.