Node.js Unit Testing: Get Started Quickly With Examples

Unit testing is important to verify the behavior of the smallest units of code in your application. It helps improve…

Node.js Unit Testing
Testim
By Testim,

Unit testing is important to verify the behavior of the smallest units of code in your application. It helps improve the quality of your code and reduces the amount of time and money you spend on bug fixing. Moreover, unit testing helps you find bugs early on in the development life cycle and increases your confidence in the code. This post we’ll show you how to get started with Node.js unit testing in practice, with examples. Think of this post as a “hello world” for unit testing in Node.js.

We’ll start with the “what” of Node.js unit testing, explaining the concept of this form of testing. After that, we move to the “why”, so you can understand what’s the value you gain by adopting unit testing. Then, the next natural step is the how of unit testing in Node.js: you’ll see how to get started with unit testing in the quickest possible way. For the tutorial part of the post, we’ll assume two things:

  • You have Node.js installed on your machine
  • You’re comfortable using the command line.

After that, we’ll cover several topics:

  • more benefits of Node.js unit testing
  • the anatomy of a Node.js unit testing
  • a comparison between some of the most popular Node.js testing frameworks
  • useful tips for writing great Node.js tests

Finally, we give you a summary of what you’ve learned. Before wrapping up, we share some final tips on what to do next.

First of all, let’s discuss what is Node.js unit testing in the first place.

What Is Node.js Unit Testing?

Let’s start with some definitions. Node.js unit testing is simply unit testing made to Node.js applications. That’s the obvious definition, but kind of useless if you’re not familiar with the concept of unit testing itself, so let’s go further.

Unit testing is one of the most important types of automated testing. It focuses on examining the smallest parts of code—the so-called units—by isolating that code. Since unit tests exercise isolated units, that means the system under test can’t interact with external dependencies, such as databases, the file system, or HTTP services. During tests, such dependencies are replaced by “fake” implementations, such as mocks/stubs/spies, which are collectively known as test doubles.

Expand Your Test Coverage

Fast and flexible authoring of AI-powered end-to-end tests — built for scale.
Start Testing Free

Due to the restrictions above, unit tests are typically easy to write and can be executed with almost no configuration, as they are often made of just one function call. Besides that, it’s easy to collect and display results from unit tests. It’s important to write unit tests in a way that tries all possible outcomes of a snippet of code. In each unit test, the returned value of the unit must equal the expected value. If that’s not the case, then your test will fail.

Next, let’s learn about the reasons behind doing Node.js unit testing.

Why Do You Need Testing?

Node.js unit testing helps you to guarantee the quality of your product. Imagine you launch a new application that requires the user to sign up. First of all, the user has to fill in all their data. Next, they hit the signup button to submit their profile. However, the application returns an error message, and the user isn’t able to sign up.

It’s very likely the user will remove the app and look for a better alternative. There’s no reason the user would wait for a better version of the app when even its first step fails.

This example shows the importance of testing your business logic. The easiest way to test your business logic is by using unit testing because it tests the smallest units of code in your application.

Besides that, unit tests are great for catching regressions. “Regression” is the name we give to the phenomenon of new changes to the codebase causing problems in other parts of the application. Without unit testing—and automated testing in general—you would need to comprehensively test your complete app by hand after every change. This would be highly impractical to so. Fortunately, unit testing can be leveraged in this scenario. A comprehensive suite of unit tests—which run in your CI/CD pipeline—acts as a great safety net for developers.

How To Get Started With Node.js Unit Testing?

Having covered the what and why of unit testing, the next big question left for us to tackle is the how. How is unit testing actually performed? How to get started with it? That’s what you’ll see now.

Creating a Simple Node.js App

Let’s start by creating a Node.js application. Using the command line, you’ll create a new directory, access it and start a new app:

mkdir string-calculator
cd string-calculator
npm -y

After that, you’ll have a package.json file with the following content:

{
"name": "string-calculator",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

Now, open the content of the folder with your preffered text editor.

Implementing a Feature

You might be wondering why I picked “string-calculator” as the name for the directory and project. It’s simple: the app you’ll create is based on the String Calculator Kata, a programming exercise created by Roy Osherov.

Let’s proceed. In the folder’s root, create a new file called index.js. Paste the following code on it:

const http = require('http')
const qs = require('querystring')
const calculator = require('./calculator')

const server = http.createServer(function(request, response) {
  console.dir(request.param)

  if (request.method == 'POST') {
    console.log('POST')
    var body = ''
    request.on('data', function(data) {
      body += data
    })

    request.on('end', function() {
      const post = qs.parse(body)
      const numbers = post.numbers
      const result = calculator.add(numbers)
      response.writeHead(200, {'Content-Type': 'text/html'})
      response.end('Result: ' + result)
    })
  } else {
    var html = `
            <html>
                <body>
                    <form method="post" action="http://localhost:3000">Numbers: 
                        <input type="text" name="numbers" />
                        <input type="submit" value="Add" />
                    </form>
                </body>
            </html>`
    response.writeHead(200, {'Content-Type': 'text/html'})
    response.end(html)
  }
})

const port = 3000
const host = '127.0.0.1'
server.listen(port, host)
console.log(`Listening at http://${host}:${port}`)

Now, you’ll create yet another file. This time, it’ll be called calculator.js. Save it with the following content:

function add(numbers) {
    return numbers
        .split(',')
        .map(x => parseInt(x))
        .reduce((a, b) => a + b)
}

exports.add = add;

Testing Manually

The application you just created does the following: when accessed via the GET method, it displays a form, asking for numbers. The user should then enter some numbers, separated by comma, and press the button. Then, the application will display as a result the sum of the numbers entered.

With that in mind, let’s test do a little bit of manual exploratory testing. First, start the app by running the following command on your terminal, at the project’s root folder:

node index.js

If everything works fine, you’ll see the message: Listening at http://127.0.0.1:3000. Open that address using your preferred browser, and you’ll see a page like this one:

Not the most beautiful web app ever, but it does the trick. Informe a bunch of integers separated by comma, click on the Add button and you’ll see the result. Now, get creative and try testing different combinations, including invalid ones. What happens when you enter text that is not numbers? What if you put two commas in a row?

Starting With Unit Testing: Install a Test Framework

We’re now ready to start adding unit tests to this app. To do unit testing, you need a unit testing framework.  A unit testing framework is a tool that takes care of what you’ll need to perform unit testing:

  • it provides you with a syntax for writing the test cases
  • it executes the existing test cases
  • finally, it validates and displays the results from test runs

There are many test frameworks for JavaScript. For this tutorial, we’ll go with Jest.

To install Jest, go back to your terminal and, at the root of the project, run the following command:

npm install jest –save-dev

After the installing is complete, go to your editor, and make a small change to your package.json file. Under scripts, you’ll find the following line:

"test": "echo \"Error: no test specified\" && exit 1"

Replace it with this:

“test”: “jest”

Now, to test your app, you just have to run the following command:

npm test

Of course, since we don’t have any tests yet, you’ll get an error message saying that no tests were found. Let’s fix that.

Writing and Running Your First Unit Test

Start by creating a new file in the project folder and naming it calculator.test.js. The file will have the following content:

const calculator = require('./calculator')

test('string with a single number should result in the number itself', () => {
    expect(calculator.add('1')).toBe(1);
  });

We start by importing the module that contains the code we’d like to test. After that, comes the test itself.

As you can see, the test itself is a simple function, that gets two parameters:

  • the first is a string that describes the test
  • and the second is another function which express the action we will test

In this case, we expect that, when calling the add function passing the string ‘1’ as a parameter, we should get the number 1 as a result.

How can we run the test? Go to your terminal and run npm test or simply jest. You should see a result like this:

Congrats! This is your first Jest unit test on your Node.js application!

Going Further: Let’s See The Test Fail

When you’re writing unit tests, it’s essential you can trust your tests. That’s why it’s so important to see your tests fails: it’s a crucial way to reduce the likelihood of errors in your tests.

So, let’s go to the production code and change it, deliberately adding a defect. I’ll change the last line of the add function to this:

.reduce((a, b) => a + b + 1)

Now, the result will be obviously wrong, so our test should fail. Right? Let’s see:

> jest

 PASS  ./calculator.test.js
  √ string with a single number should result in the number itself (2 ms)

Test Suites: 1 passed, 1 total     
Tests:       1 passed, 1 total

As it turns out, the test still passes. What happened? Well, since we’re passing a string with a single number, it gets converted to an array with a single integer, which causes the callback of the reduce function to not be executed.

That’s actually great. Your unit tests are giving you feedback about your code. The feedback is: you need more tests!

Let’s write a test that addresses the scenario of a string with two numbers:

test('string with two numbers separated by comma should result in the sum of the numbers', () => {
    expect(calculator.add('4,5')).toBe(9);
  });

Now, if I run the tests, I get the following output:

Now, you’d only have to revert the production code back to normal for both tests to pass.

To finalize the example, let’s add two more test cases, the first testing the scenario of a string with three numbers, and the last one the string with four numbers:

test('string with three numbers separated by comma should result in the sum of the numbers', () => {
    expect(calculator.add('2,8,4')).toBe(14);
});

test('string with four numbers separated by comma should result in the sum of the numbers', () => {
    expect(calculator.add('2,0,4,5')).toBe(11);
});

Node.js Unit Testing Frameworks: Let’s Compare Some Options

For the tutorial above, we’ve used Jest. Jest is one of the most popular unit testing tools, for JavaScript in general and also for Node.js. However, there are many more frameworks at your disposal. Before going further, let’s do a quick comparison between some of the main names out there.

Mocha

Mocha is a popular JavaScript testing framework. Many would argue that its main strength is its flexibility. With Mocha, you have a lot of freedom when it comes to deciding how you’re going to work. In practice, that means that a lot of required testing functionality (e.g. assertions, mocking/stubbing, etc) are provided by third-party tools. For instance, using Mocha along with Chai (an assertion library) is a popular combination.

The downside of such flexibility is the need for more configuration, which results in a more complex setup.

AVA

AVA is a minimalistic test tool, which is highly opinionated and executes test concurrently, which improves performance.

AVA doesn’t use globals—unlike tools such as Mocha—and it uses clear syntax for the test outputs, removing noise and allowing you to focus on the most essential information.

Tape

This is another very minimal framework. Like AVA, it doesn’t support globals. It has a quick setup, not requiring you to handle a lot of configuration.

Jasmine

Jasmine defines itself as a “batteries included” framework. That means it comes with all that you need, which means getting started should be easy and quick.

Jasmine is often called a BDD framework, which causes some confusion. Rest assured: you can use Jasmine even if you’re not using this particular approach.

As a downside, Jasmine does make use of globals, which can pollute the code.

Jest

Last but not least, we have Jest, which is the tool we used in our tutorial.

Jest was initially created by Facebook to test React applications. But since then, the tool has gained a lot of traction, being adopted for non-React apps as well. Today, Jest is considered by many to be the best framework for JavaScript testing.

Jest boasts to be a “zero configuration” tool. It comes out of the box with everything you need, so you can get started as quickly as possible. Tests are paralelized and isolated, in order to maximize performance.

Benefits of Node.js Unit Testing

If you aren’t convinced yet of the benefits of unit testing, here are a few elements you should consider when making your testing decision.

1. Improved Code Quality

It’s obvious that unit testing increases the quality of your code. It gives you the guarantee that your business logic is correct and no unexpected behavior can occur. Besides that, unit testing helps you find bugs and defects in your code.

2. Early Discovery of Code Bugs

Testing helps you find bugs earlier on in the software development life cycle. Unit testing is especially useful for finding bugs in the business logic. By finding bugs earlier in the software development cycle, you can resolve problems earlier so they won’t affect other pieces of code later on.

In addition, the earlier detection of bugs also reduces the overall cost of development because you spend less time on bug fixing in a later stage of the project.

3. Validation of Your Design and Code Structure

When writing tests, you’re indirectly thinking about the design of your code and how it fits into the larger codebase. Unit testing is a second chance to validate your architecture and code structure. In other words, you’ll have to think about writing testable code and make design decisions according to this. You want to avoid designing complex components that are hard to test.

Besides that, when you’re writing unit tests, you’ll review your code a second time. This means you might find problems with your code, such as illogical flows that need to be resolved.

4. Improved Customer Satisfaction

As unit testing contributes to the overall quality of your project, you’ll likely increase customer satisfaction. A product with a higher quality helps you gain trustworthy clients that rely on your quality. Product quality is a key metric that helps you stand out in the market—especially a saturated market.

5. Generate Code Coverage Reports

First, let’s define a code coverage report with a practical example from the Microsoft .NET documentation.

Code coverage is a measurement of the amount of code that is run by unit tests – either lines, branches, or methods. As an example, if you have a simple application with only two conditional branches of code (branch a, and branch b), a unit test that verifies conditional branch a will report branch code coverage of 50%.

In other words, without a code coverage report, you don’t know what percentage of code is covered by tests. This is important to increase confidence in your code but also to have a metric that defines code quality.

Most often, a code coverage report tells you the following metrics:

  • Branch coverage, such as  a conditional branch
  • Line coverage
  • Function coverage

For Node.js specifically, popular code coverage libraries include:

  • Instanbul: Works with most testing frameworks, such as tap, mocha, AVA, and many more
  • Jest: Offers a CLI option --coverage to generate a code coverage report.
  • C8: Code coverage using Node.js’s built-in functionality
  • Codecov: Code coverage tool for 20+ languages that integrates well with most CI/CD pipelines.

The Anatomy of a Node.js Unit Test

A Node.js unit test consists of three steps. Let’s explore them.

Step 1: Arrange

Make sure that everything’s ready for running your test. This includes setting the right state, mocking I/O operations, or adding spies to objects. (The “Unit Testing Concepts” section later in this post explains mocking and spying.)

Besides creating mocks and spies, test arrangement includes creating fixtures. A fixture includes a prefixed state or set of objects that you’ll use as a baseline for running tests.

Step 2: Act

In the “act” step, you call the function or piece of code you want to test.

Step 3: Assert

During the “assert” step, you want to validate if the code produces the expected output. Besides checking the output of a function, you can also determine if certain functions have been called with the right arguments or if a component contains the correct state.

The assert step generally uses expect statements to make those assertions.

expect(
    stubs.storage.entities.Account.decreaseFieldBy,
).toHaveBeenCalledWith(
    { publicKey: block.generatorPublicKey },
    'producedBlocks',
    '1',
    stubs.tx,
);

Bringing It All Together

Let’s bring all three elements together to create a unit test. Here’s what one looks like:

it('should decrease "producedBlocks" field by "1" for the generator delegate', async () => {
  // Arrange
  const block = {
    height: 2,
    generatorPublicKey: 'generatorPublicKey#RANDOM',
  };

  // Act
  await dpos.undo(block);

  // Assert
  expect(
    stubs.storage.entities.Account,
  ).toHaveBeenCalledWith(
    'producedBlocks',
    '1',
  );
});

Next, let’s explore two common concepts in unit testing.

Node.js Unit Testing Concepts

Let’s explore mocking, stubbing, and spying:

  • Mocking: The concept of mocking is primarily used in unit testing. You’d use mocking for isolating code by simulating the behavior of real objects and replacing the real object with the mocked object or function. For example, you can use a mock to make a function throw an error to evaluate how the function you’re testing handles this error.
  • Spying: A spy allows you to catch function invocations so you can later verify if the object got called with the right arguments. A spy won’t change the behavior of a function.
  • Stubbing: Stubs are similar to spies. Instead of spying on a function, you can use a stub to control the behavior of a function. For example, a function makes an HTTP call to an external API. As you want predictable behavior for unit tests that don’t rely on unpredictable outcomes from an external API, we can stub the API call with a predefined value to make the test predictable again.

Let’s take a look at some practical tips for writing unit tests.

4 Tips for Writing Node.js Unit Tests

Consider these four helpful tips when writing your first unit tests.

1. Set a low number of assertions per unit test. A higher number of assertions indicates you might not be testing the smallest unit of code. Besides that, sometimes it makes sense to split your unit test into two test definitions that each test more specific behavior.

2. Avoid repeating assertions. If you’ve verified in one test that a certain property returns “null,” avoid asserting for this null value in other, similar tests. Too many assertion statements will bloat your test case definition. Besides that, too many assertions will make it harder for other developers to determine what they really want to verify with this test.

3. Stick to a strict usage of arrange, act, and assert. Doing so helps make your tests more readable and easier to understand for other developers.

4. Avoid using assertionless tests. An assertionless test calls a function and wants to verify if the function doesn’t throw any errors. It’s a bad practice to use these kinds of tests because they have a higher chance of returning false positives. The below code snippet is an example of a test without any assertions. It calls a function during the “act” step and has no assertions in the “assert” step. The test can fail only when the call during the “act” step fails.

it('should decrease "producedBlocks" field by "1" for the generator delegate', async () => {
  // Arrange
  const block = {
    height: 2,
    generatorPublicKey: 'generatorPublicKey#RANDOM',
  };

  // Act
  await dpos.undo(block);

  // Assert
 
});

Node.js Unit Testing Summary

In short, unit testing is focused on isolating code in order to verify the logic itself, not the integrations. You can isolate code by using a testing technique like mocking in order to simulate real objects’ behavior.

Unit tests are the most simple type of tests and are easy to write. You can increase the readability of unit tests by using the arrange, act, and assert pattern.

Now that you know more about unit testing, what should your next steps be? My recommendation for you would be to learn more about unit testing and automated testing in general. And as it turns out, you’re in the best possible place for that, since the Testim blog has many posts on the subject.

For instance, you can start by learning more about unit testing best practices and the differences and similarities between unit testing and integration testing. It’s also important to understand the role unit testing plays in a comprehensive testing strategy, and for that you can read our post on the concept known as the test automation pyramid. Additionally, you can expand on the importance of early test by reading our posts about the concept of shift-left testing:

Node.js Unit Testing: Understand It, Leverage It, But Go Beyond It

Once you have a deeper understanding of Node.js unit testing (and unit testing in general), you’ll be ready to leverage this form of testing in your QA strategy. You’ll be able to recognize that, despite being valuable and necessary, unit testing is far from being the only type of automated testing you need for ensuring quality in your applications.

One of the most important limitations of unit tests is that they exercise the application in an “unrealistic” way. Your users don’t use your apps by interacting with its units in complete isolation. Instead, they interact with the application as a whole, interacting with its user interface and exercising all of its layers. That’s why it’s important to also use types of testing that exercise the app in that way as well, and end-to-end testing is perfect for that.

Unsurprisingly, you can also learn more about end-to-end testing with posts on the Testim blog. And when it’s time to pick a tool for E2E testing, you might want to take a look at Testim Automate.

Good luck writing your first tests!

This post was written by Michiel Mulders. Michiel is a passionate blockchain developer who loves writing technical content. Besides that, he loves learning about marketing, UX psychology, and entrepreneurship. When he’s not writing, he’s probably enjoying a Belgian beer!