📜 ⬆️ ⬇️

My “Wow, I didn't know that!” Moments with Jest

Hello! The JavaScript Developer Course will start this Thursday. In this regard, we decided to share the translation of another interesting material. Enjoy your reading.



Jest has always been my indispensable tool for unit testing. He is so reliable that I begin to think that I have always under-utilized it. Although the tests worked, over time I refactored them here and there, because I did not know that Jest could. Every time this is a new code when I check the Jest documentation.
')
So, I’m going to share some of my favorite tricks with Jest that some of you may already know because you read the documentation, and not like me (shame), but I hope that this will help those who only quickly ran it !

By the way, I use Jest v24.8.0 as a reference material, so be careful, some things may not work on the version of Jest that you are currently using. In addition, the examples do not represent the actual test code, it is just a demonstration.

#one. .toBe vs .toEqual


At first, all these statements looked normal to me:

expect('foo').toEqual('foo') expect(1).toEqual(1) expect(['foo']).toEqual(['foo']) 

Based on the use of chai for equality statements (to.equal), this is simply natural. In fact, Jest will not complain, and these statements go as usual.

However, Jest has .toBe and .toEqual. The first is used to assert equality using Object.is , and the second is to provide a deep comparison of objects and arrays. .toEqual has a fallback on using Object.is if it turns out that a deep comparison is not needed, such as an equality statement for primitive values, which explains why the previous example went very well.

 expect('foo').toBe('foo') expect(1).toBe(1) expect(['foo']).toEqual(['foo']) 

That way, you can skip all the if-else in .toEqual using .toBe , if you already know what values ​​you are testing.
A common mistake is that you will use .toBe to .toBe the equality of primitive values.

 expect(['foo']).toBe(['foo']) 

If you look at the source code when .toBe fails, it will try to determine if you really made this error by calling a function that uses .toEqual. This can be a bottleneck when optimizing your test.

If you are sure that you are applying primitive values, your code can be reorganized as such for optimization purposes:

 expect(Object.is('foo', 'foo')).toBe(true) 

More details in the documentation .

# 2. More suitable comparisons


Technically, you can use .toBe to validate any values. With Jest, you can specifically use certain comparison tools that will make your test more readable (and in some cases shorter).

 // expect([1,2,3].length).toBe(3) // expect([1,2,3]).toHaveLength(3) const canBeUndefined = foo() // expect(typeof canBeUndefined !== 'undefined').toBe(true) // expect(typeof canBeUndefined).not.toBe('undefined') // expect(canBeUndefined).not.toBe(undefined) // expect(canBeUndefined).toBeDefined() class Foo { constructor(param) { this.param = param } // expect(new Foo('bar') instanceof Foo).toBe(true) // expect(new Foo('bar')).toBeInstanceOf(Foo) 

These are just some of those that I chose from a long list of Jest comparators in the documentation; you can take a look at the rest.

# 3. Testing using snapshots (snapshot testing) on ​​elements without a user interface


You may have heard about testing with snapshots in Jest , where it helps you track changes in your UI elements. But testing is not limited to snapshots.

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 

It would be tiring if you claimed more and more employees. In addition, if it turns out that for each employee you need to make more statements, multiply the number of new statements by the number of employees, and you will get an idea.
With the help of testing snapshots, all this can be done just like this:

 const allEmployees = getEmployees() const happyEmployees = giveIncrementByPosition(allEmployees) expect(happyEmployees).toMatchSnapshot() 

Whenever regressions occur, you would know exactly which tree in the node does not match the snapshot.

But convenience has its price: this method is more error prone. There is a possibility that you will not know that the snapshot is in fact incorrect, and ultimately you will still make it. So double check your snapshot as if it were your own approval code (because it is).

Of course, testing is not limited to this. Read the full documentation .

#four. describe.each and test.each


Have you ever written a test that looks something like 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') }) }) 

It's monotonous and routine, right? Imagine doing this with a lot of cases.
Using description.each and test.each you can compress the code as follows:

 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}`) }) }) 

However, I still have to use it in my own test, as I prefer my test to be detailed, but I just thought it would be an interesting trick.

See the documentation for more details about the arguments (spoiler: table syntax is really cool).

#five. Single imitation of global functions


At some point, you will have to test something that depends on global functions in a particular test case. For example, a function that receives information about the current date using a Date javascript object, or a library that relies on it. The difficulty is that when it comes to the current date, you can never get the correct statement.

 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 


In the end, you would have to override the global Date object so that it is consistent and manageable:

 function foo () { return Date.now() } Date.now = () => 1234567890123 expect(foo()).toBe(1234567890123) // 

However, this is considered bad practice, since redefinition is maintained between tests. You will not notice this if there is no other test based on Date.now, but this will also leak.

 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) // ??? }) 

Before, I “hacked” it so that it would not 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 }) 

However, there is a much better, less hacking way to do this:

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

Thus, jest.spyOn monitors the global Date object simulates the implementation of the now function for only one call. This, in turn, will leave Date.now intact for the rest of the tests.

There is definitely more information on stubs in Jest. See the full documentation for more details.

This article is getting quite long, so I think that's all for now. This affects only a small part of the Jest features, and I just highlight my favorites. If you have other interesting facts let me know.

Also, if you often used Jest, see Majestic , which is a graphical interface without configs for Jest, a really good alternative to the boring terminal output. I'm not sure if there is an author in dev.to, but nonetheless respect for this person.

As always, thank you for your attention!

That's all. See you on the course.

Source: https://habr.com/ru/post/457616/


All Articles