How to Write Unit Tests with Jest

Picture of Jordan Baczuk
By Jordan Baczuk

What is a Unit Test and Why Are They Important?

A unit test is a function that is written to test that another function is implemented properly. It should test various cases and verify that the function responds as expected. This will help you know if your code will run smoothly in production, whenever you make changes to your code, all unit tests should be run prior to deployment. This helps you catch potential bugs that may have been introduced while changes were made. To begin, let's consider a function that converts Fahrenheit to Celsius. We can start by creating a project.

Setup the project

mkdir testing cd testing npm init

Then follow the prompts (you can accept all the defaults)

Add Jest

npm install -D jest

This will add Jest as a development dependency because you typically don't need to include jest in your production build, it will just add unnecessary bloat. Also, make sure your scripts property in package.json includes "test": "jest".

package.json

1{ 2 "name": "javascript-tutorials", 3 "scripts": { 4 "test": "jest" 5 }, 6}

Add our files

touch temperature.js temperature.spec.js

temperature.js will house our conversion function temperature.spec.js will house our unit tests

Write our tests first

It is good practice to write your tests first. Some people like to name their test files with .spec.js extension, suggesting that it is a specification for the code under test. Let's determine all the edge cases we want to test in order to make sure that our function will do what we want.

temperature.spec.js

1describe('fahrenheitToCelsius', () => { 2 it.todo('converts 0') 3 it.todo('converts minimum temperature (−273.15C or -459.67F)') 4 it.todo('converts freezing') 5 it.todo('converts boiling') 6 it.todo('converts various temperatures') 7 it.todo('throws if below range') 8 it.todo('throws if not a number') 9})

We use describe to group tests and it or test to specify a single test. In this case we've also added .todo to signify the test still needs to be written. For a list of globals, see the Jest API Docs.

Notice how we consider various cases such as invalid input, and certain expected temperature conversions. It is important to consider all the ways the function could potentially fail so you can write tests for each case.

Now if we run our tests with npm test, you'll notice it returns that our test suite passed, and we have 7 todo tests.

Now, let's write our tests. First create a function in temperature.js using the formula for temperature conversion.

temperature.js

1module.exports.fahrenheitToCelsius = (degrees) => { 2 return (degrees - 32) * (5 / 9) 3}

We haven't handled all the cases, but now we can import the function into our test file to write our tests. Also, let's decide that we want to round to the nearest hundredth decimal place. Also we'll be using CommonJS modules because ECMAScript Modules aren't supported out of the box yet in Jest, but if you really want to use them, see ECMAScript Modules in Jest.

temperature.spec.js

1const { fahrenheitToCelsius } = require('./temperature') 2 3describe('fahrenheitToCelsius', () => { 4 it('converts 0', () => { 5 expect(fahrenheitToCelsius(0)).toBe(-17.78) 6 }) 7 it('converts minimum temperature (−273.15C or -459.67F)', () => { 8 expect(fahrenheitToCelsius(-459.67)).toBe(-273.15) 9 }) 10 it('converts freezing', () => { 11 expect(fahrenheitToCelsius(32)).toBe(0) 12 }) 13 it('converts boiling', () => { 14 expect(fahrenheitToCelsius(212)).toBe(100) 15 }) 16 it('converts various temperatures', () => { 17 expect(fahrenheitToCelsius(-40)).toBe(-40) 18 expect(fahrenheitToCelsius(-20)).toBe(-28.89) 19 expect(fahrenheitToCelsius(10)).toBe(-12.22) 20 expect(fahrenheitToCelsius(75)).toBe(23.89) 21 }) 22 it('throws if below range', () => { 23 expect(() => fahrenheitToCelsius(-459.68)).toThrow() 24 expect(() => fahrenheitToCelsius(-500)).toThrow() 25 }) 26 it('throws if not a number', () => { 27 expect(() => fahrenheitToCelsius('123')).toThrow() 28 expect(() => fahrenheitToCelsius({})).toThrow() 29 expect(() => fahrenheitToCelsius([])).toThrow() 30 expect(() => fahrenheitToCelsius(null)).toThrow() 31 expect(() => fahrenheitToCelsius(undefined)).toThrow() 32 expect(() => fahrenheitToCelsius(true)).toThrow() 33 }) 34}) 35

expect() is used to verify the output of our function, and the operator .toBe() which uses Object.is, is like a === check, only even more strict. For a list of operators see Jest Docs - Expect. Now if we run our tests, we get 2 passes and 5 failures.

We need to fix these failures so we can have all green! I like to run jest in watch mode so it will re-run all my tests automatically whenever I save my code.

npm test --watch

Here is the final working code:

temperature.js

1module.exports.fahrenheitToCelsius = (degrees) => { 2 if (typeof degrees !== 'number') throw new TypeError() 3 if (degrees < -459.67) throw RangeError() 4 const conversion = (degrees - 32) * (5 / 9) 5 return Math.round((conversion + Number.EPSILON) * 100) / 100 6}

How to Write a Good Unit Test?

Writing good tests starts by designing a good function. Unit testing forces you to define what you want the function to do before you write it. This encourages writing your code modularly so that it is easily testable and maintainable. Keeping functions concise and focused on a specific task is a great rule of thumb for writing testable code. It also tends to favor functional programming because it can be harder to write tests for code that is heavily object-oriented. This is because you have to account for state rather than just inputs and outputs.

