164

Image following test case:

it('valid emails checks', () => {
  ['abc@y.com', 'a@b.nz'/*, ...*/].map(mail => {
    expect(isValid(mail)).toBe(true);
  });
});

I would like to add auto-generated message for each email like Email 'f@f.com' should be valid so that it's easy to find failing test cases.

Something like:

// .map(email =>
expect(isValid(email), `Email ${email} should be valid`).toBe(true);

Is it possible in Jest ?

In Chai it was possible to do with second parameter like expect(value, 'custom fail message').to.be... and in Jasmine seems like it's done with .because clause. But cannot find solution in Jest.

Jurosh
  • 6,984
  • 7
  • 40
  • 51

13 Answers13

80

You try this lib that extends jest: https://github.com/mattphillips/jest-expect-message

test('returns 2 when adding 1 and 1', () => {
  expect(1 + 1, 'Woah this should be 2!').toBe(3);
});
Klesun
  • 12,280
  • 5
  • 59
  • 52
Alexey Volkov
  • 978
  • 6
  • 4
  • 4
    This is a very clean way and should be preferred to try & catch solutions. – Pieter De Bie Nov 10 '19 at 06:17
  • 1
    This does not seem to be accurate with TS – coler-j Oct 23 '20 at 15:15
  • 1
    This will fail: `expect()` takes at most one argument. – Marc Feb 18 '21 at 08:48
  • 2
    I am using this library with typescript and it works flawlessly – starikcetin Mar 14 '21 at 01:19
  • 3
    @Marc Make sure you have followed the [Setup](https://github.com/mattphillips/jest-expect-message#setup) instructions for `jest-expect-message`. Jest needs to be configured to use that module. – cybersam Apr 28 '21 at 18:32
  • 7
    To work with typescript, make sure to also install the corresponding types `npm i jest-expect-message @types/jest-expect-message` – PencilBow Oct 19 '21 at 11:17
  • You need to add this to your module.exports object in jest.config.js: setupFilesAfterEnv: ['jest-expect-message'], – A Fader Darkly Oct 25 '21 at 13:29
  • 4
    Doesn't work with jest 27+. There is an [open issue](https://github.com/mattphillips/jest-expect-message/pull/40) though. – Neeraj Nov 09 '21 at 10:59
  • 1
    This library hasn't been updated since 2018. Appears to be abandoned. Someone did create a drop-in replacement for it, though: https://www.npmjs.com/package/jest-27-expect-message – Matthew Souther Aug 23 '22 at 23:12
  • 1
    @Neeraj Looks like it closed 14 hours ago. "Fix is available in: https://www.npmjs.com/package/jest-expect-message/v/1.0.4" – Damien Golding Sep 09 '22 at 07:27
  • That would be a nice solution, but sadly it doesn't work. It's type definitions are broken. – panzi Apr 26 '23 at 17:56
58

I don't think it's possible to provide a message like that. But you could define your own matcher.

For example you could create a toBeValid(validator) matcher:

expect.extend({
  toBeValid(received, validator) {
    if (validator(received)) {
      return {
        message: () => `Email ${received} should NOT be valid`,
        pass: true
      };
    } else {
      return {
        message: () => `Email ${received} should be valid`,
        pass: false
      };
    }
  }
});

And then you use it like this:

expect(mail).toBeValid(isValid);

Note: toBeValid returns a message for both cases (success and failure), because it allows you to use .not. The test will fail with the corresponding message depending on whether you want it to pass the validation.

expect(mail).toBeValid(isValid);
// pass === true: Test passes
// pass === false: Failure: Email ... should be valid

expect(mail).not.toBeValid(isValid);
// pass === true: Failure: Email ... should NOT be valid
// pass === false: Test passes
skyclo
  • 3
  • 1
  • 4
Michael Jungo
  • 31,583
  • 3
  • 91
  • 84
  • That's great thanks, one question - when using this in some file, it's local for that test file right ? If I would like to have that function in some global should I use `beforeAll` then ? – Jurosh Aug 16 '17 at 19:00
  • 1
    I'm not entirely sure if it's only for the file, but if it's available throughout the test run, it probably depends on which file is executed first and when tests are run in parallel, that becomes a problem. But what you could do, is export the `toBeValid` function in a helper file and import it and register it with `expect.extend({ toBeValid })` wherever you need it. – Michael Jungo Aug 16 '17 at 19:51
  • 2
    Man, I'm not going to knock your answer, but I can't believe this is missing from jest matchers. This is a fundamental concept. – Daniel Kaplan Aug 30 '21 at 15:58
38

Although it's not a general solution, for the common case of wanting a custom exception message to distinguish items in a loop, you can instead use Jest's test.each.

For example, your sample code:

it('valid emails checks', () => {
  ['abc@y.com', 'a@b.nz'/*, ...*/].map(mail => {
    expect(isValid(mail)).toBe(true);
  });
});

Could instead become

test.each(['abc@y.com', 'a@b.nz'/*, ...*/])(
    'checks that email %s is valid',
    mail => {
        expect(isValid(mail)).toBe(true);
    }
);
Jarno
  • 6,243
  • 3
  • 42
  • 57
Josh Kelley
  • 56,064
  • 19
  • 146
  • 246
36

I did this in some code I was writing by putting my it blocks inside forEach.

By doing this, I was able to achieve a very good approximation of what you're describing.

Pros:

  • Excellent "native" error reports
  • Counts the assertion as its own test
  • No plugins needed.

Here's what your code would look like with my method:


// you can't nest "it" blocks within each other,
// so this needs to be inside a describe block. 
describe('valid emails checks', () => {
  ['abc@y.com', 'a@b.nz'/*, ...*/].forEach(mail => {
    // here is where the magic happens
    it(`accepts ${mail} as a valid email`, () => {
      expect(isValid(mail)).toBe(true);
    })
  });
});

Errors then show up like this.

Notice how nice these are!

 FAIL  path/to/your.test.js
  ● valid emails checks › accepts abc@y.com as a valid email

    expect(received).toBe(expected)

    Expected: "abc@y.com"
    Received: "xyz@y.com"

      19 |    // here is where the magic happens
      20 |    it(`accepts ${mail} as a valid email`, () => {
    > 21 |      expect(isValid(mail)).toBe(true);
                                       ^
      22 |    })
James McMahon
  • 48,506
  • 64
  • 207
  • 283
Monarch Wadia
  • 4,400
  • 4
  • 36
  • 37
  • 1
    I remember something similar is possible in Ruby, and it's nice to find that Jest supports it too. – Hew Wolff Oct 13 '21 at 02:47
  • 1
    I find this construct pretty powerful, it's strange that this answer is so neglected :) – Slava Fomin II Nov 20 '21 at 07:51
  • 2
    How did the expected and received become the emails? isn't the expected supposed to be "true"? – ProblemsLoop Dec 28 '21 at 16:02
  • 2
    Your solution is Josh Kelly's one, with inappropriate syntax. Use it.each(yourArray) instead (which is valid since early 2020 at least). – Mozgor Mar 15 '22 at 14:30
  • Thanks for your feedback Mozgor. Both approaches are valid and work just fine. The advantage of Josh Kelly's approach is that templating is easier with `test.each`. The advantage of my approach is that the code is much more flexible and intuitive. As a contrived example, each successive test's input can be derived based on the previous test, while still printing appropriate test failure messages. It's clean and workmanlike. I don't think 'inappropriate syntax' is a valid critique. Instead, we're just using a different approach with different pros and cons. There is no silver bullet. – Monarch Wadia Dec 01 '22 at 21:02
  • As @ProblemsLoop pointed out, the `Expected` and `Received` examples don't make sense. All you get is `Expected: true\nRecieved: false`. – ericP Mar 30 '23 at 16:30
18

You can use try-catch:

try {
    expect(methodThatReturnsBoolean(inputValue)).toBeTruthy();
}
catch (e) {
    throw new Error(`Something went wrong with value ${JSON.stringify(inputValue)}`, e);
}
Mikk
  • 551
  • 4
  • 9
  • 17
    This is solution is a bad idea, you can't make a difference when the tests failed because the return was false or `methodThatReturnsBoolean` thrown an exception. – dave008 Aug 30 '19 at 03:01
  • 1
    @dave008, yes both cases fail the test, but the error message is very explanatory and dependent on what went wrong. – Mikk Sep 01 '19 at 16:14
  • @Marc you must have a problem with your code -- in the example there is only one parameter/value given to the `expect` function. – Mikk Feb 17 '21 at 17:20
7

Just had to deal with this myself I think I'll make a PR to it possibly: But this could work with whatever you'd like. Basically, you make a custom method that allows the curried function to have a custom message as a third parameter.

It's important to remember that expect will set your first parameter (the one that goes into expect(akaThisThing) as the first parameter of your custom function.

For a generic Jest Message extender which can fit whatever Jest matching you'd already be able to use and then add a little bit of flourish:

expect.extend({
  toEqualMessage(received, expected, custom) {
    let pass = true;
    let message = '';
    try {
      // use the method from Jest that you want to extend
      // in a try block
      expect(received).toEqual(expected);
    } catch (e) {
      pass = false;
      message = `${e}\nCustom Message: ${custom}`;
    }
    return {
      pass,
      message: () => message,
      expected,
      received
    };
  }
});

declare global {
  // eslint-disable-next-line @typescript-eslint/no-namespace
  namespace jest {
    // eslint-disable-next-line @typescript-eslint/naming-convention
    interface Matchers<R> {
      toEqualMessage(a: unknown, b: string): R;
    }
  }
}

Will show up like:

    Error: expect(received).toEqual(expected) // deep equality

    Expected: 26
    Received: 13
    Custom Message: Sad Message Indicating failure :(

For specific look inside the expect(actualObject).toBe() in case that helps your use case:

import diff from 'jest-diff'

expect.extend({
toBeMessage (received, expected, msg) {
  const pass = expected === received
  const message = pass
? () => `${this.utils.matcherHint('.not.toBe')}\n\n` +
        `Expected value to not be (using ===):\n` +
        `  ${this.utils.printExpected(expected)}\n` +
        `Received:\n` +
        `  ${this.utils.printReceived(received)}`
      : () => {
        const diffString = diff(expected, received, {
          expand: this.expand
        })
        return `${this.utils.matcherHint('.toBe')}\n\n` +
        `Expected value to be (using ===):\n` +
        `  ${this.utils.printExpected(expected)}\n` +
        `Received:\n` +
        `  ${this.utils.printReceived(received)}` +
        `${(diffString ? `\n\nDifference:\n\n${diffString}` : '')}\n` +
        `${(msg ? `Custom:\n  ${msg}` : '')}`
      }

    return { actual: received, message, pass }
  }
})

// usage:
expect(myThing).toBeMessage(expectedArray, ' was not actually the expected array :(')
Zargold
  • 1,892
  • 18
  • 24
  • 1
    Great job; I added this to my setupTests.js for my Create-React-App created app and it solved all my troubles... – hbarck Oct 21 '18 at 12:11
7

Another way to add a custom error message is by using the fail() method:

it('valid emails checks', (done) => {
  ['abc@y.com', 'a@b.nz'/*, ...*/].map(mail => {
    if (!isValid(mail)) {
      done.fail(`Email '${mail}' should be valid`)
    } else {
      done()
    }
  })
})
Ilyich
  • 4,966
  • 3
  • 39
  • 27
  • done cannot be used for async tests, it gives error: Test functions cannot both take a 'done' callback and return something. Either use a 'done' callback, or return a promise. – Andrei Diaconescu Mar 22 '23 at 11:25
1

you can use this: (you can define it inside the test)

      expect.extend({
ToBeMatch(expect, toBe, Msg) {  //Msg is the message you pass as parameter
    const pass = expect === toBe;
    if(pass){//pass = true its ok
        return {
            pass: pass,
            message: () => 'No ERRORS ',
          };
    }else{//not pass
        return {
            pass: pass,
            message: () => 'Error in Field   '+Msg + '  expect  ' +  '  ('+expect+') ' + 'recived '+'('+toBe+')',
          };
    }
},  });

and use it like this

     let z = 'TheMassageYouWantWhenErrror';
    expect(first.name).ToBeMatch(second.name,z);
1

I end up just testing the condition with logic and then using the fail() with a string template.

i.e.

it('key should not be found in object', () => {
    for (const key in object) {
      if (Object.prototype.hasOwnProperty.call(object, key)) {
        const element = object[key];
        if (element["someKeyName"] === false) {
          if (someCheckerSet.includes(key) === false) {
            fail(`${key} was not found in someCheckerSet.`)
          }
        }
K.H. B
  • 363
  • 2
  • 9
1

To expand on @Zargold's answer:

For more options like the comment below, see MatcherHintOptions doc

// custom matcher - omit expected
expect.extend({
  toBeAccessible(received) {
    if (pass) return { pass };
    return {
      pass,
      message: () =>
        `${this.utils.matcherHint('toBeAccessible', 'received', '', {
          comment: 'visible to screen readers',
        })}\n
Expected: ${this.utils.printExpected(true)}
Received: ${this.utils.printReceived(false)}`,
    };
  }

enter image description here

// custom matcher - include expected
expect.extend({
  toBeAccessible(received) {
    if (pass) return { pass };
    return {
      pass,
      message: () =>
        `${this.utils.matcherHint('toBeAccessible', 'received', 'expected', { // <--
          comment: 'visible to screen readers',
        })}\n
Expected: ${this.utils.printExpected(true)}
Received: ${this.utils.printReceived(false)}`,
    };
  }

enter image description here

piouson
  • 3,328
  • 3
  • 29
  • 29
1

Instead of using the value, I pass in a tuple with a descriptive label. For example, when asserting form validation state, I iterate over the labels I want to be marked as invalid like so:

errorFields.forEach((label) => {
  const field = getByLabelText(label);

  expect(field.getAttribute('aria-invalid')).toStrictEqual('true');
});

Which gives the following error message:

expect(received).toStrictEqual(expected) // deep equality

    - Expected  - 1
    + Received  + 1

      Array [
        "Day",
    -   "false",
    +   "true",
      ]
Kellen
  • 581
  • 5
  • 18
0

You can rewrite the expect assertion to use toThrow() or not.toThrow(). Then throw an Error with your custom text. jest will include the custom text in the output.

// Closure which returns function which may throw
function isValid (email) {
  return () => {
     // replace with a real test!
     if (email !== 'some@example.com') {
       throw new Error(`Email ${email} not valid`)
     }
  }
}

expect(isValid(email)).not.toThrow()

Mark Stosberg
  • 12,961
  • 6
  • 44
  • 49
0

I'm usually using something like

it('all numbers should be in the 0-60 or 180-360 range', async () => {
    const numbers = [0, 30, 180, 120];
    for (const number of numbers) {
        if ((number >= 0 && number <= 60) || (number >= 180 && number <= 360)) {
            console.log('All good');
        } else {
            expect(number).toBe('number between 0-60 or 180-360');
        }
    }
});

Generates: enter image description here

Jan Dočkal
  • 89
  • 1
  • 8