Skip to main content

Test-Driven Development in JavaScript – How to Use Jest

Test-driven development is a coding practice where you write the result you want your program to produce before creating the program.

In other words, TDD requires you to pre-specify the output your intended program must produce to pass the test of functioning the way you envisioned.

So, in an effective test-driven development practice, you would first write tests that express the result you expect from your intended program.

Afterward, you would develop the program to pass the prewritten test.

For instance, suppose you wish to create an addition calculator. In such a case, the TDD approach will be like so:

Test-driven development workflow diagram

Test-driven development workflow diagram
  1. Write a test specifying the result you expect the calculator to produce to pass the test of being the program you had in mind.
  2. Develop the calculator to pass the prewritten test.
  3. Run the test to check whether the calculator passes or fails the test.
  4. Refactor your test code (if necessary).
  5. Refactor your program (if necessary).
  6. Continue the cycle until the calculator matches your vision.

Let's now see a JavaScript example of a TDD workflow.

JavaScript Example of a Test-Driven Development Workflow

The steps below will use a simple JavaScript program to show you how to approach TDD.

1. Write your test

Write a test that specifies the result you expect your calculator program to produce:

function additionCalculatorTester() {
if (additionCalculator(4, 6) === 10) {
console.log("✔ Test Passed");
} else {
console.error("❌ Test Failed");
}
}

2. Develop your program

Develop the calculator program to pass the prewritten test:

function additionCalculator(a, b) {
return a + b;
}

3. Run the test

Run the test to check whether the calculator passes or fails the test:

additionCalculatorTester();

Try it on StackBlitz

4. Refactor the test

After you've confirmed that your program passed the prewritten test, it's time to check if there's any need to refactor the test.

For instance, you could refactor additionCalculatorTester() to use a conditional operator like so:

function additionCalculatorTester() {
additionCalculator(4, 6) === 10
? console.log("✔ Test Passed")
: console.error("❌ Test Failed");
}

5. Refactor the program

Let's also refactor the production code to use an arrow function.

const additionCalculator = (a, b) => a + b;

6. Run the test

Rerun the test to ensure your program still works as intended.

additionCalculatorTester();

Try it on StackBlitz

Notice that in the examples above, we implemented TDD without using any libraries.

But you can also use powerful test-running tools, like Jasmine, Mocha, Tape, and Jest, to make your test implementation faster, simpler, and more fun.

Let's see how to use Jest, for example.

How to Use Jest as a Test Implementation Tool

Here are the steps you'll need to follow to get started using Jest as your test implementation tool:

Step 1: Get the right Node and NPM version

Make sure you have Node 10.16 (or greater) and NPM 5.6 (or greater) installed on your system.

You can get both by installing the latest LTS from the Node.js website.

If you prefer to use Yarn, ensure you have Yarn 0.25 (or greater).

Step 2: Create a project directory

Create a new folder for your project.

mkdir addition-calculator-jest-project

Step 3: Navigate to your project folder

Using the command line, navigate to your project directory.

cd path/to/addition-calculator-jest-project

Step 4: Create a package.json file

Initialize a package.json file for your project.

npm init -y

Step 5: Install Jest

Install Jest as a development dependency package like so:

npm install jest --save-dev

Step 6: Make Jest your project's test runner tool

Open your package.json file and add Jest to the test field.

{
"scripts": {
"test": "jest"
}
}

Step 7: Create your project file

Create a file that you will use to develop your program.

touch additionCalculator.js

Step 8: Create your test file

Create a file that you will use to write your test cases.

touch additionCalculator.test.js
note

Your test file's name must end with .test.js—so that Jest can recognize it as the file containing your test code.

Step 9: Write your test case

Open your test file and write a test code that specifies the result you expect your program to produce.

Here's an example:

// additionCalculator.test.js

const additionCalculator = require("./additionCalculator");

test("addition of 4 and 6 to equal 10", () => {
expect(additionCalculator(4, 6)).toBe(10);
});

Here's what we did in the snippet above:

  1. We imported the additionCalculator.js project file into the additionCalculator.test.js test file.
  2. We wrote a test case specifying that we expect the additionCalculator() program to output 10 whenever users provide 4 and 6 as its argument.
