Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fake promises for fake timers #6876

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c8334fd
Create and test fake promises
Berzeg Aug 19, 2018
a2fe3f1
Merge branch 'master' into fake-promises-for-fake-timers
Berzeg Aug 19, 2018
34e06e1
Integrate fake promises with fake timers
Berzeg Aug 20, 2018
75caae7
Add FakePromises to jest configuration/environment
Berzeg Aug 20, 2018
b67bdc6
remove fake promise setters from fake timers class
Berzeg Aug 20, 2018
ff9a55a
Update docuemntation to include fakePromises and changes to fakeTimers
Berzeg Aug 21, 2018
5709e02
Merge branch 'master' into fake-promises-for-fake-timers
Berzeg Aug 21, 2018
8792dc9
add promise mock changes to CHANGELOG
Berzeg Aug 21, 2018
05e705e
fix broken fakePromise test and flatten tests
Berzeg Aug 21, 2018
afb2b5b
Update stale snapshot
Berzeg Aug 21, 2018
bf04905
Get rid of fakePromise memory leaks
Berzeg Aug 23, 2018
6ddadb3
Use proper changelog format, fix docs typo
Berzeg Aug 23, 2018
6a64998
Merge branch 'master' into fake-promises-for-fake-timers
Berzeg Aug 23, 2018
bc7c78e
remove empty file
Berzeg Aug 25, 2018
233be98
Make fake promise docs consistent with rest
Berzeg Aug 25, 2018
5da3111
remove fakePromise's nextTick dependency
Berzeg Aug 25, 2018
d9d8055
Update fake promise docs
Berzeg Aug 25, 2018
cc4601b
Merge branch 'master' into fake-promises-for-fake-timers
Berzeg Aug 30, 2018
f78f243
Merge branch 'master' into fake-promises-for-fake-timers
Berzeg Sep 11, 2018
3c0917c
format fake promises changelog properly
Berzeg Sep 11, 2018
18c87e1
warn if fake promises used without babel-jest
Berzeg Sep 12, 2018
4060a7c
undo fake promise changelog formatting
Berzeg Sep 13, 2018
e6d30c6
remove unnecessary newline in changelog
Berzeg Sep 13, 2018
6bf8ffe
add async-to-generator to babel-preset-jest
Berzeg Sep 14, 2018
2c9f3a6
add option to mock promises implied in async-await syntax
Berzeg Sep 16, 2018
7879ece
document changes to fake promise api
Berzeg Sep 16, 2018
c6ebf8d
set compileAsyncToGenerator option to true for tests that use fake pr…
Berzeg Sep 17, 2018
932d76f
add new options to jest-cli snapshot test
Berzeg Sep 17, 2018
0cc3b74
update failing snapshots
Berzeg Sep 17, 2018
e4b91e3
Merge branch 'master' into fake-promises-for-fake-timers
Berzeg Oct 11, 2018
faee4f9
Merge branch 'master' into fake-promises-for-fake-timers
Berzeg Oct 29, 2018
954a164
Add `clearAllPromises` to jest api
Berzeg Oct 29, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added -....
Empty file.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
### Fixes

