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

Add support for async matchers #5919

Merged
merged 1 commit into from
Apr 12, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Features

* `[expect]` Add support for async matchers
([#5836](https://github.com/facebook/jest/pull/5919))
* `[expect]` Suggest toContainEqual
([#5948](https://github.com/facebook/jest/pull/5953))
* `[jest-config]` Export Jest's default options
Expand Down
45 changes: 39 additions & 6 deletions docs/ExpectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,45 @@ test('even and odd numbers', () => {
});
```

Matchers should return an object with two keys. `pass` indicates whether there
was a match or not, and `message` provides a function with no arguments that
returns an error message in case of failure. Thus, when `pass` is false,
`message` should return the error message for when `expect(x).yourMatcher()`
fails. And when `pass` is true, `message` should return the error message for
when `expect(x).not.yourMatcher()` fails.
`expect.extends` also supports async matchers. Async matchers return a Promise
so you will need to await the returned value. Let's use an example matcher to
illustrate the usage of them. We are going to implement a very similar matcher
than `toBeDivisibleBy`, only difference is that the divisible number is going to
be pulled from an external source.

```js
expect.extend({
async toBeDivisibleByExternalValue(received) {
const externalValue = await getExternalValueFromRemoteSource();
const pass = received % externalValue == 0;
if (pass) {
return {
message: () =>
`expected ${received} not to be divisible by ${externalValue}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${received} to be divisible by ${externalValue}`,
pass: false,
};
}
},
});

test('is divisible by external value', async () => {
await expect(100).toBeDivisibleByExternalValue();
await expect(101).not.toBeDivisibleByExternalValue();
});
```

Matchers should return an object (or a Promise of an object) with two keys.
`pass` indicates whether there was a match or not, and `message` provides a
function with no arguments that returns an error message in case of failure.
Thus, when `pass` is false, `message` should return the error message for when
`expect(x).yourMatcher()` fails. And when `pass` is true, `message` should
return the error message for when `expect(x).not.yourMatcher()` fails.

These helper functions can be found on `this` inside a custom matcher:

Expand Down
68 changes: 36 additions & 32 deletions flow-typed/npm/jest_v21.x.x.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
* An array that contains all the object instances that have been
* instantiated from this mock function.
*/
instances: Array<TReturn>
instances: Array<TReturn>,
},
/**
* Resets all information stored in the mockFn.mock.calls and
Expand Down Expand Up @@ -45,15 +45,15 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
* will also be executed when the mock is called.
*/
mockImplementation(
fn: (...args: TArguments) => TReturn
fn: (...args: TArguments) => TReturn,
): JestMockFn<TArguments, TReturn>,
/**
* Accepts a function that will be used as an implementation of the mock for
* one call to the mocked function. Can be chained so that multiple function
* calls produce different results.
*/
mockImplementationOnce(
fn: (...args: TArguments) => TReturn
fn: (...args: TArguments) => TReturn,
): JestMockFn<TArguments, TReturn>,
/**
* Just a simple sugar function for returning `this`
Expand All @@ -66,14 +66,14 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
/**
* Sugar for only returning a value once inside your mock
*/
mockReturnValueOnce(value: TReturn): JestMockFn<TArguments, TReturn>
mockReturnValueOnce(value: TReturn): JestMockFn<TArguments, TReturn>,
};

type JestAsymmetricEqualityType = {
/**
* A custom Jasmine equality tester
*/
asymmetricMatch(value: mixed): boolean
asymmetricMatch(value: mixed): boolean,
};

type JestCallsType = {
Expand All @@ -83,21 +83,25 @@ type JestCallsType = {
count(): number,
first(): mixed,
mostRecent(): mixed,
reset(): void
reset(): void,
};

type JestClockType = {
install(): void,
mockDate(date: Date): void,
tick(milliseconds?: number): void,
uninstall(): void
uninstall(): void,
};

type JestMatcherResult = {
type JestMatcherSyncResult = {
message?: string | (() => string),
pass: boolean
pass: boolean,
};

type JestMatcherAsyncResult = Promise<JestMatcherSyncResult>;

type JestMatcherResult = JestMatcherSyncResult | JestMatcherAsyncResult;

type JestMatcher = (actual: any, expected: any) => JestMatcherResult;

type JestPromiseType = {
Expand All @@ -110,7 +114,7 @@ type JestPromiseType = {
* Use resolves to unwrap the value of a fulfilled promise so any other
* matcher can be chained. If the promise is rejected the assertion fails.
*/
resolves: JestExpectType
resolves: JestExpectType,
};

/**
Expand All @@ -133,7 +137,7 @@ type EnzymeMatchersType = {
toIncludeText(text: string): void,
toHaveValue(value: any): void,
toMatchElement(element: React$Element<any>): void,
toMatchSelector(selector: string): void
toMatchSelector(selector: string): void,
};

type JestExpectType = {
Expand Down Expand Up @@ -277,7 +281,7 @@ type JestExpectType = {
* Use .toThrowErrorMatchingSnapshot to test that a function throws a error
* matching the most recent snapshot when it is called.
*/
toThrowErrorMatchingSnapshot(): void
toThrowErrorMatchingSnapshot(): void,
};

type JestObjectType = {
Expand Down Expand Up @@ -329,7 +333,7 @@ type JestObjectType = {
* implementation.
*/
fn<TArguments: $ReadOnlyArray<*>, TReturn>(
implementation?: (...args: TArguments) => TReturn
implementation?: (...args: TArguments) => TReturn,
): JestMockFn<TArguments, TReturn>,
/**
* Determines if the given function is a mocked function.
Expand All @@ -352,7 +356,7 @@ type JestObjectType = {
mock(
moduleName: string,
moduleFactory?: any,
options?: Object
options?: Object,
): JestObjectType,
/**
* Returns the actual module instead of a mock, bypassing all checks on
Expand Down Expand Up @@ -420,32 +424,32 @@ type JestObjectType = {
* Creates a mock function similar to jest.fn but also tracks calls to
* object[methodName].
*/
spyOn(object: Object, methodName: string): JestMockFn<any, any>
spyOn(object: Object, methodName: string): JestMockFn<any, any>,
};

type JestSpyType = {
calls: JestCallsType
calls: JestCallsType,
};

/** Runs this function after every test inside this context */
declare function afterEach(
fn: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void;
/** Runs this function before every test inside this context */
declare function beforeEach(
fn: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void;
/** Runs this function after all tests have finished inside this context */
declare function afterAll(
fn: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void;
/** Runs this function before any tests have started inside this context */
declare function beforeAll(
fn: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void;

/** A context for grouping tests together */
Expand All @@ -463,7 +467,7 @@ declare var describe: {
/**
* Skip running this describe block
*/
skip(name: string, fn: () => void): void
skip(name: string, fn: () => void): void,
};

/** An individual test unit */
Expand All @@ -478,7 +482,7 @@ declare var it: {
(
name: string,
fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void,
/**
* Only run this test
Expand All @@ -490,7 +494,7 @@ declare var it: {
only(
name: string,
fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void,
/**
* Skip running this test
Expand All @@ -502,7 +506,7 @@ declare var it: {
skip(
name: string,
fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void,
/**
* Run the test concurrently
Expand All @@ -514,13 +518,13 @@ declare var it: {
concurrent(
name: string,
fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number
): void
timeout?: number,
): void,
};
declare function fit(
name: string,
fn: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void;
/** An individual test unit */
declare var test: typeof it;
Expand All @@ -538,7 +542,7 @@ declare var expect: {
/** The object that you want to make assertions against */
(value: any): JestExpectType & JestPromiseType & EnzymeMatchersType,
/** Add additional Jasmine matchers to Jest's roster */
extend(matchers: { [name: string]: JestMatcher }): void,
extend(matchers: {[name: string]: JestMatcher}): void,
/** Add a module that formats application-specific data structures. */
addSnapshotSerializer(serializer: (input: Object) => string): void,
assertions(expectedAssertions: number): void,
Expand All @@ -549,7 +553,7 @@ declare var expect: {
objectContaining(value: Object): void,
/** Matches any received string that contains the exact expected string. */
stringContaining(value: string): void,
stringMatching(value: string | RegExp): void
stringMatching(value: string | RegExp): void,
};

// TODO handle return type
Expand All @@ -572,8 +576,8 @@ declare var jasmine: {
createSpy(name: string): JestSpyType,
createSpyObj(
baseName: string,
methodNames: Array<string>
): { [methodName: string]: JestSpyType },
methodNames: Array<string>,
): {[methodName: string]: JestSpyType},
objectContaining(value: Object): void,
stringMatching(value: string): void
stringMatching(value: string): void,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`shows the correct errors in stderr when failing tests 1`] = `
Object {
"rest": "FAIL __tests__/failure.test.js
✕ fail with expected non promise values
✕ fail with expected non promise values and not
✕ fail with expected promise values
✕ fail with expected promise values and not

● fail with expected non promise values

Error
Error: Expected value to have length:
2
Received:
1
received.length:
1

● fail with expected non promise values and not

Error
Error: Expected value to not have length:
2
Received:
1,2
received.length:
2

● fail with expected promise values

Error
Error: Expected value to have length:
2
Received:
1
received.length:
1

● fail with expected promise values and not

Error
Error: Expected value to not have length:
2
Received:
1,2
received.length:
2

",
"summary": "Test Suites: 1 failed, 1 total
Tests: 4 failed, 4 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites matching /failure.test.js/i.
",
}
`;
30 changes: 30 additions & 0 deletions integration-tests/__tests__/expect-async-matcher.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2014-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 SkipOnWindows = require('../../scripts/SkipOnWindows');
const runJest = require('../runJest');
const {extractSummary} = require('../Utils');
const dir = path.resolve(__dirname, '../expect-async-matcher');

SkipOnWindows.suite();
Copy link
Member

Choose a reason for hiding this comment

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

is this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@SimenB I used it because of the filesystem differences with the snapshots. I think that the failures example that you provided is doing it for the same reason.


test('works with passing tests', () => {
const result = runJest(dir, ['success.test.js']);
expect(result.status).toBe(0);
});

test('shows the correct errors in stderr when failing tests', () => {
const result = runJest(dir, ['failure.test.js']);

expect(result.status).toBe(1);
expect(extractSummary(result.stderr)).toMatchSnapshot();
});
Loading