note
  • test() is one of Jest's global methods. It accepts three arguments:
    1. The name of the test ("addition of 4 and 6 to equal 10").
    2. A function containing the expectations you wish to test.
    3. An optional timeout argument.
  • expect() is a Jest method that lets you test the output of your code.
  • toBe() is a Jest matcher function that enables you to compare expect()'s argument to primitive values.

Suppose you run the test code now. The test would fail because you've not developed the program for which you created the test. So, let's do that now.

Step 10: Develop your program

Open your project file and develop a program to pass the prewritten test.

Here's an example:

// additionCalculator.js

function additionCalculator(a, b) {
return a + b;
}

module.exports = additionCalculator;

The snippet above created an additionCalculator() program and exported it with the module.exports statement.

Step 11: Run the test

Run the prewritten test to check if your program passed or failed.

npm run test

Suppose your project contains multiple test files and you wish to run a specific one. In such a case, specify the test file like so:

npm run test additionCalculator.test.js

Once you've initiated the test, Jest will print a pass or fail message on your editor's console. The message will look similar to this:

$ jest
PASS ./additionCalculator.test.js
√ addition of 4 and 6 to equal 10 (2 ms)

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

If you prefer Jest to run your test automatically, add the --watchAll option to your package.json's test field.

Here's an example:

{
"scripts": {
"test": "jest --watchAll"
}
}

After adding --watchAll, re-execute the npm run test (or yarn test) command to make Jest automatically begin rerunning your test whenever you save changes.

note

You can quit the watch mode by pressing the Q key on your keyboard.

Step 12: Refactor the test code

So, now that you've confirmed that your program is working as intended, it's time to check if there's any need to refactor the test code.

For instance, suppose you realized that the additionalCalculator should allow users to add any number of digits. In that case, you can refactor your test code like so:

// additionCalculator.test.js

const additionCalculator = require("./additionCalculator");

describe("additionCalculator's test cases", () => {
test("addition of 4 and 6 to equal 10", () => {
expect(additionCalculator(4, 6)).toBe(10);
});

test("addition of 100, 50, 20, 45 and 30 to equal 245", () => {
expect(additionCalculator(100, 50, 20, 45, 30)).toBe(245);
});

test("addition of 7 to equal 7", () => {
expect(additionCalculator(7)).toBe(7);
});

test("addition of no argument provided to equal 0", () => {
expect(additionCalculator()).toBe(0);
});
});

Note that the describe() method we used in the snippet above is an optional code—it helps organize related test cases into groups.

describe() accepts two arguments:

  1. A name you wish to call the test case group—for instance, "additionCalculator's test cases".
  2. A function containing your test cases.

Step 13: Refactor the program

So, now that you've refactored your test code, let's do the same for the additionalCalculator program.

// additionCalculator.js

function additionCalculator(...numbers) {
return numbers.reduce((sum, item) => sum + item, 0);
}

module.exports = additionCalculator;

Here's what we did in the snippet above:

  1. The ...numbers code used JavaScript's rest operator (...) to put the function's arguments into an array.
  2. The numbers.reduce((sum, item) => sum + item, 0) code used JavaScript's reduce() method to sum up all the items in the numbers array.

Step 14: Rerun the test

Once you've finished refactoring your code, rerun the test to confirm that your program still works as expected.

And that's it!

Congratulations! You've successfully used Jest to develop an addition calculator program using a test-driven development approach! 🎉

Important Stuff to Know about Using ES6 Modules with Jest

Jest does not currently recognize ES6 modules.

However, suppose you prefer to use ES6's import/export statements. In that case, do the following:

1. Install Babel as a development dependency

npm install @babel/preset-env --save-dev

2. Create a .babelrc file in your project's root

touch .babelrc

3. Open the .babelrc file and replicate the code below

{ "presets": ["@babel/preset-env"] }

The configuration above will now allow you to change step 9's require() statement from this:

const additionCalculator = require("./additionCalculator");

...to this:

import additionCalculator from "./additionCalculator";

Likewise, you can now also substitute step 10's export statement from this:

module.exports = additionCalculator;

...to this:

export default additionCalculator;
note

Jest also specified similar instructions in their using Babel documentation.

4. Rerun the test

You can now rerun the test to confirm that your program still works!

So, now that we know what test-driven development is, we can discuss its advantages.

What Are the Advantages of Test-Driven Development?

