Getting started with the Jest Javascript testing framework

Testing Typescript with Jest

Author's image
Tamás Sallai
6 mins

Using a testing library is entirely optional as you can always just write a test.js with code that uses console.log statements and throws Errors when something has a value that is no expected. But a testing framework provides a lot of tools to make this process easier, such as user-friendly assertions, mocks, and a CLI. And it feels familiar to whoever used that in a separate project.

Jest is a framework for testing in Javascript. It focuses on simplicity and still provides everything you'd expect from such a library.

I started using it recently and I found it quite good and easy-to-use. This article is about what I like in Jest and what obstacles I encountered when I started using it.

Typescript and React transpilation

In my case, I wanted to test a part of a webapp that did not depend on the DOM (some utility functions). I use WebPack with React and Typescript to compile the code to the browser. Fortunately, Jest can work with Typescript and React but it required some configuration.

The problem here is that Jest and WebPack are two separate entities here where the former runs during testing and the latter when compiling for the browser. This means I needed to setup a separate transpilation pipeline for the test.

Fortunately, Jest can use Babel with the babel-jest package. And Babel can transform Typescript to Javascript.

One unexpected obstacle was that I also needed to setup React transpilation even though my tests did not touch React component. In my case, I had a tsx file reachable with imports from the test file:

// animationutils.test.js
import * from "./animationutils.ts";

test("get result", () => {
	const result = animationutils.calculateSomething();
	expect(result).toBe(21);
});
// animationutils.ts <== this is the file I wanted to test
import * from "./utils.tsx"; // imports a tsx

export const calculateSomething = () => utils.getValue() + 1;
// utils.tsx
export const getValue = () => 20;

// not called by the test
export const getElement = () => (
	<div/>
);

My test file imported the file to test (animationutils.ts) and that in turn imported a TSX (utils.tsx). When Babel transpiled the test code, it encountered JSX and it did not know how to handle it. Even though the test did not use any React components, they just happen to be in the same file.

This is a case of blue-red functions just on the file level. During transpilation, a TSX file requires React configuration. And all the files that import TSX files too. And all the files that import files that import TSX files, and so on.

As a result, I saw strange errors until I added and configured React and Typescript transpilation:

// babel.config.js

module.exports = {
	presets: [
		"@babel/preset-typescript",
		"@babel/preset-react",
		"@babel/preset-env",
	],
};

Writing tests

What I liked in Jest is that I don't need to manage a central list of tests as it discovers and runs all .test.js files in any directory.

In a test file, you can import any file with standard import statements:

import * as animationutils from "./animationutils";
import fp from "lodash/fp";
import chroma from "chroma-js";

Then the tests are functions with a name, usually also containing a bunch of expectations:

test("result", () => {
	const result = 20;
	expect(result).toBe(20);
});

Running tests

The easiest way to run tests is to add jest to the package.json scripts:

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

Then to call it, use:

npm test

This also supports watch mode with:

npm test -- --watch

Expectations

Instead of the tried-and-tested if (res !== 21) throw "error", expectations and assertions provide a more developer-friendly way to check if a value is what it should be.

To check a value:

const when = // ...
expect(when).toBe(30);

But the Jest expectations API is way more sophisticated than just checking for equality. Some examples:

// toStrictEqual: to structurally compare Objects and Arrays
expect({a: "b"}).toStrictEqual({a: "b"});

// toHaveLength: check array length
expect(["a", "b"]).toHaveLength(2);

// toContain: to item is in the array
expect(["some", "other", "element"]).toContain("element");

// toHaveProperty: a property is in an Object
// defined
expect({b: "a"}).toHaveProperty("b");

// exact value
expect({b: "D"}).toHaveProperty("b", "D");

// deep path
expect({b: {c: "D"}}).toHaveProperty("b.c", "D");

And to negate these expectations, insert .not.:

expect({a: "c"}).not.toStrictEqual({a: "b"});

expect(["a", "b", "c"]).not.toHaveLength(2);

expect(["no", "such", "item"]).not.toContain("element");

This structure makes test easy to read, and, after some getting used to, easy to write.

Mocks

Mock functions are useful when you don't want the test code to call an actual function. For example, you might not want to interface with the database and instead return a canned response:

const getUsers = jest.fn(() => [{name: "user1"}, {name: "user2"}]);

const result = getNumberOfUsersFromDatabase(getUsers);

expect(result).toBe(2);

Another use-case is to make sure a given function is run. For example, I had callback functions that had expectations. I needed a way to ensure those are called:

test("tap", () => {
	const fn = jest.fn();
	const animation = () => [
		{process: animationutils.tap(animationSymbols)(({get, getFinal}) => {
			expect(get("val")).toBe(5);
			expect(get("cval")).toBe(42);
			expect(get("changingVal")).toBe(1);
			expect(getFinal("changingVal")).toBe(2);

			fn();

			return (config) => config;
		}), when: 20},
	];
	animationutils.getAnimationBuilder(animation).getElements(20);
	expect(fn).toBeCalled();
});

Without the mock function, I couldn't be sure that the expects are reached or the test passes because they are not called.

Watch mode

Jest comes with a great watch mode. What I liked the most are the easy-to-use keyboard commands to run only failed tests or filter them by name. This makes it trivial to focus on just one aspect while ignoring all the others. Also, as its name implies, it watches files and automatically runs the tests when files are changed.

Here's a short(ish) video showing how test running and filtering works with Jest:

Snapshots

Jest also supports snapshot testing. It is when you save a known good state and the test runner compares the actual state with the saved one.

For example, the documentation shows a good example, demonstrating snapshots with a React component:

import React from 'react';
import renderer from 'react-test-renderer';
import Link from '../Link.react';

it('renders correctly', () => {
  const tree = renderer
    .create(<Link page="http://www.facebook.com">Facebook</Link>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

And the snapshot contains the rendered tag:

exports[`renders correctly 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;

I have mixed feelings about snapshot testing. A good thing about it is that it's easier to look at something and see if it's working properly or not than trying to decipher what the expectations are from the code.

On the other hand, it is much easier to just snapshot everything as-is without taking into account what the test is supposed to cover. In the above example, a test that checks whether the URL is good fails if the className is changed. I can see how this can lead to a situation where hundreds of unrelated tests are broken and it requires a lot of manual checking to update the snapshots.

Conclusion

Jest is a great framework for testing Javascript and Typescript code. It provides everything I needed to focus on the tests instead of the testing architecture: watch mode, expectations, test discovery, and an easy-to-use CLI.

While it's not the fault of Jest, it was not trivial to set up Typescript + TSX support with Babel. This is mainly the result of having WebPack for transpilation, but it still feels like duplicate effort.

May 18, 2021

Free PDF guide

Sign up to our newsletter and download the "Foreign key constraints in DynamoDB" guide.


In this article