A beginner’s guide to unit testing using different approaches in Jest

Nancy Jain
Dineout Tech
Published in
10 min readMay 30, 2022

--

Jest is a testing framework designed to ensure the correctness of any JavaScript codebase. Many large companies — including Twitter, Instagram, Pinterest, and Airbnb — use Jest for React testing.

Characteristics

  • Zero configuration — one can simply install Jest as a dependency for the project, and start writing tests.
  • Isolation — tests are executed in parallel, so It ensures that different tests don’t influence each other’s results.
  • Snapshots — allows you to verify the integrity of large objects.
  • Rich API — offers a lot of specific assertion types for testing very specific scenarios.

What you will find here

Following are the topics covered along with examples:

  • Pattern Matching
  • Globals
  • Async
  • Mocks

How it will help you

It will help you in analyzing which test strategy can be used for any scenario. For example, in case we want to check return values from pure functions, API responses and further check our business logic, etc. You will also find tips after every section that has been helpful for the project at Dineout.

Different Approaches

Pattern Matching

Jest assertions use matchers to assert on a condition. Jest uses matchers from the expect API. The expect API doc can be referenced here.

Example

Let’s create a JavaScript module calculate.js and a test file calculate.test.js for it.

Testing simple Arithmetic functions

In the above example, our calculate.js exports simple arithmetic functions (like addition, subtraction & multiplication) In calculate.test.js, we have imported these functions and written some tests to check the return value of these functions.

As you can see from the results in the console, 3 out of 4 tests are passed while the sum(2,3) test failed because the return value was expected to be 5. Therefore, we would need to go back and modify the sum method to handle this expectation.

Now we would want to compare the output of our test with a value we expect the function to return. Jest provides expect function to assert the same.

We place the test value in the expect keyword and call the exposed matcher function ToBe to compare the values.

Exposed methods like these are collectively called matchers , and you can find the entire list Jest API reference.

In order to test something opposite, not can be used with the matchers.

