Live demos are back. See Testim Mobile in action on Feb 21 | Save your spot

Javascript Unit Testing: Get Started Quickly and Easily

Welcome to the world of JavaScript unit testing. Here, we'll explore what it means to test your JavaScript application. In…

Testim
By Testim,

Welcome to the world of JavaScript unit testing. Here, we’ll explore what it means to test your JavaScript application. In this blog post, we’ll explore why and how you should go about testing your JavaScript application. We’ll also explore where exactly these tests should be run and why. Finally, we’ll explore what the tool chain will look like and how to structure some of the more common types of tests. So buckle up—it should be a fun ride!

Why Should I Test?

One of the first things that’s usually asked as I introduce some of the testing basics is, “Why should I even care?” Well, it’s fairly easy. We want to be confident that the software we create is as good today as it was last week. Tests are the little helper in the background verifying that yes, the thing you got working last week still works as intended. This little helper becomes invaluable as your system grows because there’s just no way you can verify manually that everything is always working. You have a computer that can do that repetitive task very well, so let it do that for you with tests.

This means that you should be sure to introduce testing early in your environments—or at least start now. Even if you have a large codebase that’s currently untested, you’ll want to start now with adding tests for all new work. Start on small things to accumulate wins and get some momentum behind you. This way, as your system and complexity grows, your confidence that it can be delivered into production stays high.

Where—and How Often—Do These Tests Run?

Now that we’ve got an idea of tests, what about when and where these tests should run? Well, first, they should be running continuously in the background as you develop. This way, as you create code, you’ll get a sense of what may be coupled in ways you didn’t understand—or even just the accidental change in code that breaks downstream systems. You should make sure you’re always running your tests locally.

But after the local tests, these tests should then be run in an isolated automated system as well. They should be run every time there’s new code in your main code repository. This way, you can verify independently that everything is going according to plan, and you can be confident that your system is ready for production. This automated system should be part of your deploy pipeline that adds confidence before you deploy to your production environment.

Setting Up Your Environment and Tool Chain

All right, enough talk about the theory of testing. Let’s set up a tool chain to test our JavaScript application. In this blog post, we’ll be using the Jest tool and testing framework. This tool is one of the de facto testing tools now out there because of the ease of use and all of the different testing functionality it gives. I’ll assume you have at least a simple JavaScript application already set up. This assumes that you have Node.js already installed, that your project is already initialized, and that your project has a package.json file.

With that, here’s how you would install Jest:

> yarn add --dev jest

If you’re running npm, go ahead and follow the Jest guide of how to install the tool and get started.

Now that we have Jest installed, let’s verify that we have everything up and running with a simple test. It would look something like this:

test('must pass', () => {
    expect(true).toBeTruthy();
});

If we save this file in our source folder and name it something like source.test.js (the .test.js is the important part), we can then run yarn jest and see something like this as the output:

> yarn jest
 PASS  src/source.test.js
  ✓ must pass (2ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.139s
Ran all test suites.
   Done in 2.32s.

Arrange, Act, Assert

Now that we have a test runner, how do we go about writing tests? Well, a very common and popular method of how to structure your tests is called Arrange Act Assert. First made popular in some of the first xUnit books, this method arranges your tests in a logical flow of how to set up whatever it is that needs testing. Act upon that thing, then assert the desired conditions.

Assertions

Let’s look at examples of this last step—assertion. This is the one that drives all tests anyway, so let’s look at examples of what type of assertions are used in Jest and how we can take advantage of them.

Here’s the code we’ll be testing:

exports.testString = () => {
    return 'a';
}

exports.testNumber = () => {
    return 1;
}

exports.testParameters = (a, b) => {
    return a + b;
}

And here’s what tests would look like when testing these methods:

const { testString, testNumber, testParameters } = require('./source')

test('test string compare', () => {
    expect(testString()).toBe('a');
});

test('test number compare', () => {
    expect(testNumber()).toBe(1);
});

test('test parameters', () => {
    expect(testParameters(1, 2)).toBe(3);
});

As you can see, Jest uses a pattern called matchers. Matchers allow you to write your assertions in a style that almost looks like a sentence. There are many different matchers that allow you to assert different types of state and outcomes. This lets you verify whatever state you need to ensure that your code is running as expected. Understanding what you can and can’t assert will help you in a big way when writing tests and help you design the tests that your system needs.

Setting Up Your State

Now that we know how to verify the state, how do we set up tests so we’re ready for testing? Jest gives a few options of how to set up each test so that our system is ready to be used, and then verification can happen. Let’s take a look at some code that would require some setup:

let a = 0;

exports.init = (value) => a = value;

exports.testInit = (myValue) => {
    return myValue + a;
};

Now let’s look at the code that can utilize a setup method:

const { init, testInit } = require('./source')

beforeEach(() => {
    init(5);
});

test('should add init + value', () => {
    expect(testInit(3)).toBe(8);
});

As you can see from the example, setup methods not only get your system in the desired state before you actually start running your tests, they also help you reduce duplicated calls that may also be in your system. This adds the extra bonus of helping you clean up your code. We also see that there’s such a thing as a global setup and for a setup for each run. This allows you to design your tests in whatever way you need to.

Asynchronous Tests

Previously, all of our testing examples have been fairly straightforward synchronous tests. Those are fairly easy to understand and straightforward to test, but what about asynchronous tests? These tests aren’t always as straightforward because we need to allow some kind of process to do something and then reenter where our test happens. Jest thankfully has ways of assisting us in these types of tests. Let’s look at some code that would require an asynchronous call:

exports.aPromise = (myValue) => {
    return new Promise((resolve) => {
        resolve(myValue + 5)
    });
};

Now, here’s how the tests look:

const { aPromise } = require('./source')

test('should wait for a promise', async () => {
    const value = await aPromise(5)
    expect(value).toBe(10);
});

As you can see, async and await are a big help here. Although our code may be asynchronous, we can make the code look synchronous and the flow of the test easier to understand. Thankfully, Jest has a few methods to test this type of code. This lets you match your tests to whatever style you need and makes them easier to understand and maintain.

Conclusion

I hope this blog post helped you get started with your JavaScript unit testing—and maybe even inspired you to test throughout your project. Testing will become some of the most important code in your codebase. It helps you ensure that you can deploy and stay consistent throughout the software life cycle.

This post was written by Erik Lindblom. Erik has been a full stack developer for the last 13 years. During that time, he’s tried to understand everything that’s required to deliver high quality, valuable software. Today, that means using cloud services, microservices techniques, container technologies. Tomorrow? Well, he’s ready to find out.