There are different ways to write testable functions, but I've found it helpful to categorize functions into 2 categories:

  • calculation functions
  • orchestrator functions

Calculation Functions

Calculation functions take some input(s) and generate an output. They are very simple, they generally don't deal with state or call other functions. The temperature conversion function at the beginning is a good example of this type of function.

Orchestrator Functions

Orchestrator functions only call other functions, and sometimes return a result. In our unit tests for these functions, we only care about whether the functions inside were called, not what the result is. We already tested each individual function and those tests should suffice for testing calculations. This introduces function mocking, which means creating a fake version of a function just to test whether or not it was called.

In our example we could split our function into 2 functions. One that calculates the conversion, and another that rounds the result. Let's take a look:

temperature.js

1const roundDecimals = (value) => { 2 return Math.round((value + Number.EPSILON) * 100) / 100 3} 4 5const fahrenheitToCelsius = (degrees) => { 6 if (typeof degrees !== 'number') throw new TypeError() 7 if (degrees < -459.67) throw RangeError() 8 return (degrees - 32) * (5 / 9) 9} 10 11const convertDegrees = (degrees) => { 12 // we need to reference the module instance so jest can mock the entire module 13 return temperatureHelpers.roundDecimals( 14 temperatureHelpers.fahrenheitToCelsius(degrees) 15 ) 16} 17 18const temperatureHelpers = { 19 convertDegrees, 20 fahrenheitToCelsius, 21 roundDecimals 22} 23 24module.exports = temperatureHelpers

and our tests

temperature.spec.js

1const temperatureFuncs = require('./temperature') 2const { roundDecimals, fahrenheitToCelsius, convertDegrees } = temperatureFuncs 3 4describe('roundDecimals', () => { 5 it('rounds to nearest 2 decimals', () => { 6 expect(roundDecimals(12.345)).toBe(12.35) 7 expect(roundDecimals(12.344)).toBe(12.34) 8 }) 9}) 10 11describe('fahrenheitToCelsius', () => { 12 it('converts 0', () => { 13 expect(fahrenheitToCelsius(0)).toBe(-17.77777777777778) 14 }) 15 it('converts minimum temperature (−273.15C or -459.67F)', () => { 16 expect(fahrenheitToCelsius(-459.67)).toBe(-273.15000000000003) 17 }) 18 it('converts freezing', () => { 19 expect(fahrenheitToCelsius(32)).toBe(0) 20 }) 21 it('converts boiling', () => { 22 expect(fahrenheitToCelsius(212)).toBe(100) 23 }) 24 it('converts various temperatures', () => { 25 expect(fahrenheitToCelsius(-40)).toBe(-40) 26 expect(fahrenheitToCelsius(-20)).toBe(-28.88888888888889) 27 expect(fahrenheitToCelsius(10)).toBe(-12.222222222222223) 28 expect(fahrenheitToCelsius(75)).toBe(23.88888888888889) 29 }) 30 it('throws if below range', () => { 31 expect(() => fahrenheitToCelsius(-459.68)).toThrow() 32 expect(() => fahrenheitToCelsius(-500)).toThrow() 33 }) 34 it('throws if not a number', () => { 35 expect(() => fahrenheitToCelsius('123')).toThrow() 36 expect(() => fahrenheitToCelsius({})).toThrow() 37 expect(() => fahrenheitToCelsius([])).toThrow() 38 expect(() => fahrenheitToCelsius(null)).toThrow() 39 expect(() => fahrenheitToCelsius(undefined)).toThrow() 40 expect(() => fahrenheitToCelsius(true)).toThrow() 41 }) 42}) 43 44describe('convertDegrees', () => { 45 it('calls the correct functions in order', () => { 46 const fahrenheitToCelsiusSpy = jest.spyOn( 47 temperatureFuncs, 48 'fahrenheitToCelsius' 49 ) 50 fahrenheitToCelsiusSpy.mockImplementation(() => 4) 51 const roundDecimalsSpy = jest.spyOn(temperatureFuncs, 'roundDecimals') 52 53 convertDegrees(0) 54 55 expect(fahrenheitToCelsiusSpy).toHaveBeenCalledTimes(1) 56 expect(fahrenheitToCelsiusSpy).toHaveBeenCalledWith(0) 57 expect(roundDecimalsSpy).toHaveBeenCalledTimes(1) 58 expect(roundDecimalsSpy).toHaveBeenCalledWith(4) 59 60 // undo mocking the module functions 61 fahrenheitToCelsiusSpy.mockRestore() 62 roundDecimalsSpy.mockRestore() 63 }) 64})

We can mock individual functions in the temperature module by using jest.spyOn. This allows us to see if the function was called, how many times, and with what arguments. Then we call .mockImplementation() to replace the mock function with our own. In this case we want fahrenheitToCelsius to always return 4. This effectively isolates our tests on convertDegrees from the other functions so that way if the other functions fail for some reason, we know whether convertDegrees is still working or not. For a complete list of methods, see Jest Object.

To see the full final code, see the jest folder in the javascript-tutorials repository.