Jest For Everyone: The Basics

banner

Source: jestjs.io

Before continuing this part please read this one first.

In the previous part of the tutorial, we wrote our first test. In this part, we are going to learn the basic of Jest to write better tests by following real-world scenarios.

If you remember about the part-1. We write tests for sayHi function.

1const sayHi = require('../sayHi.js');
2
3test('should sayHi to John', () => {
4 const result = sayHi('John');
5 const expected = 'Hi John!';
6
7 expect(result).toBe(expected);
8});

In this test, the most important line is 7 expect(result).toBe(expected);. Here we tested our function.

matcher details

In sayHI function returns string. But when we are going to build an application there will not only be a function that will return a string. There will be boolean, array, object, promise, and exception (error). How can we test them?

Jest provides us necessary matcher to test those kinds of values. Let's learn them.

What is a matcher?

Matcher gives you different ways to test your expected value. There are many matchers available. Some of them are common and some of them are specific to specific kinds of data like the object, or arrays.

Common matchers

Did notice something? In our first test, we used toBe function. This is a common matcher. Using a common matcher we can test anything. There are more common matchers toEqual, toStrictEqual.

Let's rewrite our first test (sayHi) using these common matchers.

1const sayHi = require('../src/sayHi.js');
2
3test('should say hi to John', () => {
4 const result = sayHi('John');
5 const expected = 'Hi John!';
6
7 expect(result).toEqual(expected);
8 expect(result).toStrictEqual(expected);
9});

Run the test, it will pass without giving any errors.

1test('common matcher example with primitive values', () => {
2 const result = 6;
3 const expected = 6;
4
5 expect(result).toBe(expected);
6 expect(result).toEqual(expected);
7 expect(result).toStrictEqual(expected);
8
9 expect(true).toBe(true);
10 expect(false).toEqual(false);
11 expect(null).toStrictEqual(null);
12});

This test will also be passed. You can see using those matchers we can test both number, boolean, and string.

1test('common matcher example with array value (tobe)', () => {
2 expect([]).toBe([]); // will fail
3});
4
5test('common matcher example with array value (toEqual)', () => {
6 expect([]).toEqual([]);
7});
8
9test('common matcher example with array value (toStrictEqual)', () => {
10 expect([]).toStrictEqual([]);
11});

Notice for the same value [] they produced a different result. toBe failed and others passed. Why?

Because toBe uses Object.is to test equality. This means checks value and references. Two empty arrays reference are not the same, they are different. That's why it fails. But toEqual and toStrictEqual only check for value not for the reference that why they are passed.

1const obj1 = {
2 name: 'John',
3 age: 25,
4 address: undefined,
5};
6
7const obj2 = {
8 name: 'John',
9 age: 25,
10};
11
12test('common matcher example with object values', () => {
13 expect(obj1).toEqual(obj2);
14});
15
16test('common matcher example with object values', () => {
17 expect(obj1).toStrictEqual(obj2);
18});

Again different result! toEqual passed and toStrictEqual failed. Why?

When toEqual compares objects, it ignores undefined properties, undefined array items, array sparseness, and object type mismatch, But toStrictEqual don't.

This common matcher works well with primitive values, but be careful when testing non-primitive/reference values. Use the most suitable one based on your requirement.

Let's learn about the other available matchers.

String

Let's extend our sayHi function. Now our sayHi function will return Hi, Hello, Hey randomly instead of Hi only.

1function extendedSayHi(name) {
2 const greetings = Math.random() * 10 > 5 ? 'Hello' : 'Hi';
3
4 return `${greetings} ${name}!`;
5}
6
7module.exports = extendedSayHi;

Let's write test for this.

1const extendedSayHi = require('../src/extendedSayHi.js');
2
3test('should say hi to John', () => {
4 const result = extendedSayHi('John');
5 const expected = 'Hi John!';
6
7 expect(result).toBe(expected);
8});

Can you find an error on the above test?

