Skip to content

docs: update expect.extend examples #13195

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

Merged
merged 5 commits into from
Aug 31, 2022
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
168 changes: 146 additions & 22 deletions docs/ExpectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ When you're writing tests, you often need to check that values meet certain cond

For additional Jest matchers maintained by the Jest Community check out [`jest-extended`](https://github.com/jest-community/jest-extended).

:::info

The TypeScript examples from this page will only work as documented if you import `expect` from `'@jest/globals'`:

```ts
import {expect} from '@jest/globals';
```

:::

## Methods

import TOCInline from '@theme/TOCInline';
Expand Down Expand Up @@ -37,52 +47,166 @@ The argument to `expect` should be the value that your code produces, and any ar

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
```js tab={"span":3} title="toBeWithinRange.js"
import {expect} from '@jest/globals';

function toBeWithinRange(actual, floor, ceiling) {
if (
typeof actual !== 'number' ||
typeof floor !== 'number' ||
typeof ceiling !== 'number'
) {
throw new Error('These must be of type number!');
}

const pass = actual >= floor && actual <= ceiling;
if (pass) {
return {
message: () =>
`expected ${this.utils.printReceived(
actual,
)} not to be within range ${this.utils.printExpected(
`${floor} - ${ceiling}`,
)}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${this.utils.printReceived(
actual,
)} to be within range ${this.utils.printExpected(
`${floor} - ${ceiling}`,
)}`,
pass: false,
};
}
}

expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
toBeWithinRange,
});
```

```js title="__tests__/ranges.test.js"
import {expect, test} from '@jest/globals';
import '../toBeWithinRange';

test('is within range', () => expect(100).toBeWithinRange(90, 110));

test('is NOT within range', () => expect(101).not.toBeWithinRange(0, 100));

test('asymmetric ranges', () => {
expect({apples: 6, bananas: 3}).toEqual({
apples: expect.toBeWithinRange(1, 10),
bananas: expect.not.toBeWithinRange(11, 20),
});
});
```

```ts title="toBeWithinRange.d.ts"
// optionally add a type declaration, e.g. it enables autocompletion in IDEs
declare module 'expect' {
interface AsymmetricMatchers {
toBeWithinRange(floor: number, ceiling: number): void;
}
interface Matchers<R> {
toBeWithinRange(floor: number, ceiling: number): R;
}
}

export {};
```

```ts tab={"span":2} title="toBeWithinRange.ts"
import {expect} from '@jest/globals';
import type {MatcherFunction} from 'expect';

const toBeWithinRange: MatcherFunction<[floor: unknown, ceiling: unknown]> =
// `floor` and `ceiling` get types from the line above
// it is recommended to type them as `unknown` and to validate the values
function (actual, floor, ceiling) {
if (
typeof actual !== 'number' ||
typeof floor !== 'number' ||
typeof ceiling !== 'number'
) {
throw new Error('These must be of type number!');
}

const pass = actual >= floor && actual <= ceiling;
if (pass) {
return {
message: () =>
`expected ${received} not to be within range ${floor} - ${ceiling}`,
// `this` context will have correct typings
`expected ${this.utils.printReceived(
actual,
)} not to be within range ${this.utils.printExpected(
`${floor} - ${ceiling}`,
)}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${received} to be within range ${floor} - ${ceiling}`,
`expected ${this.utils.printReceived(
actual,
)} to be within range ${this.utils.printExpected(
`${floor} - ${ceiling}`,
)}`,
pass: false,
};
}
},
};

expect.extend({
toBeWithinRange,
});

test('numeric ranges', () => {
expect(100).toBeWithinRange(90, 110);
expect(101).not.toBeWithinRange(0, 100);
declare module 'expect' {
interface AsymmetricMatchers {
toBeWithinRange(floor: number, ceiling: number): void;
}
interface Matchers<R> {
toBeWithinRange(floor: number, ceiling: number): R;
}
}
```

```ts tab title="__tests__/ranges.test.ts"
import {expect, test} from '@jest/globals';
import '../toBeWithinRange';

test('is within range', () => expect(100).toBeWithinRange(90, 110));

test('is NOT within range', () => expect(101).not.toBeWithinRange(0, 100));

test('asymmetric ranges', () => {
expect({apples: 6, bananas: 3}).toEqual({
apples: expect.toBeWithinRange(1, 10),
bananas: expect.not.toBeWithinRange(11, 20),
});
});
```

:::note
:::tip

In TypeScript, when using `@types/jest` for example, you can declare the new `toBeWithinRange` matcher in the imported module like this:
The type declaration of the matcher can live in a `.d.ts` file or in an imported `.ts` module (see JS and TS examples above respectively). If you keep the declaration in a `.d.ts` file, make sure that it is included in the program and that it is a valid module, i.e. it has at least an empty `export {}`.

```ts
interface CustomMatchers<R = unknown> {
toBeWithinRange(floor: number, ceiling: number): R;
}
:::

declare global {
namespace jest {
interface Expect extends CustomMatchers {}
interface Matchers<R> extends CustomMatchers<R> {}
interface InverseAsymmetricMatchers extends CustomMatchers {}
}
}
:::tip

Instead of importing `toBeWithinRange` module to the test file, you can enable the matcher for all tests by moving the `expect.extend` call to a [`setupFilesAfterEnv`](Configuration.md/#setupfilesafterenv-array) script:

```js
import {expect} from '@jest/globals';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tried it out. Jest will transpile setupFilesAfterEnv scripts, so perhaps this can stay in ESM / TS for simplicity.

// remember to export `toBeWithinRange` as well
import {toBeWithinRange} from './toBeWithinRange';

expect.extend({
toBeWithinRange,
});
```

:::
Expand Down
3 changes: 2 additions & 1 deletion examples/expect-extend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"@jest/globals": "workspace:*",
"babel-jest": "workspace:*",
"expect": "workspace:*",
"jest": "workspace:*"
"jest": "workspace:*",
"typescript": "^4.8.2"
},
"scripts": {
"test": "jest"
Expand Down
4 changes: 2 additions & 2 deletions examples/expect-extend/toBeWithinRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import {expect} from '@jest/globals';
import type {MatcherFunction} from 'expect';

const toBeWithinRange: MatcherFunction<[floor: number, ceiling: number]> =
function (actual: unknown, floor: unknown, ceiling: unknown) {
const toBeWithinRange: MatcherFunction<[floor: unknown, ceiling: unknown]> =
function (actual, floor, ceiling) {
Comment on lines -11 to +12
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cleaning up. I remember my thinking that expect.extend could grab types from MatcherFunction generics. Not sure if that was good idea. It involves lots of complexity (also in documentation), but has rather limited usage. For instance, does not work if expect is not imported after calling expect.extend. Fine for something small, but does not work with setupFilesAfterEnv. Better to stick with the declaration – single solution which works in many situations.

if (
typeof actual !== 'number' ||
typeof floor !== 'number' ||
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9392,6 +9392,7 @@ __metadata:
babel-jest: "workspace:*"
expect: "workspace:*"
jest: "workspace:*"
typescript: ^4.8.2
languageName: unknown
linkType: soft

Expand Down