DEV Community

Cover image for My "Whoa, I didn't know that!" moments with Jest
briwa
briwa

Posted on • Edited on

My "Whoa, I didn't know that!" moments with Jest

Jest has always been my go-to unit testing tool. It is so robust I'm starting to think that I've always been underutilizing it. Although the tests are passing, over time I've refactored my test here and there because I didn't know Jest can do that. It's always a different code every time I checked back Jest docs.

So, I'm going to share some of my favorite tricks with Jest that some of you might already know because you didn't skip reading the docs like me (shame on me), but I hope this helps those who did!

FWIW, I'm using Jest v24.8.0 as the reference, so be aware if certain things don't work on the Jest version you're currently using. Also, the examples do not represent the actual test code, it is just merely a demonstration.

#1. .toBe vs .toEqual

All these assertions looked good to me at first:

expect('foo').toEqual('foo')
expect(1).toEqual(1)
expect(['foo']).toEqual(['foo'])
Enter fullscreen mode Exit fullscreen mode

Coming from using chai to do equality assertions (to.equal), it's just natural. In fact, Jest wouldn't complain and these assertions are passing as usual.

However, Jest has .toBe and .toEqual. The former is used to assert equality using Object.is, while the latter is to assert deep equality on objects and arrays. Now, .toEqual has a fallback to use Object.is if it turns out that it doesn't need deep equality, such as asserting equalities on primitive values, which explains why the earlier example was passing just fine.

expect('foo').toBe('foo')
expect(1).toBe(1)
expect(['foo']).toEqual(['foo'])
Enter fullscreen mode Exit fullscreen mode

So, you can skip all the if-elses in .toEqual by using .toBe if you already know what kind of values you're testing.

A common mistake is that you would be using .toBe to assert equality on non-primitive values.

expect(['foo']).toBe(['foo'])
Enter fullscreen mode Exit fullscreen mode

If you look at the source code, when .toBe fails, it would try to see if you are indeed making that mistake by calling a function that is used by .toEqual. This could be a bottleneck when optimizing your test.

If you are sure that you are asserting primitive values, your code can be refactored as such, for optimization purpose:

expect(Object.is('foo', 'foo')).toBe(true)
Enter fullscreen mode Exit fullscreen mode

Check out more details in the docs.

#2. More befitting matchers

Technically, you can use .toBe to assert any kind of values. With Jest, you can specifically use certain matchers that would make your test more readable (and in some cases, shorter).

// ๐Ÿค”
expect([1,2,3].length).toBe(3)

// ๐Ÿ˜Ž
expect([1,2,3]).toHaveLength(3)
Enter fullscreen mode Exit fullscreen mode
const canBeUndefined = foo()

// ๐Ÿค”
expect(typeof canBeUndefined !== 'undefined').toBe(true)

// ๐Ÿค”
expect(typeof canBeUndefined).not.toBe('undefined')

// ๐Ÿค”
expect(canBeUndefined).not.toBe(undefined)

// ๐Ÿ˜Ž
expect(canBeUndefined).toBeDefined()
Enter fullscreen mode Exit fullscreen mode
class Foo {
  constructor(param) {
    this.param = param
  }
}

// ๐Ÿค”
expect(new Foo('bar') instanceof Foo).toBe(true)

// ๐Ÿ˜Ž
expect(new Foo('bar')).toBeInstanceOf(Foo)
Enter fullscreen mode Exit fullscreen mode

These are just a few I picked from a long list of Jest matchers in the docs, you can check out the rest.

#3. Snapshot testing on a non-UI elements

You might have heard about snapshot testing in Jest, where it helps you monitor changes on your UI elements. But snapshot testing is not limited to that.

Consider this example:

const allEmployees = getEmployees()
const happyEmployees = giveIncrementByPosition(allEmployees)

