diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 2793a77926e9..8c19aa282c37 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -202,6 +202,24 @@ test('prepareState prepares a valid state', () => { The `expect.assertions(1)` call ensures that the `prepareState` callback actually gets called. +### `expect.hasAssertions()` + +`expect.hasAssertions()` verifies that at least one assertion is called during a test. This is often useful when testing asynchronous code, in order to make sure that assertions in a callback actually got called. + +For example, let's say that we have a few functions that all deal with state. `prepareState` calls a callback with a state object, `validateState` runs on that state object, and `waitOnState` returns a promise that waits until all `prepareState` callbacks complete. We can test this with: + +```js +test('prepareState prepares a valid state', () => { + expect.hasAssertions(); + prepareState(state => { + expect(validateState(state)).toBeTruthy(); + }); + return waitOnState(); +}); +``` + +The `expect.hasAssertions()` call ensures that the `prepareState` callback actually gets called. + ### `expect.objectContaining(object)` `expect.objectContaining(object)` matches any received object that recursively matches the expected properties. That is, the expected object is a **subset** of the received object. Therefore, it matches a received object which contains properties that are **not** in the expected object. diff --git a/integration_tests/__tests__/__snapshots__/failures-test.js.snap b/integration_tests/__tests__/__snapshots__/failures-test.js.snap index 511b1fda427f..6f61df20fc78 100644 --- a/integration_tests/__tests__/__snapshots__/failures-test.js.snap +++ b/integration_tests/__tests__/__snapshots__/failures-test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`throwing not Error objects 1`] = ` +exports[`not throwing Error objects 1`] = ` Object { "rest": " FAIL __tests__/throw-number-test.js ● Test suite failed to run @@ -17,7 +17,7 @@ Ran all test suites matching \\"throw-number-test.js\\". } `; -exports[`throwing not Error objects 2`] = ` +exports[`not throwing Error objects 2`] = ` Object { "rest": " FAIL __tests__/throw-string-test.js ● Test suite failed to run @@ -35,7 +35,7 @@ Ran all test suites matching \\"throw-string-test.js\\". } `; -exports[`throwing not Error objects 3`] = ` +exports[`not throwing Error objects 3`] = ` Object { "rest": " FAIL __tests__/throw-object-test.js ● Test suite failed to run @@ -52,7 +52,7 @@ Ran all test suites matching \\"throw-object-test.js\\". } `; -exports[`throwing not Error objects 4`] = ` +exports[`not throwing Error objects 4`] = ` Object { "rest": " FAIL __tests__/assertion-count-test.js ● .assertions() › throws @@ -70,7 +70,6 @@ Object { Expected two assertions to be called but only received one assertion call. - at addAssertionErrors (../../packages/jest-jasmine2/build/setup-jest-globals.js:64:21) ● .assertions() › throws on redeclare of assertion count @@ -87,16 +86,24 @@ Object { Expected zero assertions to be called but only received one assertion call. - at addAssertionErrors (../../packages/jest-jasmine2/build/setup-jest-globals.js:64:21) + + ● .hasAssertions() › throws when there are not assertions + + expect.hasAssertions() + + Expected at least one assertion to be called but received none. + .assertions() ✕ throws ✕ throws on redeclare of assertion count ✕ throws on assertion + .hasAssertions() + ✕ throws when there are not assertions ", "summary": "Test Suites: 1 failed, 1 total -Tests: 3 failed, 3 total +Tests: 4 failed, 4 total Snapshots: 0 total Time: <> Ran all test suites matching \\"assertion-count-test.js\\". diff --git a/integration_tests/__tests__/failures-test.js b/integration_tests/__tests__/failures-test.js index ff5626343191..607446088993 100644 --- a/integration_tests/__tests__/failures-test.js +++ b/integration_tests/__tests__/failures-test.js @@ -25,11 +25,12 @@ const stripInconsistentStackLines = summary => { .replace(/\n^.*process\._tickCallback.*$/gm, '') .replace(/\n^.*_throws.*$/gm, '') .replace(/\n^.*Function\..*(throws|doesNotThrow).*$/gm, '') + .replace(/\n^.*setup-jest-globals\.js.*$/gm, '') .replace(/(\n^.*Object.)\.test(.*$)/gm, '$1$2'); return summary; }; -test('throwing not Error objects', () => { +test('not throwing Error objects', () => { let stderr; stderr = runJest(dir, ['throw-number-test.js']).stderr; expect(stripInconsistentStackLines(extractSummary(stderr))).toMatchSnapshot(); diff --git a/integration_tests/failures/__tests__/assertion-count-test.js b/integration_tests/failures/__tests__/assertion-count-test.js index 58f6250b03f0..cdbdda0034db 100644 --- a/integration_tests/failures/__tests__/assertion-count-test.js +++ b/integration_tests/failures/__tests__/assertion-count-test.js @@ -24,8 +24,16 @@ const noAssertions = () => { expect(true).toBeTruthy(); }; +const hasNoAssertions = () => { + expect.hasAssertions(); +}; + describe('.assertions()', () => { it('throws', throws); it('throws on redeclare of assertion count', redeclare); it('throws on assertion', noAssertions); }); + +describe('.hasAssertions()', () => { + it('throws when there are not assertions', hasNoAssertions); +}); diff --git a/packages/jest-jasmine2/src/jest-expect.js b/packages/jest-jasmine2/src/jest-expect.js index 8de270e03c0a..a3b8c9fcbec2 100644 --- a/packages/jest-jasmine2/src/jest-expect.js +++ b/packages/jest-jasmine2/src/jest-expect.js @@ -32,8 +32,7 @@ module.exports = (config: {expand: boolean}) => { expand: config.expand, }); expect.extend({toMatchSnapshot, toThrowErrorMatchingSnapshot}); - - expect.addSnapshotSerializer = addSerializer; + (expect: Object).addSnapshotSerializer = addSerializer; const jasmine = global.jasmine; jasmine.anything = expect.anything; diff --git a/packages/jest-jasmine2/src/setup-jest-globals.js b/packages/jest-jasmine2/src/setup-jest-globals.js index f26de34028c7..57bbbc0b643e 100644 --- a/packages/jest-jasmine2/src/setup-jest-globals.js +++ b/packages/jest-jasmine2/src/setup-jest-globals.js @@ -51,28 +51,53 @@ const addSuppressedErrors = result => { }; function addAssertionErrors(result) { - const {assertionCalls, assertionsExpected} = getState(); + const { + assertionCalls, + expectedAssertionsNumber, + isExpectingAssertions, + } = getState(); setState({ assertionCalls: 0, - assertionsExpected: null, + expectedAssertionsNumber: null, }); if ( - typeof assertionsExpected === 'number' && - assertionCalls !== assertionsExpected + typeof expectedAssertionsNumber === 'number' && + assertionCalls !== expectedAssertionsNumber ) { - const expected = EXPECTED_COLOR(pluralize('assertion', assertionsExpected)); + const expected = EXPECTED_COLOR( + pluralize('assertion', expectedAssertionsNumber), + ); const message = new Error( - matcherHint('.assertions', '', assertionsExpected, { + matcherHint('.assertions', '', String(expectedAssertionsNumber), { isDirectExpectCall: true, }) + '\n\n' + `Expected ${expected} to be called but only received ` + - `${RECEIVED_COLOR(pluralize('assertion call', assertionCalls))}.`, + RECEIVED_COLOR(pluralize('assertion call', assertionCalls || 0)) + + '.', ).stack; result.status = 'failed'; result.failedExpectations.push({ actual: assertionCalls, - expected: assertionsExpected, + expected: expectedAssertionsNumber, + message, + passed: false, + }); + } + if (isExpectingAssertions && assertionCalls === 0) { + const expected = EXPECTED_COLOR('at least one assertion'); + const received = RECEIVED_COLOR('received none'); + const message = new Error( + matcherHint('.hasAssertions', '', '', { + isDirectExpectCall: true, + }) + + '\n\n' + + `Expected ${expected} to be called but ${received}.`, + ).stack; + result.status = 'failed'; + result.failedExpectations.push({ + actual: 'none', + expected: 'at least one', message, passed: false, }); diff --git a/packages/jest-matchers/src/__tests__/assertion-counts-test.js b/packages/jest-matchers/src/__tests__/assertion-counts-test.js index b01a71ce7ad5..c603a0b8122e 100644 --- a/packages/jest-matchers/src/__tests__/assertion-counts-test.js +++ b/packages/jest-matchers/src/__tests__/assertion-counts-test.js @@ -29,3 +29,10 @@ describe('.assertions()', () => { jestExpect.assertions(0); }); }); + +describe('.hasAssertions()', () => { + it('does not throw if there is an assertion', () => { + jestExpect.hasAssertions(); + jestExpect('a').toBe('a'); + }); +}); diff --git a/packages/jest-matchers/src/index.js b/packages/jest-matchers/src/index.js index 8050a009a3e4..3e6340cc3d6a 100644 --- a/packages/jest-matchers/src/index.js +++ b/packages/jest-matchers/src/index.js @@ -15,7 +15,6 @@ import type { ExpectationObject, ExpectationResult, MatcherContext, - MatcherState, MatchersObject, RawMatcherFn, ThrowingMatcherFn, @@ -56,14 +55,15 @@ if (!global[GLOBAL_STATE]) { matchers: Object.create(null), state: { assertionCalls: 0, - assertionsExpected: null, + expectedAssertionsNumber: null, + isExpectingAssertions: false, suppressedErrors: [], }, }, }); } -const expect: Expect = (actual: any): ExpectationObject => { +const expect = (actual: any): ExpectationObject => { const allMatchers = global[GLOBAL_STATE].matchers; const expectation = { not: {}, @@ -275,13 +275,16 @@ expect.extend(matchers); expect.extend(spyMatchers); expect.extend(toThrowMatchers); -expect.assertions = (expected: number) => - (global[GLOBAL_STATE].state.assertionsExpected = expected); - -expect.setState = (state: MatcherState) => { +expect.addSnapshotSerializer = () => void 0; +expect.assertions = (expected: number) => { + global[GLOBAL_STATE].state.expectedAssertionsNumber = expected; +}; +expect.hasAssertions = () => { + global[GLOBAL_STATE].state.isExpectingAssertions = true; +}; +expect.setState = (state: Object) => { Object.assign(global[GLOBAL_STATE].state, state); }; - expect.getState = () => global[GLOBAL_STATE].state; -module.exports = expect; +module.exports = (expect: Expect); diff --git a/packages/pretty-format/src/plugins/AsymmetricMatcher.js b/packages/pretty-format/src/plugins/AsymmetricMatcher.js index 766627cd1702..bfb046f4bb41 100644 --- a/packages/pretty-format/src/plugins/AsymmetricMatcher.js +++ b/packages/pretty-format/src/plugins/AsymmetricMatcher.js @@ -4,6 +4,7 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. + * * @flow */ diff --git a/types/Matchers.js b/types/Matchers.js index 37ba2d345522..30f8e18b39f7 100644 --- a/types/Matchers.js +++ b/types/Matchers.js @@ -26,13 +26,34 @@ export type ThrowingMatcherFn = (actual: any) => void; export type PromiseMatcherFn = (actual: any) => Promise; export type MatcherContext = {isNot: boolean}; export type MatcherState = { - assertionCalls?: number, - assertionsExpected?: ?number, + assertionCalls: number, + isExpectingAssertions: ?boolean, + expectedAssertionsNumber: ?number, currentTestName?: string, + expand?: boolean, + suppressedErrors: Array, testPath?: Path, }; + +export type AsymmetricMatcher = Object; export type MatchersObject = {[id: string]: RawMatcherFn}; -export type Expect = (expected: any) => ExpectationObject; +export type Expect = { + (expected: any): ExpectationObject, + addSnapshotSerializer(any): void, + assertions(number): void, + extend(any): void, + getState(): MatcherState, + hasAssertions(): void, + setState(Object): void, + + any(expectedObject: any): AsymmetricMatcher, + anything(): AsymmetricMatcher, + arrayContaining(sample: Array): AsymmetricMatcher, + objectContaining(sample: Object): AsymmetricMatcher, + stringContaining(expected: string): AsymmetricMatcher, + stringMatching(expected: string | RegExp): AsymmetricMatcher, +}; + export type ExpectationObject = { [id: string]: ThrowingMatcherFn, resolves: {