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

fix: support variadic custom asymmetric matchers #6898

Merged
merged 1 commit into from
Aug 29, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Fixes

- `[expect]` Fix variadic custom asymmetric matchers ([#6898](https://github.com/facebook/jest/pull/6898))
- `[jest-cli]` Fix incorrect `testEnvironmentOptions` warning ([#6852](https://github.com/facebook/jest/pull/6852))
- `[jest-each`] Prevent done callback being supplied to describe ([#6843](https://github.com/facebook/jest/pull/6843))
- `[jest-config`] Better error message for a case when a preset module was found, but no `jest-preset.js` or `jest-preset.json` at the root ([#6863](https://github.com/facebook/jest/pull/6863))
Expand Down
40 changes: 29 additions & 11 deletions docs/ExpectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,41 @@ The argument to `expect` should be the value that your code produces, and any ar

### `expect.extend(matchers)`

You can use `expect.extend` to add your own matchers to Jest. For example, let's say that you're testing a number theory library and you're frequently asserting that numbers are divisible by other numbers. You could abstract that into a `toBeDivisibleBy` matcher:
You can use `expect.extend` to add your own matchers to Jest. For example, let's say that you're testing a number utility library and you're frequently asserting that numbers appear within particular ranges of other numbers. You could abstract that into a `toBeWithinRange` matcher:

```js
expect.extend({
toBeDivisibleBy(received, argument) {
const pass = received % argument == 0;
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
if (pass) {
return {
message: () =>
`expected ${received} not to be divisible by ${argument}`,
`expected ${received} not to be within range ${floor} - ${ceiling}`,
pass: true,
};
} else {
return {
message: () => `expected ${received} to be divisible by ${argument}`,
message: () =>
`expected ${received} to be within range ${floor} - ${ceiling}`,
pass: false,
};
}
},
});

test('even and odd numbers', () => {
expect(100).toBeDivisibleBy(2);
expect(101).not.toBeDivisibleBy(2);
test('numeric ranges', () => {
expect(100).toBeWithinRange(90, 110);
expect(101).not.toBeWithinRange(0, 100);
expect({apples: 6, bananas: 3}).toEqual({
apples: expect.toBeDivisibleBy(2),
bananas: expect.not.toBeDivisibleBy(2),
apples: expect.toBeWithinRange(1, 10),
bananas: expect.not.toBeWithinRange(11, 20),
});
});
```

`expect.extend` 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.
#### Async Matchers

`expect.extend` 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 matcher called `toBeDivisibleByExternalValue`, where the divisible number is going to be pulled from an external source.

```js
expect.extend({
Expand Down Expand Up @@ -91,8 +94,23 @@ test('is divisible by external value', async () => {
});
```

#### Custom Matchers API

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.

Matchers are called with the argument passed to `expect(x)` followed by the arguments passed to `.yourMatcher(y, z)`:

```js
expect.extend({
yourMatcher(x, y, z) {
return {
pass: true,
message: '',
};
},
});
```

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

#### `this.isNot`
Expand Down
46 changes: 43 additions & 3 deletions packages/expect/src/__tests__/__snapshots__/extend.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`defines asymmetric matchers 1`] = `
exports[`defines asymmetric unary matchers 1`] = `
"<dim>expect(</><red>received</><dim>).toEqual(</><green>expected</><dim>)</>

Expected value to equal:
Expand All @@ -19,7 +19,7 @@ Difference:
<dim> }</>"
`;

exports[`defines asymmetric matchers that can be prefixed by not 1`] = `
exports[`defines asymmetric unary matchers that can be prefixed by not 1`] = `
"<dim>expect(</><red>received</><dim>).toEqual(</><green>expected</><dim>)</>

Expected value to equal:
Expand All @@ -38,6 +38,46 @@ Difference:
<dim> }</>"
`;

exports[`is available globally 1`] = `"expected 15 to be divisible by 2"`;
exports[`defines asymmetric variadic matchers 1`] = `
"<dim>expect(</><red>received</><dim>).toEqual(</><green>expected</><dim>)</>

Expected value to equal:
<green>{\\"value\\": toBeWithinRange<4, 11>}</>
Received:
<red>{\\"value\\": 3}</>

Difference:

<green>- Expected</>
<red>+ Received</>

<dim> Object {</>
<green>- \\"value\\": toBeWithinRange<4, 11>,</>
<red>+ \\"value\\": 3,</>
<dim> }</>"
`;

exports[`defines asymmetric variadic matchers that can be prefixed by not 1`] = `
"<dim>expect(</><red>received</><dim>).toEqual(</><green>expected</><dim>)</>

Expected value to equal:
<green>{\\"value\\": not.toBeWithinRange<1, 3>}</>
Received:
<red>{\\"value\\": 2}</>

Difference:

<green>- Expected</>
<red>+ Received</>

<dim> Object {</>
<green>- \\"value\\": not.toBeWithinRange<1, 3>,</>
<red>+ \\"value\\": 2,</>
<dim> }</>"
`;

exports[`is available globally when matcher is unary 1`] = `"expected 15 to be divisible by 2"`;

exports[`is available globally when matcher is variadic 1`] = `"expected 15 to be within range 1 - 3"`;

exports[`is ok if there is no message specified 1`] = `"<red>No message was specified for this matcher.</>"`;
45 changes: 42 additions & 3 deletions packages/expect/src/__tests__/extend.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ jestExpect.extend({

return {message, pass};
},
toBeWithinRange(actual, floor, ceiling) {
const pass = actual >= floor && actual <= ceiling;
const message = pass
? () => `expected ${actual} not to be within range ${floor} - ${ceiling}`
: () => `expected ${actual} to be within range ${floor} - ${ceiling}`;

return {message, pass};
},
});

it('is available globally', () => {
it('is available globally when matcher is unary', () => {
jestExpect(15).toBeDivisibleBy(5);
jestExpect(15).toBeDivisibleBy(3);
jestExpect(15).not.toBeDivisibleBy(6);
Expand All @@ -32,6 +40,15 @@ it('is available globally', () => {
).toThrowErrorMatchingSnapshot();
});

it('is available globally when matcher is variadic', () => {
jestExpect(15).toBeWithinRange(10, 20);
jestExpect(15).not.toBeWithinRange(6);

jestExpect(() =>
jestExpect(15).toBeWithinRange(1, 3),
).toThrowErrorMatchingSnapshot();
});

it('exposes matcherUtils in context', () => {
jestExpect.extend({
_shouldNotError(actual, expected) {
Expand Down Expand Up @@ -78,7 +95,7 @@ it('exposes an equality function to custom matchers', () => {
expect(() => jestExpect().toBeOne()).not.toThrow();
});

it('defines asymmetric matchers', () => {
it('defines asymmetric unary matchers', () => {
expect(() =>
jestExpect({value: 2}).toEqual({value: jestExpect.toBeDivisibleBy(2)}),
).not.toThrow();
Expand All @@ -87,11 +104,33 @@ it('defines asymmetric matchers', () => {
).toThrowErrorMatchingSnapshot();
});

it('defines asymmetric matchers that can be prefixed by not', () => {
it('defines asymmetric unary matchers that can be prefixed by not', () => {
expect(() =>
jestExpect({value: 2}).toEqual({value: jestExpect.not.toBeDivisibleBy(2)}),
).toThrowErrorMatchingSnapshot();
expect(() =>
jestExpect({value: 3}).toEqual({value: jestExpect.not.toBeDivisibleBy(2)}),
).not.toThrow();
});

it('defines asymmetric variadic matchers', () => {
expect(() =>
jestExpect({value: 2}).toEqual({value: jestExpect.toBeWithinRange(1, 3)}),
).not.toThrow();
expect(() =>
jestExpect({value: 3}).toEqual({value: jestExpect.toBeWithinRange(4, 11)}),
).toThrowErrorMatchingSnapshot();
});

it('defines asymmetric variadic matchers that can be prefixed by not', () => {
expect(() =>
jestExpect({value: 2}).toEqual({
value: jestExpect.not.toBeWithinRange(1, 3),
}),
).toThrowErrorMatchingSnapshot();
expect(() =>
jestExpect({value: 3}).toEqual({
value: jestExpect.not.toBeWithinRange(5, 7),
}),
).not.toThrow();
});
16 changes: 9 additions & 7 deletions packages/expect/src/jest_matchers_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,18 @@ export const setMatchers = (
// expect is defined

class CustomMatcher extends AsymmetricMatcher {
sample: any;
sample: Array<any>;

constructor(sample: any, inverse: boolean = false) {
constructor(inverse: boolean = false, ...sample: Array<any>) {
super();
this.sample = sample;
this.inverse = inverse;
this.sample = sample;
}

asymmetricMatch(other: any) {
const {pass} = ((matcher(
(other: any),
(this.sample: any),
...(this.sample: any),
): any): SyncExpectationResult);

return this.inverse ? !pass : pass;
Expand All @@ -85,15 +85,17 @@ export const setMatchers = (
}

toAsymmetricMatcher() {
return `${this.toString()}<${this.sample}>`;
return `${this.toString()}<${this.sample.join(', ')}>`;
}
}

expect[key] = (sample: any) => new CustomMatcher(sample);
expect[key] = (...sample: Array<any>) =>
new CustomMatcher(false, ...sample);
if (!expect.not) {
expect.not = {};
}
expect.not[key] = (sample: any) => new CustomMatcher(sample, true);
expect.not[key] = (...sample: Array<any>) =>
new CustomMatcher(true, ...sample);
}
});

Expand Down