Actually, the above test sometimes will pass and sometimes will fail. Because extendedSayHi will return Hi or Hello based on the random value. This means that our returned value will not be the same always. So, we can't use common matchers, then how can we test that? In this condition, we can use toMatch Matcher.

1const extendedSayHi = require('../src/extendedSayHi.js');
2
3test('should say hi to John', () => {
4 const result = extendedSayHi('John');
5
6 expect(result).toMatch(/(hi|hello)\\s+jhon/i);
7});

We have tested our extendedSayHi function. In toMatch function, we passed regular expression. Using toMatch we can test string values.

toMatch(regexp | string) - to check that a string matches a regular expression.

Number

Let's write a floatSum function. floatSum function will take two float numbers as arguments and will return the sum of the numbers.

1function floatSum(num1, num2) {
2 return num1 + num2;
3}
4
5module.exports = floatSum;

Let's write the test for floatSum function.

1const floatSum = require('../src/floatSum.js');
2
3test('0.3 + 0.3 should be equal to 0.6', () => {
4 const result = floatSum(0.2, 0.1);
5
6 expect(result).toBe(0.3);
7});

Test failing, Everything seems ok. What's wrong? It is failing because of JavaScript's wired behavior. The function doesn't actually return 0.3. Let's update it.

1const floatSum = require('../src/floatSum.js');
2
3test('0.3 + 0.3 should be equal to 0.6', () => {
4 const result = floatSum(0.2, 0.1);
5
6 expect(result).toBeCloseTo(0.3);
7});

Now we are not testing the exact value, testing the approximate close value with the help of toBeCloseTo. There are more matchers available to test number values you can explore from Jest docs.

toBeCloseTo(number, numDigits?) - to compare floating point numbers for approximate equality.

Boolean

We saw earlier we can test boolean values using common matchers.

1expect(true).toBe(true);
2expect(false).toEqual(false);

Now look at the function below.

1const willReturnTruthyValue = () => {
2 const val = Math.random(3);
3
4 if (val < 1) {
5 return 1;
6 }
7 if (val < 2) {
8 return true;
9 }
10 if (val < 0) {
11 return 'nothing';
12 }
13};

How can we test this function? Do you have any idea? This function is not consistant with the return value. But look carefully it always returns truthy values.

1it('should check the truthy value', () => {
2 expect(willReturnTruthyValue()).toBeTruthy();
3});

Now we are checking for truthy values. Now look at another similar example with a falsy value.

1const willReturnFalsyValue = () => {
2 const val = Math.random(3);
3 if (val < 1) {
4 return 0;
5 }
6 if (val < 2) {
7 return false;
8 }
9 if (val < 0) {
10 return '';
11 }
12};

To test this function/scenario, there is toBeFalsy matcher available.

1it('should check the falsy value', () => {
2 expect(willReturnFalsyValue()).toBeFalsy();
3});

In this kind of scenario when only care about them is true or false. We can use toBeTruthy and toBeFalsy to test our function.

  • toBeTruthy - when you don't care what a value is and you want to ensure a value is true in a boolean context.
  • toBeFalsy - when you don't care what a value is and you want to ensure a value is false in a boolean context.

Array

Imagine, we have a database, where we store our favorite book list. To store books in the database we are using addFavBook function.

1const mockDB = [];
2
3function addFavBook(book) {
4 mockDB.push(book);
5}

Now we want to test our function, is adding the book to the list or not?

1const { mockDB, addFavBook } = require('../src/addFavBook');
2
3it('should add new book to the list', () => {
4 const newBook = 'Show your work';
5
6 addFavBook(newBook);
7
8 expect(mockDB).toContain(newBook);
9});

Our function working properly. Using toContain we are testing whether our new book was added or not in the mockDB. There is more matcher to test this kind of scenario like toHaveLength, toContainEqaul.

  • toHaveLength(number) - to check that an object has a .length property and it is set to a certain numeric value.
  • toContain(item) - to check that an item is in an array (for primitives)

Object

Now think about the common scenario, A user wants to signup on to our app. In this case, the frontend sends data user data to the backend. The backend does the operation to the database and returns a new user object with id and access token (it may vary, I am just saying this for the example purpose).

