Testing JavaScript Modules with Jest

Jest is a very easy to use testing library for JavaScript code, that includes handy features such as generating code-coverage and reports and watching for changes to your source code to automatically re-run tests.

It is incredibly easy to get started with Jest; in our example, we are going to get started in under 15 minutes by building out tests for a very simple calculator module.

First off, let's assume the following directory structure:

-my-calculator
--calculator.js
--package.json

Where package.json has the contents of:

{
  "name": "my-calculator",
  "version": "1.0.0",
  "description": "A calculator for practicing Jest tests",
  "main": "calculator.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT"
}

And where calculator.js has the contents of:

const add = (x, y) => {
  return x + y;
};

const subtract = (x, y) => {
  return x - y;
};

const multiply = (x, y) => {
  return x * y;
};

const divide = (x, y) => {
  return x / y;
};

module.exports = {
  add,
  subtract,
  multiply,
  divide
};

This is a very simple calculator, with tons of room for improvement. For now, we're going to focus on writing some very, very basic unit tests for our calculator.

At this point, we're going to add the node module for Jest using npm (or yarn, if you prefer), saving it as a dev-dependency:

> npm install --save-dev jest

or

> yarn add --dev jest 

We are also going to update our package.json file to run jest as our test script, making package.json:

{
  "name": "my-calculator",
  "version": "1.0.0",
  "description": "A calculator for practicing Jest tests",
  "main": "calculator.js",
  "scripts": {
    "test": "jest"
  },
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "jest": "^22.1.3"
  }
}

This will allow us to run the npm test command; for now, this will error out because we do not have any testing files.

To allow Jest to run, we must also also add a file called calculator.test.js alongside calculator.js, giving us the folder structure of:

-my-calculator
--node_modules/
--calculator.js
--calculator.test.js
--package.json

Our calculator.test.js will import our calculator file, and perform tests on it to make sure that our module is well made. We will organize our Jest tests into to sections denoted with the describe method.

In our calculator.test.js file, we will first include our module and then test our some basic addition and subtraction

const { add, subtract, multiply, divide } = require("./calculator");

describe("valid additions", () => {
  test("1 + 1 = 2", () => {
    expect(add(1, 1)).toEqual(2);
  });

  test("10 + 20 = 30", () => {
    expect(add(10, 20)).toEqual(30);
  });
});

describe("valid subtractions", () => {
  test("10 - 2 = 8", () => {
    expect(subtract(10, 2)).toEqual(8);
  });

  test("87 - 523 = -436", () => {
    expect(subtract(87, 523)).toEqual(-436);
  });
});

Now, if we run npm test, Jest will run and give us a test report:

 ~/Work/my-calculator > npm test

> my-calculator@1.0.0 test /Users/xueye/Work/my-calculator
> jest

 PASS  ./calculator.test.js
  valid additions
    ✓ 1 + 1 = 2 (3ms)
    ✓ 10 + 20 = 30 (1ms)
  valid subtractions
    ✓ 10 - 2 = 8
    ✓ 87 - 523 = -436

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

These tests are great, but there's a lot more information we need!

Let's go back to our package.json file and update the test command to be jest --coverage, and run it again:

 ~/Work/my-calculator > npm test

