JavaScript Jest Tutorial for Full Stack Developers

JavaScript Jest Tutorial: How to use Tests to Write better Code

Share This Post

You may think that the job of a developer is just to write code. Yet, this is a fairly simple view that leaves many details out of the picture. While it’s true that your job is to write code, all that matters is how you write that code. Even more, how you allocate your time to writing code. You want to spend your time adding features and improving your code, not fixing countless bugs. This is why testing is important, and in this Jest tutorial, we will see how to use it to test our code.

In this tutorial we discuss why testing is important, and how to use Jest to test your JavaScript code. Hence, you should be a little bit more than familiar with JavaScript. You should have even a basic application already running, and grasp the basics of frontend and backend development. In case you don’t feel confident, you may want to start here first.

Jest: What and Why

What is Jest and Testing?

When creating an application, you want to be sure it does what you expect it to do. For example, if you are working on a social media platform you expect that whenever the user hit “post comment”, the comment gets saved and then displayed. Hence, after you write the code, you want to test that it is working just fine. Traditionally, you do that by yourself: you browse to the right page and try to post a comment to see what happens.

Manual testing has some important limitations to consider. The most evident of those is that it takes time to execute. Don’t think only when you add a new simple feature, think about when you do some modification deep buried in the code. You should test all the functionalities that rely on it, for every single modification you make, even the smallest.

The second limitation is that it offers you poor options to precisely detect where the bugs are, because you only see the error – if any – that is reported to the user.

Another smaller limitation is that you cannot test everything. If your app transfer real money, you may not have the same ease of posting a “test” wire transfer.

Manual testing is crucial, and there is no replacement for that. Yet, because of its limitation, doing just that is a poor strategy. At some point, you will end up with some modification that breaks everything, and you won’t be able to tell what did you break exactly. It can get so bad that you will have to ditch your entire app.

Unit testing is what comes to the rescue. We are talking about a way to automatically verify that some parts of the code run as expected. Jest is a framework to implement unit tests.

Why Testing? Why with Jest?

By reading about the limitations of manual testing, you should start to grasp why it is beneficial to run tests automatically.

First and foremost, you can run thousands of tests in a few seconds any time you want. Since you run them with an npm command, you can easily integrate them into your DevOps lifecycle. That is, whenever you are building your app for production – just before releasing it to the users – you can run tests and halt everything in case of problems.

Another great motivation to use tests is that you can test individual functions and classes individually. This allows you to easily understand where the problem is and how to fix it.

Finally, running tests allows you to have a descriptive approach to programming. In fact, you may even write the tests for your new feature before creating your new feature. Why? Because you describe what you expect from the feature (tests), and then tweak the feature until all specifications are met. This is TDD, or Test-Driven Development.

All of this leads to better code, fewer bugs, and thus easier scalability and no hard time adding new features.

Why Jest? There are many testing frameworks out there for JavaScript, but Jest is our top pick. It allows us to test both backend and frontend code with the same simplicity, and it comes with great features out of the box, such as coverage testing. In other words, it has everything we need and everything we could possibly ever want. If you want to futureproof your app, Jest is the way to go.

Getting Started with Jest

Installation

Installing Jest is simple because it is a simple npm dependency like many others. You want to run your tests when you are developing, so you should install it as a development dependency.

npm install --save-dev jest

If you are using Babel (for example to use import instead of require), you should also install babel-jest, always aa a dev dependency.

Configuring Jest

Quite simply, you can run Jest by typing jest in your console (if you installed it with -g) or otherwise by setting up a npm run test command in your package.json. However, we do not want to simply run Jest: we want to run it in our own way. This is where the Jest configuration comes into the picture.

Much like Babel or Vue, you can create a jest.config.js file to instruct Jest on how to run. Every time you run the jest command, it will look for that file in the project root to understand how it should run.

Here is an example of jest.config.js with everything you need to get started.