1const addUser = userObj => {
2 // save the user in the database
3 return {
4 _id: Math.random(10),
5 accessToken: `Token_${Math.random()}`,
6 ...userObj,
7 };
8};

Lets test this function.

1const addUser = require('../src/addUser');
2
3test('should add new user', () => {
4 const user = {
5 name: 'Chester Bennington',
6 profession: 'Singer',
7 };
8 // adding the user
9 const result = addUser(user);
10
11 expect(result).toHaveProperty('_id');
12 expect(result).toHaveProperty('accessToken');
13 expect(result).toMatchObject(user);
14});

First checking the result whether we are getting the extra properties or not (_id, accessToken). Then checking whether our passed object matched the return object or not. We can test using these matchers which function returns object.

  • toHaveProperty(key, value?) - To check if the property at provided reference key exists for an object.
  • toMatchObject(object) - To check if an object matches a subset of the properties of an object.

Promise

The previous two examples show DB operation. But we DB operation happens we can't get the normal return value because it async operation. It should return the promise value. If a function return promise how can we test them?

1const promise = error => {
2 return new Promise((resolve, reject) => {
3 setTimeout(() => {
4 resolve('promise resolved'); //
5 }, 2000);
6 });
7};

Let's write the test for this function.

1const promiseFunc = require('../src/promiseFunc.js');
2
3test('should test promise value', () => {
4 expect(promiseFunc()).toMatch(/promise resolved/i); // will fail
5});

We can't write tests as we did earlier because it is promise value. We need to use then/catch or async/await to test this.

1const promiseFunc = require('../src/promiseFunc.js');
2
3test('should test promise value', () => {
4 return promiseFunc().then(data => {
5 expect(data).toMatch(/promise resolved/i);
6 });
7});

There is one thing to notice, if we don't add return it will not work. Keep this in your mind.

1test('should test promise value', async () => {
2 const result = await promiseFunc();
3 expect(result).toMatch(/promise resolved/i);
4});

We can also use this instead of the above example. This is how we can test promise values.

Error

Till now we have tested every function that runs without any error. What if a function throws an error, how can we test that function?

1const willThrowError = () => {
2 throw new Error('This is a funny error.');
3};

Let's learn how to test this scenario.

1it('should check throw error or not', () => {
2 expect(() => willThrowError()).toThrow();
3 expect(() => willThrowError()).toThrow(/funny error/i);
4});

toThrow ensure that a function throws an error or not. Did you notice something different in this test?

We are calling willThrowError function inside another function. Why? If we don't wrap with function the test will fail. So, keep in this mind when testing this scenario.

toThrow(error?) - to test that a function throws when it is called.

Exception

There is a bonus part. We can test null, undefined, and NaN using a common matcher.

1const willReturnNaN = () => NaN;
2const willReturnNull = () => null;
3const willReturnUndefined = () => undefined;
4
5it('should return null value', () => {
6 expect(willReturnNull()).toBe(null);
7});
8
9it('should return undefined value', () => {
10 expect(willReturnUndefined()).toBe(undefined);
11});
12
13it('should return NaN value', () => {
14 expect(willReturnNaN()).toBe(NaN);
15});

But there are better matcher tests this kind of scenarios.

1const willReturnNaN = () => NaN;
2const willReturnNull = () => null;
3const willReturnUndefined = () => undefined;
4
5it('should return null value', () => {
6 expect(willReturnNull()).toBeNull();
7});
8
9it('should return undefined value', () => {
10 expect(willReturnUndefined()).toBeUndefined();
11});
12
13it('should return NaN value', () => {
14 expect(willReturnNaN()).toBeNaN();
15});

Summary

Now we know how to use matchers to test different kinds of values. It is time to practice what we know. I highly encourage you to find some open-source projects and write tests for those projects. Thanks for your effort and time to read this article. Happy testing!

See you in the next part, next part we learn about modifiers, mock, and spy.

You can get the example code from my github repo (branch: part-2).