Why you should never use .toBe in Jest/Vitest

thejaredwilcurt

The Jared Wilcurt

Posted on July 21, 2020

Why you should never use .toBe in Jest/Vitest

Patrick Stewart dressed as Hamlet with the text

Alright, this is going to be a short one. Let's just get to the point, what should you use instead, and why.

.toEqual() is a better choice in every scenario.

Wait... I thought these were the same thing, just aliased?

Most do! And that right there is the problem. Here's the difference:

  • .toEqual works based on deep equality
  • .toBe is literally just doing a Object.is(x, y) under the hood. Which is slightly different, but basically the same as x === y.

Here is an example where the two differ:



let x = { z: true };
let y = { z: true };

expect(x)
  .toBe(y); // FALSE

expect(x)
  .toEqual(y); // TRUE


Enter fullscreen mode Exit fullscreen mode

Now sure, this confusion could have been avoided had these been named something like .toDeepEqual() and .toStrictlyEqual(). But that is not the world we live in! And it is unlikely to ever be, since they already have .toStrictEqual() built in which actually is closer to a deep equal, than a strict equal (===). Not at all confusing! 🤬

In most cases, you are comparing an actual value with a hard coded expectation.



test('Generates kitten', () => {
  let kitten = generateKitten();

  expect(kitten)
    .toEqual({
      fluffy: true,
      type: 'kitty',
      tail: true,
      feet: 4
    });
});


Enter fullscreen mode Exit fullscreen mode

So in these cases, .toEqual() gives you exactly what you want. It also shows a diff of the specific properties and values that do not match when a test fails.

Screenshot of a test failing that uses toEqual. Jest highlights the part that didn't match in the diff

But what about primatives? Like undefined, 4, 'text', true?

In these cases the .toEqual and the .toBe are equivalent, because the first thing they both check is if the values are strictly equal. So performance wise there is no difference. .toEqual just handles more cases if the strict equality fails on non-primatives.

Okay, so if it doesn't matter, then I can use either for primitives?

You can... but you shouldn't. The naming of them is close enough that the subtle difference between when one should be used over the other isn't intuitive or obvious. You should default to using .toEqual in all cases to prevent any confusion. For the same reason that I don't use ++x or x++ in my codebases. I don't want to assume that the person that wrote that line of code understands the subtle differences between .toEqual and .toBe or the very subtle differences between Object.is and ===. It's much safer to always use .toEqual so anyone in the codebase will follow this same approach. I'd rather just avoid the issue. Plus, consistency matters.

But what if I actually do want to test if something is a reference?

Sure, here's that hypothetical, and where people might wrongfully tell you to use .toBe:



// Two players, both happen to have the same name and age
const player1 = { name: 'John', age: 25 };
const player2 = { name: 'John', age: 25 };
const players = [player1, player2];

function getFirstPlayer () {
  return players[0];
}

test('getFirstPlayer', () => {
  // USING TOBE
  expect(getFirstPlayer())
    .toBe(player1); // passes

  expect(getFirstPlayer())
    .not.toBe(player2); // passes

  // USING TOEQUAL
  expect(getFirstPlayer())
    .toEqual(player1); // passes

  expect(getFirstPlayer())
    .not.toEqual(player2); // fails
});


Enter fullscreen mode Exit fullscreen mode

In this example we actually want to know if a value is a reference. In most cases we don't want that, but here we do. So using .toBe works, but it isn't obvious to others that we are using it to validate that something is a reference. So, even though the test passes, it's not really a good choice. We should make the intent of our code clear and obvious.

Here is a more obvious approach. (Note the use of .toEqual)



test('getFirstPlayer', () => {
  const result = getFirstPlayer();

  expect(result === player1)
    .toEqual(true);

  expect(result === player2)
    .toEqual(false);
});


Enter fullscreen mode Exit fullscreen mode

Benefits of this approach:

  1. Intent is obvious, and Obvious is always better. It is reasonable to expect other developers to be familiar with how === works and to understand we are purposefully checking if both variables reference the same value.
  2. Consistency. Using .toEqual for this will match the rest of your tests. It should be the default choice when doing matchers.
  3. Reduces reliance on assumptions. (Assuming everyone knows how .toBe works. Which they don't. I literally had to correct the stack overflow post after looking up the Jest source code).
  4. Reduces need for communication. (Verifying that everyone knows how .toBe, and by extension, Object.is works.)

Ahh ok I get it. Reduce the cognitive overhead of choosing one over the other

Yes, .toBe is never required, while .toEqual often is. So use it by default in all cases and completely avoid .toBe.


Credits:

💖 💪 🙅 🚩
thejaredwilcurt
The Jared Wilcurt

Posted on July 21, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related