module.exports = {
  clearMocks: true,
  collectCoverage: true,
  collectCoverageFrom: [ 'src/**' ],
  coverageDirectory: 'coverage',
  coverageReporters: [
     'json',
     'text',
     'cobertura',
     'lcov',
  ],
  reporters: ['default', 'jest-junit'],
  setupFilesAfterEnv: ['./spec/setupTests.js'],
  testEnvironment: 'node',
};

Understanding the Configuration

Let’s discuss what we are configuring in this file:

  • First, we use clearMock to tell Jest to clear the mocks after every test run. A mock is a “fake” call to something, for example, something that emulates the database for the test so that you don’t have to run it against real data.
  • Then, we move on to coverage reporting. Basically, we are telling Jest to understand which parts of the code have been tested, and which ones have not. We will see that Jest can verify each individual line. So, we tell Jest to collect coverage (collectCoverage), from which files do so (collectCoverageFrom), where to store the report about coverage (coverageDirectory), and what report formats to use (coverageReporters).
  • We move on to the report format of the tests with reporters. This is not a report about the coverage, it is a report about what tests are failing.
  • Finally, we provide a file to run after setting up the environment but before running the tests, and we tell what the environment is.

A side note to this configuration is about the cobertura and jest-junit reports. Those will generate an XML file, and unlike the others, they are meant to be read by a computer. You want to use them when running tests with a DevOps process, and having one more report does no harm.

Extra Setup

This configuration setup may not be enough. In fact, it will require some specific tweaks, some of which are mandatory and some of which may depend on the actual use cases.

First, jest-junit does not come prepackaged, so you have to install it.

npm install --save-dev jest-unit

Second, you will need to create the /spec/setupTests.js file. In some cases, this may be just an empty file, as you need no extra setup. However, in some other cases, you do need extra setup.

The most common case here is that your tests will need regenerator-runtime library. So, you can install it:

npm install --save-dev regenerator-runtime

And, once you do that, you can import it in your setupTests.js file. In fact. the one you see below would be the entire content of that file.

import 'regenerator-runtime/runtime';

Running Tests EASILY

The easiest way to run Jest tests is by setting up an npm command. Doing so is simple, you only need to modify package.json. Look for the scripts section, and here add a command. I named it “test”, and it only needs to run jest, with no additional parameters.

{
  // Other parts of package.json
  "scripts": {
    // Other npm commands e.g., build, serve
    "test": "jest"
  },
}

Now you can simply do npm run test whenever you feel like running tests.

Writing Tests

Now that Jest is ready, we can start to write tests. We are about to see that tests are basically simple JavaScript functions that call our code with some parameters and check that the result is what it should be.

Placing Tests in the Right Folder

Whenever we run our tests, Jest will look for test files in the test folder. That test folder is the /spec folder at the root of our project. Jest is smart enough to scan that entire folder, sub-folders included. However, not all the files will be run as tests.

True, tests are simply JavaScript functions calling our “real” code, but they are not simply .js files. They are .spec.js files. This does not mean they have a special and different syntax, it is only a way for Jest to easily identify which files to run.

Inside the /spec folder, you should mirror the structure of your /src folder. In fact, if you have a /src/services folder, you should have a /spec/services folder as well, and if you have MyService.js, you should have a MyService.spec.js file to run tests on it.

Occasionally, some classes or files require large tests, because you need to test for different circumstances so that if you write all the tests for the same source file in a single spec file it will get messy. For that, I find it useful to create a separate file to test for each method. So, if MyService.js requires extensive tests I may end up creating MyService.method1.spec.js, MyService.method2.spec.js, and so on. Use real method names.

Always mirror the exact folder structure from source. Never add additional sub-folders: if the structure is the same, managing tests will be much easier in the long run.

Writing our First Test

You can either write standalone tests or test suites. The second approach is the most scalable one, so that is the way we should go. However, we need to understand what a test suite is before writing the code.

A test suite is a collection of test cases that aim to test the same element, component, or service. On the other hand, a test case is a special condition that a given element should be tested against. You define test suites with describe function, and test cases with the it function inside of it.