expect(happyEmployees[0].nextMonthPaycheck).toBe(1000)
expect(happyEmployees[1].nextMonthPaycheck).toBe(5000)
expect(happyEmployees[2].nextMonthPaycheck).toBe(4000)
// ...etc
Enter fullscreen mode Exit fullscreen mode

It would be tedious if you have to assert more and more employees. Also, if it turns out that there are more assertions to be done for each employee, multiple the number of the new assertions with the employee count and you get the idea.

With snapshot testing, all of these can be done simply as such:

const allEmployees = getEmployees()
const happyEmployees = giveIncrementByPosition(allEmployees)

expect(happyEmployees).toMatchSnapshot()
Enter fullscreen mode Exit fullscreen mode

Whenever there are regressions, you would exactly know which tree in the node that doesn't match the snapshot.

Now, this handiness comes with a price: it is more error-prone. There are chances that you wouldn't know that the snapshot is in fact wrong and you would end up committing it anyway. So, double check your snapshot as if it is your own assertion code (because it is).

Of course there is more to it on snapshot testing. Check out the full docs.

#4. describe.each and test.each

Have you written some test that is somewhat similar to this?

describe('When I am a supervisor', () => {
  test('I should have a supervisor badge', () => {
    const employee = new Employee({ level: 'supervisor' })

    expect(employee.badges).toContain('badge-supervisor')
  })

  test('I should have a supervisor level', () => {
    const employee = new Employee({ level: 'supervisor' })

    expect(employee.level).toBe('supervisor')
  })
})

describe('When I am a manager', () => {
  test('I should have a manager badge', () => {
    const employee = new Employee({ level: 'manager' })

    expect(employee.badges).toContain('badge-manager')
  })

  test('I should have a manager level', () => {
    const employee = new Employee({ level: 'manager' })

    expect(employee.level).toBe('manager')
  })
})
Enter fullscreen mode Exit fullscreen mode

That is painstakingly repetitive, right? Imagine doing it with more cases.

With describe.each and test.each, you could condense the code as such:

const levels = [['manager'], ['supervisor']]
const privileges = [['badges', 'toContain', 'badge-'], ['level', 'toBe', '']]

describe.each(levels)('When I am a %s', (level) => {
  test.each(privileges)(`I should have a ${level} %s`, (kind, assert, prefix) => {
    const employee = new Employee({ level })

    expect(employee[kind])[assert](`${prefix}${level}`)
  })
})
Enter fullscreen mode Exit fullscreen mode

However, I have yet to actually use this in my own test, since I prefer my test to be verbose, but I just thought this was an interesting trick.

Check out the docs for more details on the arguments (spoiler: the table syntax is really cool).

#5. Mocking global functions once

At some point you would have to test something that depends on a global function on a particular test case. For example, a function that gets the info of the current date using Javascript object Date, or a library that relies on it. The tricky part is that if it's about the current date, you can never get the assertion right.

function foo () {
  return Date.now()
}

expect(foo()).toBe(Date.now())
// โŒ This would throw occasionally:
// expect(received).toBe(expected) // Object.is equality
// 
// Expected: 1558881400838
// Received: 1558881400837
Enter fullscreen mode Exit fullscreen mode

Eventually, you had to override Date global object so that it is consistent and controllable:

function foo () {
  return Date.now()
}

Date.now = () => 1234567890123

expect(foo()).toBe(1234567890123) // โœ…
Enter fullscreen mode Exit fullscreen mode

However, this is considered a bad practice because the override persists in between tests. You won't notice it if there's no other test that relies on Date.now, but it is leaking.

test('First test', () => {
  function foo () {
    return Date.now()
  }

  Date.now = () => 1234567890123

  expect(foo()).toBe(1234567890123) // โœ…
})

test('Second test', () => {
  function foo () {
    return Date.now()
  }

  expect(foo()).not.toBe(1234567890123) // โŒ ???
})
Enter fullscreen mode Exit fullscreen mode

I used to 'hack' it in a way that it won't leak:

