Skip to content

Commit

Permalink
feat: support concurrent in Jest Each (#9326)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark1626 committed Aug 6, 2020
1 parent f077751 commit c95abca
Show file tree
Hide file tree
Showing 22 changed files with 649 additions and 28 deletions.
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))

### 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)`

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', () => {
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

0 comments on commit c95abca

Please sign in to comment.