- `[jest-cli]` Fix incorrect `testEnvironmentOptions` warning ([#6852](https://github.com/facebook/jest/pull/6852))
- `[jest-each`] Prevent done callback being supplied to describe ([#6843](https://github.com/facebook/jest/pull/6843))
- `[jest-config`] Better error message for a case when a preset module was found, but no `jest-preset.js` or `jest-preset.json` at the root ([#6863](https://github.com/facebook/jest/pull/6863))
- `[jest-each]` Prevent done callback being supplied to describe ([#6843](https://github.com/facebook/jest/pull/6843))
- `[jest-config]` Better error message for a case when a preset module was found, but no `jest-preset.js` or `jest-preset.json` at the root ([#6863](https://github.com/facebook/jest/pull/6863))
- `[jest-util]` Introduce FakePromises to fix micro-task run-order issues when using FakeTimers. ([6876](https://github.com/facebook/jest/pull/6876))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the number should include # (same with the docs one)


### Chore & Maintenance

- `[docs]` Add custom toMatchSnapshot matcher docs ([#6837](https://github.com/facebook/jest/pull/6837))
- `[docs]` Improve the documentation regarding preset configuration ([#6864](https://github.com/facebook/jest/issues/6864))
- `[docs]` Clarify usage of `--projects` CLI option ([#6872](https://github.com/facebook/jest/pull/6872))
- `[docs]` Add documentation for promise mocks, and clarify how micro-tasks behave when using timer mocks. ([6876](https://github.com/facebook/jest/pull/6876))

## 23.5.0

Expand Down
1 change: 1 addition & 0 deletions TestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const DEFAULT_PROJECT_CONFIG: ProjectConfig = {
modulePaths: [],
name: 'test_name',
prettierPath: 'prettier',
promises: 'real',
resetMocks: false,
resetModules: false,
resolver: null,
Expand Down
6 changes: 6 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,12 @@ The projects feature can also be used to run multiple configurations or multiple

_Note: When using multi project runner, it's recommended to add a `displayName` for each project. This will show the `displayName` of a project next to its tests._

### `promises` [string]

Default: `real`

Setting this value to `fake` allows the use of fake promises to replace the `Promise` class introduced in ecma-script2015 specification. Fake promises are useful for synchronizing promises, or for use with jest's fake timers feature.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Jest's"


### `reporters` [array<moduleName | [moduleName, options]>]

Default: `undefined`
Expand Down
27 changes: 25 additions & 2 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ The `jest` object is automatically in scope within every test file. The methods
- [`jest.restoreAllMocks()`](#jestrestoreallmocks)
- [`jest.resetModules()`](#jestresetmodules)
- [`jest.retryTimes()`](#jestretrytimes)
- [`jest.runAllPromises(runAllTicks)`](#jestrunallpromises)
- [`jest.runAllTicks()`](#jestrunallticks)
- [`jest.runAllTimers()`](#jestrunalltimers)
- [`jest.advanceTimersByTime(msToRun)`](#jestadvancetimersbytimemstorun)
- [`jest.runOnlyPendingTimers()`](#jestrunonlypendingtimers)
- [`jest.setMock(moduleName, moduleExports)`](#jestsetmockmodulename-moduleexports)
- [`jest.setTimeout(timeout)`](#jestsettimeouttimeout)
- [`jest.useFakePromises()`](#jestusefakepromises)
- [`jest.useFakeTimers()`](#jestusefaketimers)
- [`jest.useRealPromises()`](#jestuserealpromises)
- [`jest.useRealTimers()`](#jestuserealtimers)
- [`jest.spyOn(object, methodName)`](#jestspyonobject-methodname)
- [`jest.spyOn(object, methodName, accessType?)`](#jestspyonobject-methodname-accesstype)
Expand Down Expand Up @@ -344,11 +347,19 @@ module.exports = {

Returns the `jest` object for chaining.

### `jest.runAllPromises(runAllTicks)`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it a bit odd to pass in a runAllTicks callback, although I agree with trying to avoid a breaking change in runAllTicks. I don't have a better suggestion at the moment, so just flagging it 🙂

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe drop it and document that people should call runAllTicks themselves?

Potentially runAllTicksAndPromises

Oooor make runAllTicks run ticks if fake timers and promises if fake promises?


Exhausts the **micro**-task queue (`process.nextTick` callbacks and promises scheduled via the `Promise` class).

Uses the provided `runAllTicks` callback to exhaust all ticks that are currently queued. Fake promises should be in use (by calling `jest.useFakePromises` ) before calling this method.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space between ` and )


This is useful for synchronously executing scheduled promises and ticks in the order in their natural run order in node.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the order in their natural run order in node.

That reads a bit weird, mind rephrasing? Also, "Node.js"


### `jest.runAllTicks()`

Exhausts the **micro**-task queue (usually interfaced in node via `process.nextTick`).
Exhausts all scheduled next tick callbacks (usually interfaced in node via `process.nextTick`).

When this API is called, all pending micro-tasks that have been queued via `process.nextTick` will be executed. Additionally, if those micro-tasks themselves schedule new micro-tasks, those will be continually exhausted until there are no more micro-tasks remaining in the queue.
When this API is called, all pending next tick callbacks that have been queued via `process.nextTick` will be executed. Additionally, if those micro-tasks themselves schedule new micro-tasks, those will be continually exhausted until there are no more micro-tasks remaining in the queue. See the (Promise Mocks)(PromiseMocks.md) doc for more information.

### `jest.runAllTimers()`

Expand Down Expand Up @@ -404,12 +415,24 @@ Example:
jest.setTimeout(1000); // 1 second
```

### `jest.useFakePromises()`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should note that async-await (as long as it's not transpiled into generators and normal Promise with Babel or something like it) will not work


Instructs Jest to use fake versions of the standard Promise API (`Promise.resolve`, `Promise.reject`, `Promise.all`, `Promise.race`, `[Promise].then`, `[Promise].catch`, and `[Promise].finally`).

Returns the `jest` object for chaining.

### `jest.useFakeTimers()`

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

Returns the `jest` object for chaining.

### `jest.useRealPromises()`

Instructs Jest to use the real versions of the standard Promise API.

Returns the `jest` object for chaining.

### `jest.useRealTimers()`

Instructs Jest to use the real versions of the standard timer functions.
Expand Down
54 changes: 54 additions & 0 deletions docs/PromiseMocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
id: promise-mocks
title: Promise Mocks
---

Can be used to supplement the [timer mocks](TimerMocks.md). Timer mocks work by running timer callbacks synchronously, in the same order they normally would, but without waiting. Promise mocks can be used with timer mocks to skip wait times and still run node micro-tasks and macro-tasks in the order they normally would.

```javascript
// promiseGame.js
'use strict';

function promiseGame(callback) {
console.log('Ready....go!');
Promise.resolve(0).then(() => {
console.log('Promise is fulfilled -- stop!');
callback && callback();
});
}

module.exports = promiseGame;
```

```javascript
// __tests__/promiseGame.js
'use strict';

jest.useFakePromises();

test('runs all queued promises', () => {
const promiseGame = require('../promiseGame');
const callback = jest.fn();
const runAllTicks = jest.fn();

promiseGame(callback);

// At this point, the promise callback should not have been called yet
expect(callback).not.toBeCalled();

// Fast-forward until all promises are completed
jest.runAllPromises(runAllTicks);

// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);

// Check that all ticks have been run
expect(runAllTicks).toBeCalled();
expect(runAllTicks).toHaveBeenCalledTimes(1);
});
```

Here we enable fake promises by calling `jest.useFakePromises();`. This mocks out the Promise class and its entire API. `jest.runAllPromises();` is used to synchronously run all queued promises

The timer mock API uses a promise mock delegate to run all queued promises and nextTick callbacks if fake promises are being used.
1 change: 1 addition & 0 deletions e2e/__tests__/__snapshots__/show_config.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ exports[`--showConfig outputs config info and exits 1`] = `
\\"modulePathIgnorePatterns\\": [],
\\"name\\": \\"[md5 hash]\\",
\\"prettierPath\\": null,
\\"promises\\": \\"real\\",
\\"resetMocks\\": false,
\\"resetModules\\": false,
\\"resolver\\": null,
Expand Down
3 changes: 3 additions & 0 deletions examples/promise/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["env"]
}
27 changes: 27 additions & 0 deletions examples/promise/__tests__/promise_game.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict';

jest.useFakePromises();

describe('promiseGame', () => {
it('runs all queued promises', () => {
const promiseGame = require('../promiseGame');
const callback = jest.fn();
const runAllTicks = jest.fn();

promiseGame(callback);

// At this point, the promise callback should not have been called yet
expect(callback).not.toBeCalled();

// Fast-forward until all promises are completed
jest.runAllPromises(runAllTicks);

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

// Check that all ticks have been run
expect(runAllTicks).toBeCalled();
expect(runAllTicks.mock.calls.length).toBeGreaterThan(0);
});
});
12 changes: 12 additions & 0 deletions examples/promise/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"private": true,
"version": "0.0.0",
"name": "example-promise",
"devDependencies": {
"babel-preset-env": "*",
"jest": "*"
},
"scripts": {
"test": "jest"
}
}
9 changes: 9 additions & 0 deletions examples/promise/promiseGame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function promiseGame(callback) {
console.log('Ready....go!');
Promise.resolve(0).then(() => {
console.log('Promise is fulfilled -- stop!');
callback && callback();
});
}

module.exports = promiseGame;
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ const jestAdapter = async (
environment.fakeTimers.useFakeTimers();
}

if (config.promises === 'fake') {
environment.fakePromises.useFakePromises();
}

globals.beforeEach(() => {
if (config.resetModules) {
runtime.resetModules();
Expand All @@ -66,6 +70,10 @@ const jestAdapter = async (
if (config.timers === 'fake') {
environment.fakeTimers.useFakeTimers();
}

if (config.promises === 'fake') {
environment.fakePromises.useFakePromises();
}
}

if (config.restoreMocks) {
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/Defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default ({
preset: null,
prettierPath: 'prettier',
projects: null,
promises: 'real',
resetMocks: false,
resetModules: false,
resolver: null,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ const getConfigs = (
modulePaths: options.modulePaths,
name: options.name,
prettierPath: options.prettierPath,
promises: options.promises,
resetMocks: options.resetMocks,
resetModules: options.resetModules,
resolver: options.resolver,
Expand Down
10 changes: 9 additions & 1 deletion packages/jest-environment-jsdom/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import type {EnvironmentOptions} from 'types/Environment';
import type {Global} from 'types/Global';
import type {ModuleMocker} from 'jest-mock';

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

class JSDOMEnvironment {
dom: ?Object;
fakePromises: ?FakePromises;
fakeTimers: ?FakeTimers<number>;
global: ?Global;
errorEventListener: ?Function;
Expand Down Expand Up @@ -77,8 +78,14 @@ class JSDOMEnvironment {
refToId: (ref: number) => ref,
};

this.fakePromises = new FakePromises({
config,
global,
});

this.fakeTimers = new FakeTimers({
config,
fakePromises: this.fakePromises,
global,
moduleMocker: this.moduleMocker,
timerConfig,
Expand All @@ -104,6 +111,7 @@ class JSDOMEnvironment {
this.errorEventListener = null;
this.global = null;
this.dom = null;
this.fakePromises = null;
this.fakeTimers = null;
return Promise.resolve();
}
Expand Down
10 changes: 9 additions & 1 deletion packages/jest-environment-node/src/index.js
Original file line number Diff line number Diff line change
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 {FakePromises, FakeTimers, installCommonGlobals} from 'jest-util';
import mock from 'jest-mock';

type Timer = {|
Expand All @@ -24,6 +24,7 @@ type Timer = {|

class NodeEnvironment {
context: ?vm$Context;
fakePromises: ?FakePromises;
fakeTimers: ?FakeTimers<Timer>;
global: ?Global;
moduleMocker: ?ModuleMocker;
Expand Down Expand Up @@ -65,8 +66,14 @@ class NodeEnvironment {
refToId: timerRefToId,
};

this.fakePromises = new FakePromises({
config,
global,
});

this.fakeTimers = new FakeTimers({
config,
fakePromises: this.fakePromises,
global,
moduleMocker: this.moduleMocker,
timerConfig,
Expand All @@ -82,6 +89,7 @@ class NodeEnvironment {
this.fakeTimers.dispose();
}
this.context = null;
this.fakePromises = null;
this.fakeTimers = null;
return Promise.resolve();
}
Expand Down
8 changes: 8 additions & 0 deletions packages/jest-jasmine2/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ async function jasmine2(
environment.fakeTimers.useFakeTimers();
}

if (config.promises === 'fake') {
environment.fakePromises.useFakePromises();
}

env.beforeEach(() => {
if (config.resetModules) {
runtime.resetModules();
Expand All @@ -86,6 +90,10 @@ async function jasmine2(
if (config.timers === 'fake') {
environment.fakeTimers.useFakeTimers();
}

if (config.promises === 'fake') {
environment.fakePromises.useFakePromises();
}
}

if (config.restoreMocks) {
Expand Down
12 changes: 12 additions & 0 deletions packages/jest-runtime/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,14 @@ class Runtime {
this.restoreAllMocks();
return jestObject;
};
const useFakePromises = () => {
this._environment.fakePromises.useFakePromises();
return jestObject;
};
const useRealPromises = () => {
this._environment.fakePromises.useFakePromises();
return jestObject;
};
const useFakeTimers = () => {
this._environment.fakeTimers.useFakeTimers();
return jestObject;
Expand Down Expand Up @@ -919,6 +927,8 @@ class Runtime {
restoreAllMocks,
retryTimes,
runAllImmediates: () => this._environment.fakeTimers.runAllImmediates(),
runAllPromises: (runAllTicks: Function) =>
this._environment.fakePromises.runAllPromises(runAllTicks),
runAllTicks: () => this._environment.fakeTimers.runAllTicks(),
runAllTimers: () => this._environment.fakeTimers.runAllTimers(),
runOnlyPendingTimers: () =>
Expand All @@ -930,7 +940,9 @@ class Runtime {
setTimeout,
spyOn,
unmock,
useFakePromises,
useFakeTimers,
useRealPromises,
useRealTimers,
};
return jestObject;
Expand Down
Loading