Below are two main advantages of adopting test-driven development (TDD) in your programming workflow.

1. Understand your program's purpose

Test-driven development helps you understand the purposes of your program.

In other words, since you would write your test before the actual program, TDD makes you think about what you want your program to do.

Then, after you've documented the program's purposes using one or more tests, you can confidently proceed to create the program.

Therefore, TDD is a helpful way to jot down the specific results you expect your intended program to produce.

2. Confidence booster

TDD is a benchmark for knowing that your program is working as expected. It gives you the confidence that your program is working correctly.

Therefore, irrespective of any future development on your codebase, TDD provides an effective way to test if your program is still working appropriately.

Let's now discuss some popular TDD terms: "unit test," "integration test," "E2E," and "test doubles."

Unit Test in Test-Driven Development: What Does It Mean?

A unit test is a test written to assess the functionality of an independent piece of program. In other words, a unit test checks if a fully isolated unit of program is working as intended.

The test we wrote for step 10's additionalCalculator program is an excellent unit test example.

Step 10's additionalCalculator()'s test is a unit test because the program is an independent function that does not depend on any external code.

Note that a unit test's primary purpose is not to check for bugs. Instead, a unit test's core purpose is to check whether an independent piece of program (called unit) behaves as intended under various test cases.

Integration Test in Test-Driven Development: What Does It Mean?

An integration test assesses the functionality of a dependent piece of program. In other words, an integration test checks if a program—which depends on other code—is working as intended.

The test we wrote for step 13's additionalCalculator program is an excellent example of an integration test.

Step 13's additionalCalculator()'s test is an integration test because the program is a dependent function that depends on JavaScript's reduce() method.

In other words, we used the prewritten test case to assess the integration of additionalCalculator() and reduce().

Therefore, suppose JavaScript makes reduce() an obsolete method. In such a case, additionalCalculator will fail its test because of the reduce() method.

End-to-End Test in Test-Driven Development: What Does It Mean?

An End-to-End (E2E) test assesses the functionality of a user interface. In other words, E2E checks if your user interface is working as intended.

Watch Max's YouTube video for a good illustration of an End-to-End test.

Test Doubles in Test-Driven Development: What Does It Mean?

Test doubles are the imitation objects used to mimic real dependencies like databases, libraries, networks, and APIs.

A test double allows you to bypass the natural objects on which your program depends. They let you test your code independently of any dependencies.

For instance, suppose you need to verify if an error detected in your app originates from an external API or your code.

But suppose the API's service is available only in production—not in the development environment. In that case, you've got two options:

  1. Wait until your app goes live—which could take months.
  2. Clone the API so that you can continue your test irrespective of the dependency's availability.

Test doubles provide a helpful way to clone your program's dependencies so that your testing activities won't encounter any disruptions.

Typical examples of test doubles are dummy objects, mocks, fakes, and stubs. Let's discuss them below.

Dummy in test-driven development: What does it mean?

A dummy is a test double used to mimic the value of a specific dependency.

For instance, suppose your app depends on a third-party method that requires you to provide some arguments. In such a case, dummy allows you to pass in pretend values to the parameters of that method.

Mock in test-driven development: What does it mean?

Mock is a test double used to mimic an external dependency without considering the responses the dependency may return.

For instance, suppose your app depends on a third-party API (for example, Facebook)—which you cannot access in the development mode. Mock allows you to bypass the API so that you can focus on testing your code regardless of the Facebook API's availability.

Stub in test-driven development: What does it mean?

A stub is a test double used to mimic an external dependency while also returning hand-coded values that you can use to assess your program's behavior with various test case responses from the dependency.

For instance, suppose your app depends on a third-party API (for example, Facebook)—which you cannot access in the development mode. Stub allows you to bypass the API while also mimicking the exact values Facebook will return.

Therefore, stub helps you assess your program's behavior with various response scenarios.

Fake in test-driven development: What does it mean?

Fake is a test double used to create a working test implementation of an external dependency with dynamic values.

For instance, you can use fake to create a local database that allows you to test how a real database will work with your program.

Overview

This article discussed what test-driven development (TDD) is. We also used Jest—a popular JavaScript test implementation tool—to see how TDD works.

Lastly, we discussed the meaning of some common TDD terminologies.