Add test.todo (#6996)

This commit is contained in:
Matt Phillips 2018-09-29 13:07:57 +01:00 committed by Simen Bekkhus
parent 15b43d6a27
commit 49b238046f
29 changed files with 416 additions and 28 deletions

View File

@ -2,6 +2,7 @@
### Features
- `[jest-jasmine2/jest-circus/jest-cli]` Add test.todo ([#6996](https://github.com/facebook/jest/pull/6996))
- `[pretty-format]` Option to not escape strings in diff messages ([#5661](https://github.com/facebook/jest/pull/5661))
- `[jest-haste-map]` Add `getFileIterator` to `HasteFS` for faster file iteration ([#7010](https://github.com/facebook/jest/pull/7010)).
- `[jest-worker]` [**BREAKING**] Add functionality to call a `setup` method in the worker before the first call and a `teardown` method when ending the farm ([#7014](https://github.com/facebook/jest/pull/7014)).

View File

@ -655,3 +655,21 @@ test('will be ran', () => {
expect(1 / 0).toBe(Infinity);
});
```
### `test.todo(name)`
Use `test.todo` when you are planning on writing tests. These tests will be highlighted in the summary output at the end so you know how many tests you still need todo.
_Note_: If you supply a test callback function then the `test.todo` will throw an error. If you have already implemented the test and it is broken and you do not want it to run, then use `test.skip` instead.
#### API
- `name`: `String` the title of the test plan.
Example:
```js
const add = (a, b) => a + b;
test.todo('add should be associative');
```

View File

@ -23,7 +23,7 @@ exports[`cannot test with no implementation 1`] = `
"FAIL __tests__/only-constructs.test.js
● Test suite failed to run
Missing second argument. It must be a callback function.
Missing second argument. It must be a callback function. Perhaps you want to use \`test.todo\` for a test placeholder.
1 |
2 | it('it', () => {});
@ -50,7 +50,7 @@ exports[`cannot test with no implementation with expand arg 1`] = `
"FAIL __tests__/only-constructs.test.js
● Test suite failed to run
Missing second argument. It must be a callback function.
Missing second argument. It must be a callback function. Perhaps you want to use \`test.todo\` for a test placeholder.
1 |
2 | it('it', () => {});

View File

@ -0,0 +1,79 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`shows error messages when called with invalid argument 1`] = `
"FAIL __tests__/todo_non_string.test.js
● Test suite failed to run
Todo must be called with only a description.
6 | */
7 |
> 8 | it.todo(() => {});
| ^
9 |
at __tests__/todo_non_string.test.js:8:4
"
`;
exports[`shows error messages when called with multiple arguments 1`] = `
"FAIL __tests__/todo_multiple_args.test.js
● Test suite failed to run
Todo must be called with only a description.
6 | */
7 |
> 8 | it.todo('todo later', () => {});
| ^
9 |
at __tests__/todo_multiple_args.test.js:8:4
"
`;
exports[`shows error messages when called with no arguments 1`] = `
"FAIL __tests__/todo_no_args.test.js
● Test suite failed to run
Todo must be called with only a description.
6 | */
7 |
> 8 | it.todo();
| ^
9 |
at __tests__/todo_no_args.test.js:8:4
"
`;
exports[`works with all statuses 1`] = `
"FAIL __tests__/statuses.test.js
✓ passes
✕ fails
○ skipped 1 test
✎ todo 1 test
● fails
expect(received).toBe(expected) // Object.is equality
Expected: 101
Received: 10
11 |
12 | it('fails', () => {
> 13 | expect(10).toBe(101);
| ^
14 | });
15 |
16 | it.skip('skips', () => {
at __tests__/statuses.test.js:13:14
"
`;

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2018-present, Facebook, Inc. 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
*/
'use strict';
const path = require('path');
const runJest = require('../runJest');
const {extractSummary} = require('../Utils');
const dir = path.resolve(__dirname, '../test-todo');
test('works with all statuses', () => {
const result = runJest(dir, ['statuses.test.js']);
expect(result.status).toBe(1);
const {rest} = extractSummary(result.stderr);
expect(rest).toMatchSnapshot();
});
test('shows error messages when called with no arguments', () => {
const result = runJest(dir, ['todo_no_args.test.js']);
expect(result.status).toBe(1);
const {rest} = extractSummary(result.stderr);
expect(rest).toMatchSnapshot();
});
test('shows error messages when called with multiple arguments', () => {
const result = runJest(dir, ['todo_multiple_args.test.js']);
expect(result.status).toBe(1);
const {rest} = extractSummary(result.stderr);
expect(rest).toMatchSnapshot();
});
test('shows error messages when called with invalid argument', () => {
const result = runJest(dir, ['todo_non_string.test.js']);
expect(result.status).toBe(1);
const {rest} = extractSummary(result.stderr);
expect(rest).toMatchSnapshot();
});

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) 2018-present, Facebook, Inc. 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.
*/
it('passes', () => {
expect(10).toBe(10);
});
it('fails', () => {
expect(10).toBe(101);
});
it.skip('skips', () => {
expect(10).toBe(101);
});
it.todo('todo');

View File

@ -0,0 +1,8 @@
/**
* Copyright (c) 2018-present, Facebook, Inc. 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.
*/
it.todo('todo later', () => {});

View File

@ -0,0 +1,8 @@
/**
* Copyright (c) 2018-present, Facebook, Inc. 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.
*/
it.todo();

View File

@ -0,0 +1,8 @@
/**
* Copyright (c) 2018-present, Facebook, Inc. 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.
*/
it.todo(() => {});

View File

@ -0,0 +1,5 @@
{
"jest": {
"testEnvironment": "node"
}
}

View File

@ -37,7 +37,9 @@ describe('test/it error throwing', () => {
expect(() => {
// $FlowFixMe: Easy, we're testing runitme errors here
circusIt('test2');
}).toThrowError('Missing second argument. It must be a callback function.');
}).toThrowError(
'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.',
);
});
it(`it throws an error when first argument isn't a string`, () => {
expect(() => {
@ -62,7 +64,9 @@ describe('test/it error throwing', () => {
expect(() => {
// $FlowFixMe: Easy, we're testing runitme errors here
circusTest('test6');
}).toThrowError('Missing second argument. It must be a callback function.');
}).toThrowError(
'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.',
);
});
it(`test throws an error when first argument isn't a string`, () => {
expect(() => {

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2015-present, Facebook, Inc. 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 strict-local
*/
'use strict';
let circusIt;
// using jest-jasmine2's 'it' to test jest-circus's 'it'. Had to differentiate
// the two with this alias.
const aliasCircusIt = () => {
const {it} = require('../index.js');
circusIt = it;
};
aliasCircusIt();
describe('test/it.todo error throwing', () => {
it('todo throws error when given no arguments', () => {
expect(() => {
// $FlowFixMe: Testing runitme errors here
circusIt.todo();
}).toThrowError('Todo must be called with only a description.');
});
it('todo throws error when given more than one argument', () => {
expect(() => {
circusIt.todo('test1', () => {});
}).toThrowError('Todo must be called with only a description.');
});
it('todo throws error when given none string description', () => {
expect(() => {
// $FlowFixMe: Testing runitme errors here
circusIt.todo(() => {});
}).toThrowError('Todo must be called with only a description.');
});
});

View File

@ -105,6 +105,10 @@ const handler: EventHandler = (event, state): void => {
event.test.status = 'skip';
break;
}
case 'test_todo': {
event.test.status = 'todo';
break;
}
case 'test_done': {
event.test.duration = getTestDuration(event.test);
event.test.status = 'done';

View File

@ -68,7 +68,9 @@ const test = (testName: TestName, fn: TestFn, timeout?: number) => {
);
}
if (fn === undefined) {
throw new Error('Missing second argument. It must be a callback function.');
throw new Error(
'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.',
);
}
if (typeof fn !== 'function') {
throw new Error(
@ -121,6 +123,32 @@ test.only = (testName: TestName, fn: TestFn, timeout?: number) => {
});
};
test.todo = (testName: TestName, ...rest: Array<mixed>) => {
if (rest.length > 0 || typeof testName !== 'string') {
const e = new Error('Todo must be called with only a description.');
if (Error.captureStackTrace) {
Error.captureStackTrace(e, test.todo);
}
throw e;
}
const asyncError = new Error();
if (Error.captureStackTrace) {
Error.captureStackTrace(asyncError, test);
}
return dispatch({
asyncError,
fn: () => {},
mode: 'todo',
name: 'add_test',
testName,
timeout: undefined,
});
};
test.each = bindEach(test);
test.only.each = bindEach(test.only);
test.skip.each = bindEach(test.skip);

View File

@ -128,12 +128,16 @@ export const runAndTransformResultsToJestFormat = async ({
let numFailingTests = 0;
let numPassingTests = 0;
let numPendingTests = 0;
let numTodoTests = 0;
const assertionResults = runResult.testResults.map(testResult => {
let status: Status;
if (testResult.status === 'skip') {
status = 'pending';
numPendingTests += 1;
} else if (testResult.status === 'todo') {
status = 'todo';
numTodoTests += 1;
} else if (testResult.errors.length) {
status = 'failed';
numFailingTests += 1;
@ -190,6 +194,7 @@ export const runAndTransformResultsToJestFormat = async ({
numFailingTests,
numPassingTests,
numPendingTests,
numTodoTests,
openHandles: [],
perfStats: {
// populated outside

View File

@ -97,6 +97,11 @@ const _runTest = async (test: TestEntry): Promise<void> => {
return;
}
if (test.mode === 'todo') {
dispatch({name: 'test_todo', test});
return;
}
const {afterEach, beforeEach} = getEachHooksForTest(test);
for (const hook of beforeEach) {

View File

@ -15,6 +15,7 @@ export const ICONS = {
failed: isWindows ? '\u00D7' : '\u2715',
pending: '\u25CB',
success: isWindows ? '\u221A' : '\u2713',
todo: '\u270E',
};
export const PACKAGE_JSON = 'package.json';
export const JEST_CONFIG = 'jest.config.js';

View File

@ -122,6 +122,7 @@ export const getSummary = (
const testsFailed = aggregatedResults.numFailedTests;
const testsPassed = aggregatedResults.numPassedTests;
const testsPending = aggregatedResults.numPendingTests;
const testsTodo = aggregatedResults.numTodoTests;
const testsTotal = aggregatedResults.numTotalTests;
const width = (options && options.width) || 0;
@ -141,6 +142,7 @@ export const getSummary = (
chalk.bold('Tests: ') +
(testsFailed ? chalk.bold.red(`${testsFailed} failed`) + ', ' : '') +
(testsPending ? chalk.bold.yellow(`${testsPending} skipped`) + ', ' : '') +
(testsTodo ? chalk.bold.magenta(`${testsTodo} todo`) + ', ' : '') +
(testsPassed ? chalk.bold.green(`${testsPassed} passed`) + ', ' : '') +
`${testsTotal} total`;

View File

@ -97,6 +97,8 @@ export default class VerboseReporter extends DefaultReporter {
return chalk.red(ICONS.failed);
} else if (status === 'pending') {
return chalk.yellow(ICONS.pending);
} else if (status === 'todo') {
return chalk.magenta(ICONS.todo);
} else {
return chalk.green(ICONS.success);
}
@ -112,26 +114,48 @@ export default class VerboseReporter extends DefaultReporter {
if (this._globalConfig.expand) {
tests.forEach(test => this._logTest(test, indentLevel));
} else {
const skippedCount = tests.reduce((result, test) => {
if (test.status === 'pending') {
result += 1;
} else {
this._logTest(test, indentLevel);
}
const summedTests = tests.reduce(
(result, test) => {
if (test.status === 'pending') {
result.pending += 1;
} else if (test.status === 'todo') {
result.todo += 1;
} else {
this._logTest(test, indentLevel);
}
return result;
}, 0);
return result;
},
{pending: 0, todo: 0},
);
if (skippedCount > 0) {
this._logSkippedTests(skippedCount, indentLevel);
if (summedTests.pending > 0) {
this._logSummedTests(
'skipped',
this._getIcon('pending'),
summedTests.pending,
indentLevel,
);
}
if (summedTests.todo > 0) {
this._logSummedTests(
'todo',
this._getIcon('todo'),
summedTests.todo,
indentLevel,
);
}
}
}
_logSkippedTests(count: number, indentLevel: number) {
const icon = this._getIcon('pending');
const text = chalk.dim(`skipped ${count} test${count === 1 ? '' : 's'}`);
_logSummedTests(
prefix: string,
icon: string,
count: number,
indentLevel: number,
) {
const text = chalk.dim(`${prefix} ${count} test${count === 1 ? '' : 's'}`);
this._logLine(`${icon} ${text}`, indentLevel);
}

View File

@ -21,6 +21,7 @@ export const makeEmptyAggregatedTestResult = (): AggregatedResult => ({
numPendingTestSuites: 0,
numPendingTests: 0,
numRuntimeErrorTestSuites: 0,
numTodoTests: 0,
numTotalTestSuites: 0,
numTotalTests: 0,
openHandles: [],
@ -57,6 +58,7 @@ export const buildFailureTestResult = (
numFailingTests: 0,
numPassingTests: 0,
numPendingTests: 0,
numTodoTests: 0,
openHandles: [],
perfStats: {
end: 0,
@ -87,10 +89,12 @@ export const addResult = (
aggregatedResults.numTotalTests +=
testResult.numPassingTests +
testResult.numFailingTests +
testResult.numPendingTests;
testResult.numPendingTests +
testResult.numTodoTests;
aggregatedResults.numFailedTests += testResult.numFailingTests;
aggregatedResults.numPassedTests += testResult.numPassingTests;
aggregatedResults.numPendingTests += testResult.numPendingTests;
aggregatedResults.numTodoTests += testResult.numTodoTests;
if (testResult.testExecError) {
aggregatedResults.numRuntimeErrorTestSuites++;

View File

@ -12,7 +12,9 @@ describe('test/it error throwing', () => {
it(`it throws error with missing callback function`, () => {
expect(() => {
it('test1');
}).toThrowError('Missing second argument. It must be a callback function.');
}).toThrowError(
'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.',
);
});
it(`it throws an error when first argument isn't a string`, () => {
expect(() => {
@ -29,7 +31,9 @@ describe('test/it error throwing', () => {
test(`test throws error with missing callback function`, () => {
expect(() => {
test('test4');
}).toThrowError('Missing second argument. It must be a callback function.');
}).toThrowError(
'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.',
);
});
test(`test throws an error when first argument isn't a string`, () => {
expect(() => {

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2015-present, Facebook, Inc. 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';
describe('test/it.todo error throwing', () => {
it('it throws error when given no arguments', () => {
expect(() => {
it.todo();
}).toThrowError('Todo must be called with only a description.');
});
it('it throws error when given more than one argument', () => {
expect(() => {
it.todo('test1', () => {});
}).toThrowError('Todo must be called with only a description.');
});
it('it throws error when given none string description', () => {
expect(() => {
it.todo(() => {});
}).toThrowError('Todo must be called with only a description.');
});
});

View File

@ -62,6 +62,7 @@ async function jasmine2(
environment.global.test = environment.global.it;
environment.global.it.only = environment.global.fit;
environment.global.it.todo = env.todo;
environment.global.it.skip = environment.global.xit;
environment.global.xtest = environment.global.xit;
environment.global.describe.skip = environment.global.xdescribe;

View File

@ -330,6 +330,9 @@ export default function(j$) {
if (currentDeclarationSuite.markedPending) {
suite.pend();
}
if (currentDeclarationSuite.markedTodo) {
suite.todo();
}
addSpecsToSuite(suite, specDefinitions);
return suite;
};
@ -452,7 +455,7 @@ export default function(j$) {
}
if (fn === undefined) {
throw new Error(
'Missing second argument. It must be a callback function.',
'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.',
);
}
if (typeof fn !== 'function') {
@ -491,6 +494,24 @@ export default function(j$) {
return spec;
};
this.todo = function() {
const description = arguments[0];
if (arguments.length !== 1 || typeof description !== 'string') {
const e = new Error('Todo must be called with only a description.');
if (Error.captureStackTrace) {
Error.captureStackTrace(e, test.todo);
}
throw e;
}
const spec = specFactory(description, () => {}, currentDeclarationSuite);
spec.todo();
currentDeclarationSuite.addChild(spec);
return spec;
};
this.fit = function(description, fn, timeout) {
const spec = specFactory(
description,

View File

@ -103,7 +103,12 @@ Spec.prototype.execute = function(onComplete, enabled) {
this.onStart(this);
if (!this.isExecutable() || this.markedPending || enabled === false) {
if (
!this.isExecutable() ||
this.markedPending ||
this.markedTodo ||
enabled === false
) {
complete(enabled);
return;
}
@ -175,6 +180,10 @@ Spec.prototype.pend = function(message) {
}
};
Spec.prototype.todo = function() {
this.markedTodo = true;
};
Spec.prototype.getResult = function() {
this.result.status = this.status();
return this.result;
@ -185,6 +194,10 @@ Spec.prototype.status = function(enabled) {
return 'disabled';
}
if (this.markedTodo) {
return 'todo';
}
if (this.markedPending) {
return 'pending';
}

View File

@ -85,12 +85,15 @@ export default class Jasmine2Reporter {
let numFailingTests = 0;
let numPassingTests = 0;
let numPendingTests = 0;
let numTodoTests = 0;
const testResults = this._testResults;
testResults.forEach(testResult => {
if (testResult.status === 'failed') {
numFailingTests++;
} else if (testResult.status === 'pending') {
numPendingTests++;
} else if (testResult.status === 'todo') {
numTodoTests++;
} else {
numPassingTests++;
}
@ -107,6 +110,7 @@ export default class Jasmine2Reporter {
numFailingTests,
numPassingTests,
numPendingTests,
numTodoTests,
perfStats: {
end: 0,
start: 0,

View File

@ -195,7 +195,10 @@ async function runTestInternal(
}
const testCount =
result.numPassingTests + result.numFailingTests + result.numPendingTests;
result.numPassingTests +
result.numFailingTests +
result.numPendingTests +
result.numTodoTests;
result.perfStats = {end: Date.now(), start};
result.testFilePath = path;

View File

@ -10,7 +10,7 @@
export type DoneFn = (reason?: string | Error) => void;
export type BlockFn = () => void;
export type BlockName = string | Function;
export type BlockMode = void | 'skip' | 'only';
export type BlockMode = void | 'skip' | 'only' | 'todo';
export type TestMode = BlockMode;
export type TestName = string;
export type TestFn = (done?: DoneFn) => ?Promise<any>;
@ -105,6 +105,10 @@ export type Event =
name: 'test_skip',
test: TestEntry,
|}
| {|
name: 'test_todo',
test: TestEntry,
|}
| {|
// test failure is defined by presence of errors in `test.errors`,
// `test_done` indicates that the test and all its hooks were run,
@ -145,7 +149,7 @@ export type Event =
name: 'teardown',
|};
export type TestStatus = 'skip' | 'done';
export type TestStatus = 'skip' | 'done' | 'todo';
export type TestResult = {|
duration: ?number,
errors: Array<FormattedError>,

View File

@ -83,7 +83,7 @@ export type AssertionLocation = {|
path: string,
|};
export type Status = 'passed' | 'failed' | 'skipped' | 'pending';
export type Status = 'passed' | 'failed' | 'skipped' | 'pending' | 'todo';
export type Bytes = number;
export type Milliseconds = number;
@ -118,6 +118,7 @@ export type AggregatedResultWithoutCoverage = {
numPassedTests: number,
numPassedTestSuites: number,
numPendingTests: number,
numTodoTests: number,
numPendingTestSuites: number,
numRuntimeErrorTestSuites: number,
numTotalTests: number,
@ -150,6 +151,7 @@ export type TestResult = {|
numFailingTests: number,
numPassingTests: number,
numPendingTests: number,
numTodoTests: number,
openHandles: Array<Error>,
perfStats: {|
end: Milliseconds,