Skip to main content Accessibility Feedback

How to unit test vanilla JavaScript

Pretty much every article I find on unit testing assumes that you’re working in React or Vue or at the very least Node. The few I’ve found on testing vanilla JS were about how to build your own testing library.

Yesterday, I released a handful of mini-courses about linting, unit testing, and end-to-end testing over at the Lean Web Club.

Today, I wanted to share the first half of the course on unit testing vanilla JavaScript. If you like it and want to access the rest of it (and the other courses), join the Lean Web Club for free.

Alright, let’s dig in!

What is unit testing?

Unit tests look at small chunks of code, and verify that they do what they’re supposed to do. They can be used to test individual functions in a library or component, or smaller parts of a bigger application.

For example, let’s say you have a function, sayHi(), that accepts a name and returns a greeting.

A unit test might verify that the function returns a string, that the name is included in that string, and that if no name is provided, it still returns a usable value.

/**
 * Get a greeting
 * @param  {String} name The name of the person to greet
 * @return {String}      The greeting
 */
function sayHi (name = 'there') {
	return `Hi ${name}!`;
}

There are many, many unit testing frameworks, including Mocha, Jasmine, Chai, and Qunit.

For this guide, we’ll be using a newer and very popular framework: Jest.

It’s easier to setup than many of the other options, provides a nice syntax that makes writing tests easier, and has pretty good documentation.

Installing Jest

Before we can use Jest, we need to install it.

Open up the Terminal app on macOS or the Command Prompt app on Windows, cd into your project directory, and run npm install --save-dev jest.

npm install --save-dev jest

For convenience, Jest is already included the package.json in the source code, so you can run npm install if you’d prefer.

cd path/to/project
npm install

Writing unit tests for vanilla JavaScript

To test vanilla JS with Jest, your code has to use ES modules.

/**
 * Get a greeting
 * @param  {String} name The name of the person to greet
 * @return {String}      The greeting
 */
function sayHi (name = 'there') {
	return `Hi ${name}!`;
}

export {sayHi};

Jest works by importing the specific functions that you want to testing, running them, and then evaluating that what you would expect to happen did.

First, let’s create a test for our sayHi() function. We’ll place our tests in the /tests directory, and use {function}.test.js as the naming convention.

/tests/sayHi.test.js

Inside our sayHi.test.js file, we’ll import the {sayHi} function. Make sure the path to your JavaScript file is relative to the /tests directory.

// Import the function
import {sayHi} from '../index.js';

Then, we’ll run the Jest test() method.

This accepts two arguments. The first is a string that describes what we’re testing. The second is a function that actually runs our test.

// Import the function
import {sayHi} from '../index.js';

// Run the test
test('Returns a greeting as a string', function () {
	// Test some stuff...
});

Like most unit testing libraries, Jest uses a natural language syntax.

Inside the callback function, we’ll use the Jest expect() method with a matcher method to test if the thing we’re testing gives us the result we want.

The most common matcher method is expect.toBe(), which functions like a strict equals (===) check.

Pass the thing you’re testing into expect(), and the expected result into toBe(). If they match, the test passes. If they don’t, it fails.

For example, to test that sayHi() returns a string, we would pass typeof sayHi() into expect(), and string into the toBe() method.

// Run the test
test('Returns a greeting as a string', function () {

	// should return a string
	expect(typeof sayHi()).toBe('string');

});

To test that the name variable is being used, we could pass in a name, and use the String.includes() method to check if its included. Since the String.includes() method returns a boolean, we would pass true into the toBe() method.

// Run the test
test('Returns a greeting as a string', function () {

	// should return a string
	expect(typeof sayHi()).toBe('string');

	// should include the provided name
	expect(sayHi('Merlin').includes('Merlin')).toBe(true);

});

Running unit tests

Jest was built to be used with Node and common JS. To use it with native ES modules, we need to add "type": "module" to our package.json file.

{
	"name": "testing-vanilla-js",
	"description": "Learn how to test your JavaScript.",
	"version": "1.0.0",
	"license": "MIT",
	"type": "module",
	"devDependencies": {
		"jest": "^28.1.3"
	}
}

In your CLI tool, cd into the project directory, and run the following command.

node --experimental-vm-modules node_modules/.bin/jest

This will run your test, and print a report in the CLI window.

 PASS  tests/sayHi.test.js
  ✓ Returns a greeting as a string (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.16 s, estimated 1 s
Ran all test suites.

Typing the Jest command would be pretty tedious, so let’s create an NPM script, test:unit, to run it for us.

{
	"name": "testing-vanilla-js",
	"description": "Learn how to test your JavaScript.",
	"version": "1.0.0",
	"license": "MIT",
	"type": "module",
	"scripts": {
		"test:unit": "node --experimental-vm-modules node_modules/.bin/jest"
	},
	"devDependencies": {
		"jest": "^28.1.3"
	}
}

Now, you can run your tests like this.

npm run test:unit

Matcher methods

You could build an entire suite of tests using just the expect.toBe() matcher. But Jest includes a bunch of other matcher methods to make testing a bit easier.

For example, we’re currently testing that the name variable works like this.

// should include the provided name
expect(sayHi('Merlin').includes('Merlin')).toBe(true);

We could instead use the toContain() matcher method.

// should include the provided name
expect(sayHi('Merlin')).toContain('Merlin');

If we wanted to test that the string returned by sayHi() without any options actually contained a value, we could use the toBeTruthy() method.

// should have a value when no name is included
expect(sayHi()).toBeTruthy();

Jest also includes a not property that will check that the matcher is not true.

For example, what if instead of checking if the returned value was truthy, we wanted to make sure it’s length was not 0? We could combine the not property on the toHaveLength() matcher.

// should have a value when no name is included
expect(sayHi()).not.toHaveLength(0);

Don’t feel like you have to memorize all of the matchers, or find the perfect one for the thing you’re trying to test.

If there’s a matcher that makes your life easier, use it. If not, toBe() and the not property work great!

Digging deeper

The rest of the course on unit testing looks at how to organize tests, test DOM manipulation scripts, test APIs and async code, and measure your code coverage.

It includes all of the source code for the lessons. There are also courses on linting and end-to-end testing, plus over 300 additional tutorials on a wide range of topics.

Join the Lean Web Club for free today to access all of it!