test('First test', () => {
  function foo () {
    return Date.now()
  }

  const oriDateNow = Date.now
  Date.now = () => 1234567890123

  expect(foo()).toBe(1234567890123) // โœ…
  Date.now = oriDateNow
})

test('Second test', () => {
  function foo () {
    return Date.now()
  }

  expect(foo()).not.toBe(1234567890123) // โœ… as expected
})
Enter fullscreen mode Exit fullscreen mode

However, there's a much better, less hacky way to do it:

test('First test', () => {
  function foo () {
    return Date.now()
  }

  jest.spyOn(Date, 'now').mockImplementationOnce(() => 1234567890123)

  expect(foo()).toBe(1234567890123) // โœ…
})

test('Second test', () => {
  function foo () {
    return Date.now()
  }

  expect(foo()).not.toBe(1234567890123) // โœ… as expected
})
Enter fullscreen mode Exit fullscreen mode

In summary, jest.spyOn spies on the global Date object and mock the implementation of now function just for one call. This would in turn keep Date.now untouched for the rest of the tests.

There is definitely more to it on the topic of mocking in Jest. Do check out the full docs for more details.


This article is getting longer, so I guess that's it for now. These are barely scratching the surface of Jest's capabilities, I was just highlighting my favorites. If you have other interesting facts, let me know as well.

And also, if you used Jest a lot, check out Majestic which is a zero-config GUI for Jest, a really good escape from the boring terminal output. I'm not sure if the author is in dev.to, but shout out to the person.

As always, thanks for reading my post!


Cover image from https://jestjs.io/

Top comments (9)

Collapse
 
wes profile image
Wes Souza

Here's an extra one I love to use:

const anyObject = {
  complex: true,
  otherProperties: "yes",
  foo: "bar",
};

expect(anyObject).toEqual(expect.objectContaining({ foo: "bar" }))
Enter fullscreen mode Exit fullscreen mode

expect.objectContaining docs.

Really useful for testing objects where you only care about a small part that changes, instead of using snapshots.

Collapse
 
aleccool213 profile image
Alec Brunelle

+1 for not leaking mocks into global state :)

Collapse
 
cubiclebuddha profile image
Cubicle Buddha

๐Ÿ‘ Bravo for sharing the ability to iterate over test cases. Iโ€™ve been looking for something like that for a long time. Iโ€™ve hand rolled it myself in jest, Xunit, Nunit, mocha, jasmine, etc. I canโ€™t wait to try this out on Tuesday when I go back to work. Thank you! :)

Collapse
 
gualison profile image
Alberto Gualis

Great post! Thank you!

I'll also add one little tip for "#3. Snapshot testing on a non-UI elements":

you can replace toMatchSnapshot() by .toMathInlineSnapshot() (check jestjs.io/docs/en/snapshot-testing...) so jest will automatically write all the expected values in your tests file so it's easier to review them (it will even pretty-format them if you use prettier!)

Happy testing!

Collapse
 
javaguirre profile image
Javier Aguirre • Edited

Majestic looks cool! I like to launch Jest from vs code using โ€˜โ€”watchโ€™, so itโ€™s running while changing key parts of my components.

Another cool way of using jest is when using the storybook plugin for Structural Testing in React.

Collapse
 
iomtt94 profile image
Bohdan Artemenko • Edited

I don't know is it correct, my first example:

test('Created with old code style where object has 2 fields', () => {
    expect(github).toMatchObject({
      'name': expect.any(String),
      'ip': expect.any(String),
    });
  });
Collapse
 
slidenerd profile image
slidenerd

i dont use any matcher except toEqual because everytime jest upgrades they always mess a few matchers here and there, if i dont use their fancy matchers, my tests have the lowest chance of breaking on every major upgrade

Collapse
 
tcelestino profile image
Tiago Celestino

Great tips!!

Collapse
 
srshifu profile image
Ildar Sharafeev

Did you know about findRelatedTests option? I wrote an article about it: dev.to/srshifu/under-the-hood-how-...