If the output value doesn’t match the expected value, these methods throw an error and the test fails. By default, Jest executes all the tests whether previous tests have failed. To opt out if one or n tests have failed previously, [bail](<https://jestjs.io/docs/en/configuration#bail-number--boolean>) configuration option can be used

The complete list of exposed matchers can be found in the Jest API reference.

Common matchers to check are: equality, truthiness, numbers, strings, and arrays

For example, if we wanted to test that the following object has exactly similar properties but didn’t care about the values in it. We could write

In this toEqual perform a deep equality check.

When to use toBe() and toEqual()

Here, toBe fails since x and y are different instances and not equal as in (x === y) === false

toBe — just checks that a value is what you expect. It uses === to check strict equality.

toEqual — when you want to do a deep equal check for all properties.

Tip: You can use toBe for primitives like strings, numbers, or booleans, for everything else use toEqual.

Partial Equality

Now what if we wanted to check if the result has at least the properties that the expected has, we can do:

Globals

Often while writing tests there must be some work that needs to happen before/after tests run. Jest provides helper functions to handle this.

Jest puts some functions and objects into the global environment. You don’t have to require or import anything to use them.

This concept is called Setup and Teardown which means setting up things before we run test and tearing down things after we run the test.

Repeating Setup For Many Tests

beforeEach allows you to run code before every test. There’s also afterEach, that can be used to run code after every test. It’s generally better to use beforeEach to clean up and prepare the environment for your test so if it fails the environment remains as it is at the time of the failure which can help to debug why the failure occurred.

For example,

Here, we have a list of animals and a group of some tests which check if the particular animal has been added at the end, then at the start of the array, later compare the length of the original array of animals to make sure the original is not modified.

After running this, the last test case of length comparison fails as the original does get modified as test cases above it has been run in sequence and modified the data.

So, in order to avoid such situations, we can use globals to reset data before or after every test case.

Here, beforeEach() has been used to reset the list of animals. afterEach() can also be used in the same.

One-Time Setup

In some cases, you only need to do the setup once, at the beginning of a file. This can be especially bothersome when the setup is asynchronous. beforeAll and afterAll can be used in the same way.

Globals can be effectively used to handle asynchronous code if you want to get some data from API/network requests.

Async

How to test async functions whose result is not available immediately?

In the above example, fetchDeta function hits a dummy API and returns a result in a promise.

In the test function, we would need to wait until that promise is resolved. Once the promise is resolved, we can make an assertion on the result and mark the test as completed.

By default, Jest tests are complete once they reach the end of their execution.

Following are the 3 approaches that direct jest that a test assertion has been completed:

Callback

If we call fetchData directly, it will get complete before ever calling the callback.

Done is a callback function that can be called after making an assertion. Jest will wait until the done callback is called before finishing the test.

If done() is never called, the test will fail (with timeout error)

Promise:

Return a promise from your test, and Jest will wait for that promise to resolve. If the promise is rejected, the test will automatically fail.

Async/await:

To write an async test, use the async keyword in front of the function passed to the test.

async and await are effectively syntactic sugar for the same logic as the promises

Tip: you can enable fake timers by calling jest.useFakeTimers()This mocks out setTimeout and other timer functions with mock functions. Check timer-mock to read more about it.

Let’s take a look at two of the most commonly used Jest terms that are also used in other testing tools: mock and spy.

In order to test any method without actually hitting the API, you can use any of the approaches below

There are three main types of module and function mocking in Jest:

  • jest.fn: Mock a function
  • jest.mock: Mock a module
  • jest.spyOn: Spy or mock a function

Mocks

The main goal of mocking is to replace something we can’t control with something we want to do or fake implementation.

The following features are provided:

  • capturing calls to a function
  • set return values
  • replace the actual implementation

There are two ways to mock functions:

  1. By creating a mock function to use in test code
  2. writing a manual mock to override a module dependency.

Using a mock function

A mock can be created by assigning the following snippet of code to a function or dependency:

If any default implementation is defined in jest.fn(). It will get executed.

The following example mocks a return value for checking specific business logic. We mock the shouldReturnTrue function and let it return false.

let’s test the implementation of a function forEach, which invokes a callback for each item in a supplied array.

Each mock function has a mock property which further has a calls property and it stores how many times the function has been called and with what parameters for each call. calls return an array of all the calls.

Spy

At times you only want to watch a method be called, but keep the original implementation. Other times you may want to mock the implementation, but restore the original later in the suite.

In this case, jest.spyOn() can be used.

It creates a mock function similar to jest.fn(). A spy can be created by assigning the following snippet of code to a function or dependency: A more common approach is to use jest.mock to automatically set all exports of a module to the Mock Function.

jest.spyOn(object, method to spy on)

It tracks calls to the object. This allows you to verify how many times a function has been called and with what parameters it has been called.

mockRestore method is used to reset a mock to its original implementation

Mocks are especially useful in unit testing because they allow you to test the business logic of your function without worrying about its dependencies.

Tip: If you want to learn more about Jest testing, you can read this guide on unit testing with Jest. you can further take a look at Jest cheat sheet for quick reference.

Writing a module to override mock dependency

A very common approach is to use jest.mock is to automatically set all exports of a module to the Mock Function.

Here, we have actual implementation of two functions fetchPlanets and fetchStarShips in utils folder. It makes a request to API to fetch respective data and returns the value.

In mock1.test.js, we have created a mock function. In the first argument, we pass the path of the module which we want to mock, and in the second, we return the object where we modify the actual implementation with our own.

In this example, we have mocked only fetchPlanets and assigned our own implementation using jest.fn().

When we call our test suite, jest first checks internally if mocked implementation of that particular function exists, if yes, It runs the same else it runs the original implementation.

So, when fetchStarships is called, there doesn’t exist mocked implementation, so it uses the actual one from the utils folder.

Tip: Placement of mocked implementation can be done in a separate mocks folder.

Mocks folder structure

Instead of placing mocked implementation and self-created dummy objects in the same test file, you can create another file with the same name(utils) in mocks/ directory and define mocked implementations separately. These can further be imported into the test file.

Tip: You can use utility functions for clearing mock instances before/after each test to ensure other test results are not impacted

Clearing Mock State

Suppose we have to run the same test multiple times. Mock functions store the information related to function calls which can lead to failing other test cases.

jest.clearAllMocks() Clears the mock.calls and mock.instances properties of all mocks.

jest.resetAllMocks() This is useful when you want to completely reset a mock back to its initial state. (Note that resetting a spy will result in a function with no return value)

  • mockClear clears only data persisting to mock calls, which means we get a fresh dataset to assert over with toHaveBeenX methods.
  • mockReset resets to mock to its initial implementation, on a spy makes the implementation be a noop (function that does nothing).

In Dineout, we use the above strategies in order to test any module. Breaking down any heavy function with numerous business logics to pure functions and testing those makes it really efficient to test functions, thereby, increasing confidence in the code and further release deployments.

In summary, this means that Jest offers a test runner, assertion library, CLI tool, and great support for different mocking techniques.

Further Reading

You can follow these links for studying further implementation in detail

  • There’s even a CLI tool that you can use from the command line, you can find in the CLI documentation.
  • You can refer excellent documentation with plenty of examples and a supportive community.
  • To learn more about testing tool, methods and best practices you can join this community Discord or ask questions on Stack Overflow.

--

--