> my-calculator@1.0.0 test /Users/xueye/Work/my-calculator
> jest --coverage

 PASS  ./calculator.test.js
  valid additions
    ✓ 1 + 1 = 2 (4ms)
    ✓ 10 + 20 = 30 (1ms)
  valid subtractions
    ✓ 10 - 2 = 8
    ✓ 87 - 523 = -436 (1ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        1.038s
Ran all test suites.
---------------|----------|----------|----------|----------|----------------|
File           |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
---------------|----------|----------|----------|----------|----------------|
All files      |    77.78 |      100 |       50 |    77.78 |                |
 calculator.js |    77.78 |      100 |       50 |    77.78 |          10,14 |
---------------|----------|----------|----------|----------|----------------|

Now, we get a coverage report! We're doing fairly okay, but we've got some room to improve. We've got to test multiply and divide. Let's add some more tests to calculator.test.js:

const { add, subtract, multiply, divide } = require("./calculator");

describe("valid additions", () => {
  test("1 + 1 = 2", () => {
    expect(add(1, 1)).toEqual(2);
  });

  test("10 + 20 = 30", () => {
    expect(add(10, 20)).toEqual(30);
  });
});

describe("valid subtractions", () => {
  test("10 - 2 = 8", () => {
    expect(subtract(10, 2)).toEqual(8);
  });

  test("87 - 523 = -436", () => {
    expect(subtract(87, 523)).toEqual(-436);
  });
});

describe("valid multiplications", () => {
  test("2 * 4 = 8", () => {
    expect(multiply(2, 4)).toEqual(8);
  });

  test("1000 * 8.5 = 8500", () => {
    expect(multiply(1000, 8.5)).toEqual(8500);
  });
});

describe("valid divisions", () => {
  test("20 / 2 = 10", () => {
    expect(divide(20, 2)).toEqual(10);
  });

  test("99 / 9 = 11", () => {
    expect(divide(99, 9)).toEqual(11);
  });
});

And if we run npm test we now get:

 ~/Work/my-calculator > npm test

> my-calculator@1.0.0 test /Users/xueye/Work/my-calculator
> jest --coverage

 PASS  ./calculator.test.js
  valid additions
    ✓ 1 + 1 = 2 (3ms)
    ✓ 10 + 20 = 30
  valid subtractions
    ✓ 10 - 2 = 8 (1ms)
    ✓ 87 - 523 = -436
  valid multiplications
    ✓ 2 * 4 = 8
    ✓ 1000 * 8.5 = 8500 (1ms)
  valid divisions
    ✓ 20 / 2 = 10
    ✓ 99 / 9 = 11

Test Suites: 1 passed, 1 total
Tests:       8 passed, 8 total
Snapshots:   0 total
Time:        0.737s, estimated 1s
Ran all test suites.
---------------|----------|----------|----------|----------|----------------|
File           |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
---------------|----------|----------|----------|----------|----------------|
All files      |      100 |      100 |      100 |      100 |                |
 calculator.js |      100 |      100 |      100 |      100 |                |
---------------|----------|----------|----------|----------|----------------|

Much better! Now we have 100% coverage! However, we haven't actually tested much -- so let's update our division function to throw an exception if we try to divide by 0:

const divide = (x, y) => {
  if (y === 0) {
    throw "Cannot divide by 0!";
  }
  
  return x / y;
};

And let's add a new test, to ensure that dividing by 0 throws in calculator.test.js:

describe("divison error cases", () => {
  test("20 / 0 throws", () => {
    expect(() => {
      expect(divide(20, 0));
    }).toThrow();
  });
});

Running npm test now gives us:

 ~/Work/my-calculator > npm test

> my-calculator@1.0.0 test /Users/xueye/Work/my-calculator
> jest --coverage

 PASS  ./calculator.test.js
  valid additions
    ✓ 1 + 1 = 2 (3ms)
    ✓ 10 + 20 = 30
  valid subtractions
    ✓ 10 - 2 = 8
    ✓ 87 - 523 = -436
  valid multiplications
    ✓ 2 * 4 = 8
    ✓ 1000 * 8.5 = 8500 (1ms)
  valid divisions
    ✓ 20 / 2 = 10
    ✓ 99 / 9 = 11
  divison error cases
    ✓ 20 / 0 throws (1ms)

Test Suites: 1 passed, 1 total
Tests:       9 passed, 9 total
Snapshots:   0 total
Time:        0.988s, estimated 1s
Ran all test suites.
---------------|----------|----------|----------|----------|----------------|
File           |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
---------------|----------|----------|----------|----------|----------------|
All files      |      100 |      100 |      100 |      100 |                |
 calculator.js |      100 |      100 |      100 |      100 |                |
---------------|----------|----------|----------|----------|----------------|

Now we know that we cannot divide by 0, making our module more predictable. We also know that our method does in fact throw.

As a general rule, I like to at least add the following test cases for each method:

  1. Check valid inputs
  2. Check that it throws when no inputs are given
  3. Check that it throws when inputs are not of the proper type
  4. Check that it throws when given illogical input
  5. Check that any edge cases are handled properly.

Want to see more advanced tests? Check out my tests for my triever and fluent-sort!

Phil Barresi

Phil Barresi

Love this programming thing, may want to do it as a career. Technical Lead, Senior Developer at Troy Web Consulting; Adjunct Instructor of Web Development classes at Stevens Institute of Technology.

Read More