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

feat: support concurrent in Jest Each #9326

Merged
merged 10 commits into from Aug 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
### Features

- `[jest-circus, jest-jasmine2]` Include `failureDetails` property in test results ([#9496](https://github.com/facebook/jest/pull/9496))
- `[jest-each, jest-jasmine, jest-circus]` Add support for .concurrent.each ([#9326](https://github.com/facebook/jest/pull/9326))
SimenB marked this conversation as resolved.
Show resolved Hide resolved

### Fixes

Expand Down
165 changes: 165 additions & 0 deletions docs/GlobalAPI.md
Expand Up @@ -465,6 +465,171 @@ test('has lemon in it', () => {

Even though the call to `test` will return right away, the test doesn't complete until the promise resolves as well.

### `test.concurrent(name, fn, timeout)`
Copy link
Member

@SimenB SimenB Aug 5, 2020

Choose a reason for hiding this comment

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

I had forgotten this is undocumented... Can you add something like

Note: test.concurrent is considered experimental - see https://github.com/facebook/jest/labels/Area%3A%20Concurrent for details on missing features and other issues


Also under the alias: `it.concurrent(name, fn, timeout)`

Use `test.concurrent` if you want the test to run concurrently.

> Note: `test.concurrent` is considered experimental - see [here])https://github.com/facebook/jest/labels/Area%3A%20Concurrent) for details on missing features and other issues

The first argument is the test name; the second argument is an asynchronous function that contains the expectations to test. The third argument (optional) is `timeout` (in milliseconds) for specifying how long to wait before aborting. _Note: The default timeout is 5 seconds._

```
test.concurrent('addition of 2 numbers', async () => {
expect(5 + 3).toBe(8);
});

test.concurrent('subtraction 2 numbers', async () => {
expect(5 - 3).toBe(2);
});
```

> Note: Use `maxConcurrency` in configuration to prevents Jest from executing more than the specified amount of tests at the same time

### `test.concurrent.each(table)(name, fn, timeout)`

Also under the alias: `it.concurrent.each(table)(name, fn, timeout)`

Use `test.concurrent.each` if you keep duplicating the same test with different data. `test.each` allows you to write the test once and pass data in, the tests are all run asynchronously.

`test.concurrent.each` is available with two APIs:

#### 1. `test.concurrent.each(table)(name, fn, timeout)`

- `table`: `Array` of Arrays with the arguments that are passed into the test `fn` for each row.
- _Note_ If you pass in a 1D array of primitives, internally it will be mapped to a table i.e. `[1, 2, 3] -> [[1], [2], [3]]`
- `name`: `String` the title of the test block.
- Generate unique test titles by positionally injecting parameters with [`printf` formatting](https://nodejs.org/api/util.html#util_util_format_format_args):
- `%p` - [pretty-format](https://www.npmjs.com/package/pretty-format).
- `%s`- String.
- `%d`- Number.
- `%i` - Integer.
- `%f` - Floating point value.
- `%j` - JSON.
- `%o` - Object.
- `%#` - Index of the test case.
- `%%` - single percent sign ('%'). This does not consume an argument.
- `fn`: `Function` the test to be ran, this is the function that will receive the parameters in each row as function arguments, **this will have to be an asynchronous function**.
- Optionally, you can provide a `timeout` (in milliseconds) for specifying how long to wait for each row before aborting. _Note: The default timeout is 5 seconds._

Example:

```js
test.concurrent.each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
])('.add(%i, %i)', (a, b, expected) => {
expect(a + b).toBe(expected);
});
```

#### 2. `` test.concurrent.each`table`(name, fn, timeout) ``

- `table`: `Tagged Template Literal`
- First row of variable name column headings separated with `|`
- One or more subsequent rows of data supplied as template literal expressions using `${value}` syntax.
- `name`: `String` the title of the test, use `$variable` to inject test data into the test title from the tagged template expressions.
- To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value`
- `fn`: `Function` the test to be ran, this is the function that will receive the test data object, **this will have to be an asynchronous function**.
- Optionally, you can provide a `timeout` (in milliseconds) for specifying how long to wait for each row before aborting. _Note: The default timeout is 5 seconds._

Example:

```js
test.concurrent.each`
a | b | expected
${1} | ${1} | ${2}
${1} | ${2} | ${3}
${2} | ${1} | ${3}
`('returns $expected when $a is added $b', ({a, b, expected}) => {
expect(a + b).toBe(expected);
});
```

### `test.concurrent.only.each(table)(name, fn)`

Also under the alias: `it.concurrent.only.each(table)(name, fn)`

Use `test.concurrent.only.each` if you want to only run specific tests with different test data concurrently.

`test.concurrent.only.each` is available with two APIs:

#### `test.concurrent.only.each(table)(name, fn)`

```js
test.concurrent.only.each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
])('.add(%i, %i)', async (a, b, expected) => {
expect(a + b).toBe(expected);
});

test('will not be ran', () => {
expect(1 / 0).toBe(Infinity);
});
```

#### `` test.only.each`table`(name, fn) ``

```js
test.concurrent.only.each`
a | b | expected
${1} | ${1} | ${2}
${1} | ${2} | ${3}
${2} | ${1} | ${3}
`('returns $expected when $a is added $b', async ({a, b, expected}) => {
expect(a + b).toBe(expected);
});

test('will not be ran', () => {
expect(1 / 0).toBe(Infinity);
});
```

### `test.concurrent.skip.each(table)(name, fn)`

Also under the alias: `it.concurrent.skip.each(table)(name, fn)`

Use `test.concurrent.skip.each` if you want to stop running a collection of asynchronous data driven tests.

`test.concurrent.skip.each` is available with two APIs:

#### `test.concurrent.skip.each(table)(name, fn)`

```js
test.concurrent.skip.each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
])('.add(%i, %i)', async (a, b, expected) => {
expect(a + b).toBe(expected); // will not be ran
});

test('will be ran', () => {
expect(1 / 0).toBe(Infinity);
});
```

#### `` test.concurrent.skip.each`table`(name, fn) ``

```js
test.concurrent.skip.each`
a | b | expected
${1} | ${1} | ${2}
${1} | ${2} | ${3}
${2} | ${1} | ${3}
`('returns $expected when $a is added $b', async ({a, b, expected}) => {
expect(a + b).toBe(expected); // will not be ran
});

test('will be ran', () => {
expect(1 / 0).toBe(Infinity);
});
```

### `test.each(table)(name, fn, timeout)`

Also under the alias: `it.each(table)(name, fn)` and `` it.each`table`(name, fn) ``
Expand Down
29 changes: 29 additions & 0 deletions e2e/__tests__/circusConcurrentEach.test.ts
@@ -0,0 +1,29 @@
/**
* 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 {json as runWithJson} from '../runJest';

it('works with concurrent.each', () => {
const {json} = runWithJson('circus-concurrent', [
'concurrent-each.test.js',
'--testRunner=jest-circus/runner',
]);
expect(json.numTotalTests).toBe(4);
expect(json.numPassedTests).toBe(2);
expect(json.numFailedTests).toBe(0);
expect(json.numPendingTests).toBe(2);
});

it('works with concurrent.only.each', () => {
const {json} = runWithJson('circus-concurrent', [
'concurrent-only-each.test.js',
'--testRunner=jest-circus/runner',
]);
expect(json.numTotalTests).toBe(4);
expect(json.numPassedTests).toBe(2);
expect(json.numFailedTests).toBe(0);
expect(json.numPendingTests).toBe(2);
});
18 changes: 18 additions & 0 deletions e2e/__tests__/jasmineAsync.test.ts
Expand Up @@ -128,6 +128,24 @@ describe('async jasmine', () => {
expect(json.testResults[0].message).toMatch(/concurrent test fails/);
});

it('works with concurrent.each', () => {
Mark1626 marked this conversation as resolved.
Show resolved Hide resolved
const {json} = runWithJson('jasmine-async', ['concurrent-each.test.js']);
expect(json.numTotalTests).toBe(4);
expect(json.numPassedTests).toBe(2);
expect(json.numFailedTests).toBe(0);
expect(json.numPendingTests).toBe(2);
});

it('works with concurrent.only.each', () => {
const {json} = runWithJson('jasmine-async', [
'concurrent-only-each.test.js',
]);
expect(json.numTotalTests).toBe(4);
expect(json.numPassedTests).toBe(2);
expect(json.numFailedTests).toBe(0);
expect(json.numPendingTests).toBe(2);
});

it("doesn't execute more than 5 tests simultaneously", () => {
const {json} = runWithJson('jasmine-async', ['concurrent-many.test.js']);
expect(json.numTotalTests).toBe(10);
Expand Down
20 changes: 20 additions & 0 deletions e2e/circus-concurrent/__tests__/concurrent-each.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';

it.concurrent.each([
[1, 2],
[2, 3],
])('adds one to number', async (a, b) => {
expect(a + 1).toBe(b);
});

it.concurrent.skip.each([
[1, 2],
[2, 3],
])('should skip this test', Promise.resolve());
22 changes: 22 additions & 0 deletions e2e/circus-concurrent/__tests__/concurrent-only-each.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.
*/

'use strict';

it.concurrent.only.each([
[1, 2],
[2, 3],
])('adds one to number', async (a, b) => {
expect(a + 1).toBe(b);
});

it.concurrent.each([
[1, 2],
[2, 3],
])('adds one to number', async (a, b) => {
expect(a + 1).toBe(b);
});
5 changes: 5 additions & 0 deletions e2e/circus-concurrent/package.json
@@ -0,0 +1,5 @@
{
"jest": {
"testEnvironment": "node"
}
}
20 changes: 20 additions & 0 deletions e2e/jasmine-async/__tests__/concurrent-each.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';

it.concurrent.each([
[1, 2],
[2, 3],
])('adds one to number', async (a, b) => {
expect(a + 1).toBe(b);
});

it.concurrent.skip.each([
[1, 2],
[2, 3],
])('should skip this test', Promise.resolve());
22 changes: 22 additions & 0 deletions e2e/jasmine-async/__tests__/concurrent-only-each.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.
*/

'use strict';

it.concurrent.only.each([
[1, 2],
[2, 3],
])('adds one to number', async (a, b) => {
expect(a + 1).toBe(b);
});

it.concurrent.each([
[1, 2],
[2, 3],
])('adds one to number', async (a, b) => {
expect(a + 1).toBe(b);
});
Expand Up @@ -22,6 +22,7 @@ import {
addSerializer,
buildSnapshotResolver,
} from 'jest-snapshot';
import {bind} from 'jest-each';
import throat from 'throat';
import {
ROOT_DESCRIBE_BLOCK_NAME,
Expand Down Expand Up @@ -77,7 +78,7 @@ export const initialize = async ({
nodeGlobal.test.concurrent = (test => {
const concurrent = (
testName: string,
testFn: () => Promise<any>,
testFn: () => Promise<unknown>,
timeout?: number,
) => {
// For concurrent tests we first run the function that returns promise, and then register a
Expand All @@ -90,18 +91,23 @@ export const initialize = async ({
nodeGlobal.test(testName, () => promise, timeout);
};

concurrent.only = (
const only = (
testName: string,
testFn: () => Promise<any>,
testFn: () => Promise<unknown>,
timeout?: number,
) => {
const promise = mutex(() => testFn());
// eslint-disable-next-line jest/no-focused-tests
test.only(testName, () => promise, timeout);
};

concurrent.only = only;
concurrent.skip = test.skip;

concurrent.each = bind(test, false);
concurrent.skip.each = bind(test.skip, false);
only.each = bind(test.only, false);

return concurrent;
})(nodeGlobal.test);

Expand Down