Both descibe and it accept two parameters: a string that is a description of what we are describing or testing, and a function that will actually run the tests.

Imagine we have a function sum that adds two numbers together. We could write our tests as follow.

import sum from '../src/sum';

describe('sum', () => {
  it('adds 2 numbers correctly', () => {
    expect(sum(2,2).toBe(4);
    expect(sum(10,5).toBe(15);
    expect(sum(8,3).toBe(11);
  });
  
  it('treats 0 correctly', () => {
    expect(sum(2,0).toBe(2);
    expect(sum(0,5).toBe(5);
    expect(sum(0,0).toBe(0);
  });
  
  it('works with negative numbers', () => {
    expect(sum(2,-1).toBe(1);
    expect(sum(2,-5).toBe(-3);
    expect(sum(-5,-8).toBe(-11);
  });
});

You can see that inside each test we run some expect() and .toBe() methods. Those are our assertions, so actual measurements of what is happening.

Asserting an Outcome

Assertions are simply the way we verify our code runs as expected. Indeed, the way to check the value of something is to provide it as a parameter to the expect() function.

Doing so will create a special object that has many useful ways to verify if it meets our desired result. In fact, on it, we can call many functions. The most popular are below:

  • .toBe(value) ensure we have exactly a given value for simple types, including null and undefined.
  • .toEqual(value) works like .toBe(), but you use it for complex types such as objects or arrays
  • .toBeGreaterThan(num) and .toBeLessThan(num) to check relationships between numbers.
  • .toThrow(error) to check that a function throws (it does not work with async functions).

When providing a value, you should always provide a static value. This is because this is the target value we expect, and we want our expectations to be always the same – static. You can also negate those assertions with a .not before. For example, if we do not want our value to be one, we could do expect(value).not.toBe(1).

There are many other assertions you can run. For those, refer to the Expect page of the Jest documentation.

Preparing Tests

Sometimes, running individual tests is not enough. We may need to do some preparation before running the test suite, some preparation before each test, and even some tear-down afterward.

Jest offers a way to do all that. In fact, inside the describe() function, we can use the following functions, not just it(). Each accepts a function as a parameter on what to do in that specific circumstance:

  • beforeEach() runs before each individual test, before every it().
  • afterEach() runs after each individual test, after every it().
  • beforeAll() triggers once inside our test suite, before all the tests.
  • afterAll() runs once inside our test suite, after all the tests.

The most common of those that you are going to use is beforeEach(). In fact, since in a test suite all tests test the same element, you may want to re-setup that element in the very same way before every test, so that they all start with the same environment. When doing so, remember to define that element inside describe(), so that it is accessible to all the suite.

describe('MyService', () => {
  let service;
  
  beforeEach(() => {
    service = new MyService();
  });
  
  it('has the correct defaults', () => {
    expect(service.value).toBe('a value');
  });
});

Mocking Functions

Sometimes we do not want to call all of our real functions. Instead, we want to mock some of them, that is: replace them with a fake just for the sake of testing. That is the case when we want to interact with the database or with an external API.

However, we do not want to simply replace our functions with the mocks. We want to be sure that we engage the mocks properly, and Jest offers us a convenient way to do so. In fact, we can work with jest.fn() and jest.spyOn().

With jest.fn(), we effectively create a new function to replace our existing one. As a parameter, we can provide a function to run: Jest will engage that functions, but it will make sure that all data we may need to assert later have been collected.

So, for example, we could do:

item.save = jest.fn(() => true);
// Some more test code
expect(item.save).toHaveBeenCalledTimes(1);

In this case, we are overriding the item.save with our mock function, and that function will always return true. Later on, we check that we called that function one time.

A more sophisticated approach comes with jest.spyOn(). With this approach, we do not override existing methods, but instead, Jest will wrap them and look if they get called or not. To use this function, we need to provide the element to spy on and the method to spy. It will return a spy object that we can use for our assertions, without altering the behavior of our existing function.

const spy = jest.spyOn(item, 'save');
// Some more test code
expect(spy).toHaveBeenCalledTimes(1);

Dependency Injection

Dependency injection is not something that Jest brings to the table. It is something you should implement in your source code, and it will make testing a lot easier.

Say, for example, that you are creating an interface to an external REST API. Most likely, you will need axios to do that implementation, so axios is one of your dependencies. You could just import it into your file and start using it, but then you are creating a hard dependency.

Instead, you want to receive that dependency as a parameter, and default to axios only if the dependency was no provided. Take a look at the code below.

import AXIOS from 'axios';

export default class MyService {
  constructor({ axios } = {}) {
    this.axios = axios ?? AXIOS;
  }

  async list() {
    return this.axios.get('/list');
  }
}

With this approach, the list() function is only loosely dependent on the axios dependency. So now, you can test 100% of this code without requiring any external calls:

  • You should test the constructor, verifying that the default value is effectively an axios instance and, in case something else is provided, that something else gets used.
  • Then, in a different test suite, you initialize the service with a mock function and run the list() method.

It may sound a little convoluted, but when implementing it is so easy. In fact, look at the following example.

import MyService from '../src/services/MyService';
import axios from 'axios';

describe('MyService', () => {
  it('has the correct defaults', () => {
   const service = new MyService();
   expect(service.axios).toBeInstanceOf(axios);
  });
  
  it('accepts parameters from constructor', () => {
    const service = new MyService({ axios: 'axios' });
    expect(service.axios).toBe('axios');
  });
});

describe('MyService.list()', () => {
  let service;
  let mock;
  
  beforeEach(() => {
    mock = jest.fn(() => true);
    service = new MyService({ axios: mock });
  });
  
  it('engages axios correctly', async () => {
    expect(await service.list()).toBe(true);
    expect(mock).toHaveBeenCalledTimes(1);
    expect(mock).toHaveBeenCalledWith('/list');
  });
});

This is something that will make all the difference when writing code. So always write with Dependency Injection in mind.

Test Coverage

With the Jest configuration we prepared at the beginning of this tutorial, we are collecting test coverage. This means that, every time you run tests, you will see a report of which parts of the code have been tested, and which ones have not.

Not only that, you will see an overall percentage of test coverage, both for the entire folder and for individual files. What percentage is good enough? What should you aim for?

You should not settle for anything less than 100% test coverage. Yes, even 99.99% is not enough, because even that 0.01% may cause a lot of problems. Furthermore, even 100% coverage testing does not mean bug-free. It means some bugs are less likely, but you will still have some. Specifically because of that, never settle for anything less than 100%.

Wrapping Jest Up

In conclusion, Jest is a powerful framework to ensure the code you write is more stable, and predictable. You should use it to write your tests, and you should write a lot of them. Unlike source code, where redundancy is bad and you want to have one file only to do one thing, with tests redundancy is not a problem. If two tests test the same thing, so be it. In fact, even better!

With this tutorial, we saw everything you need to be up to speed with Jest. In fact, if you want to test your Node.js app or module, you already know everything that you need. Instead, if you want to test your Vue app, you may want to go on with some further reading: Testing Vue.js.

Test. Test everything, test often, test generously. But, most importantly, test.

Picture of Alessandro Maggio

Alessandro Maggio

Project manager, critical-thinker, passionate about networking & coding. I believe that time is the most precious resource we have, and that technology can help us not to waste it. I founded ICTShore.com with the same principle: I share what I learn so that you get value from it faster than I did.
Picture of Alessandro Maggio

Alessandro Maggio

Project manager, critical-thinker, passionate about networking & coding. I believe that time is the most precious resource we have, and that technology can help us not to waste it. I founded ICTShore.com with the same principle: I share what I learn so that you get value from it faster than I did.

Join the Newsletter to Get Ahead

Revolutionary tips to get ahead with technology directly in your Inbox.

Alessandro Maggio

2021-05-20T16:30:30+00:00

Unspecified

Full Stack Development Course

Unspecified