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 utility library and you're frequently asserting that numbers appear within particular ranges of other numbers. You could abstract that into a toBeWithinRange matcher:

  1. expect.extend({
  2. toBeWithinRange(received, floor, ceiling) {
  3. const pass = received >= floor && received <= ceiling;
  4. if (pass) {
  5. return {
  6. message: () =>
  7. `expected ${received} not to be within range ${floor} - ${ceiling}`,
  8. pass: true,
  9. };
  10. } else {
  11. return {
  12. message: () =>
  13. `expected ${received} to be within range ${floor} - ${ceiling}`,
  14. pass: false,
  15. };
  16. }
  17. },
  18. });
  19. test('numeric ranges', () => {
  20. expect(100).toBeWithinRange(90, 110);
  21. expect(101).not.toBeWithinRange(0, 100);
  22. expect({apples: 6, bananas: 3}).toEqual({
  23. apples: expect.toBeWithinRange(1, 10),
  24. bananas: expect.not.toBeWithinRange(11, 20),
  25. });
  26. });

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.

  1. expect.extend({
  2. async toBeDivisibleByExternalValue(received) {
  3. const externalValue = await getExternalValueFromRemoteSource();
  4. const pass = received % externalValue == 0;
  5. if (pass) {
  6. return {
  7. message: () =>
  8. `expected ${received} not to be divisible by ${externalValue}`,
  9. pass: true,
  10. };
  11. } else {
  12. return {
  13. message: () =>
  14. `expected ${received} to be divisible by ${externalValue}`,
  15. pass: false,
  16. };
  17. }
  18. },
  19. });
  20. test('is divisible by external value', async () => {
  21. await expect(100).toBeDivisibleByExternalValue();
  22. await expect(101).not.toBeDivisibleByExternalValue();
  23. });

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):

  1. expect.extend({
  2. yourMatcher(x, y, z) {
  3. return {
  4. pass: true,
  5. message: () => '',
  6. };
  7. },
  8. });

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

this.isNot

A boolean to let you know this matcher was called with the negated .not modifier allowing you to flip your assertion and display a clear and correct matcher hint (see example code).

this.promise

A string allowing you to display a clear and correct matcher hint:

  • 'rejects' if matcher was called with the promise .rejects modifier
  • 'resolves' if matcher was called with the promise .resolves modifier
  • '' if matcher was not called with a promise modifier

this.equals(a, b)

This is a deep-equality function that will return true if two objects have the same values (recursively).

this.expand

A boolean to let you know this matcher was called with an expand option. When Jest is called with the —expand flag, this.expand can be used to determine if Jest is expected to show full diffs and errors.

this.utils

There are a number of helpful tools exposed on this.utils primarily consisting of the exports from jest-matcher-utils.

The most useful ones are matcherHint, printExpected and printReceived to format the error messages nicely. For example, take a look at the implementation for the toBe matcher:

  1. const diff = require('jest-diff');
  2. expect.extend({
  3. toBe(received, expected) {
  4. const options = {
  5. comment: 'Object.is equality',
  6. isNot: this.isNot,
  7. promise: this.promise,
  8. };
  9. const pass = Object.is(received, expected);
  10. const message = pass
  11. ? () =>
  12. this.utils.matcherHint('toBe', undefined, undefined, options) +
  13. '\n\n' +
  14. `Expected: ${this.utils.printExpected(expected)}\n` +
  15. `Received: ${this.utils.printReceived(received)}`
  16. : () => {
  17. const difference = diff(expected, received, {
  18. expand: this.expand,
  19. });
  20. return (
  21. this.utils.matcherHint('toBe', undefined, undefined, options) +
  22. '\n\n' +
  23. (difference && difference.includes('- Expect')
  24. ? `Difference:\n\n${diffString}`
  25. : `Expected: ${this.utils.printExpected(expected)}\n` +
  26. `Received: ${this.utils.printReceived(received)}`)
  27. );
  28. };
  29. return {actual: received, message, pass};
  30. },
  31. });

This will print something like this:

  1. expect(received).toBe(expected)
  2. Expected value to be (using Object.is):
  3. "banana"
  4. Received:
  5. "apple"

When an assertion fails, the error message should give as much signal as necessary to the user so they can resolve their issue quickly. You should craft a precise failure message to make sure users of your custom assertions have a good developer experience.

Custom snapshot matchers

To use snapshot testing inside of your custom matcher you can import jest-snapshot and use it from within your matcher.

Here's a simple snapshot matcher that trims a string to store for a given length, .toMatchTrimmedSnapshot(length):

  1. const {toMatchSnapshot} = require('jest-snapshot');
  2. expect.extend({
  3. toMatchTrimmedSnapshot(received, length) {
  4. return toMatchSnapshot.call(
  5. this,
  6. received.substring(0, length),
  7. 'toMatchTrimmedSnapshot',
  8. );
  9. },
  10. });
  11. it('stores only 10 characters', () => {
  12. expect('extra long string oh my gerd').toMatchTrimmedSnapshot(10);
  13. });
  14. /*
  15. Stored snapshot will look like:
  16. exports[`stores only 10 characters: toMatchTrimmedSnapshot 1`] = `"extra long"`;
  17. */