Testing with Jest - Part 1

Writing tests is not just scribbling a few test blocks and leaving it like that. so we present Jest, methods, best practices (with examples)

$ant article

By Amna Šubić

11 min read

Testing with Jest - Part 1 article
Note: This article assumes that you are familiar with JavaScript and React coding principles. If you are not familiar with these principles, please take a look at following links, before continuing with the article:
- JavaScript
- React

Overview

Tests. That dreaded task most developers try to avoid. "Why do I have to write tests? It takes time, that I could have used to write more code.” You may think writing tests is only for the project manager’s satisfaction, but in fact it helps your personal development. Having good test coverage increases your code quality and ensures you are more resilient towards regression bugs. You will improve your speed, because the machine is always faster than you. You don't want to test your code manually every time you make a small change.

Writing good code does not mean writing a function in fewer lines of code. It means writing testable code, because when code is testable, it ensures that all possible cases are covered and no matter what the input is, it will respond with an appropriate output. Testing code ensures that you, as a developer, become more powerful. More powerful developer leads to better code, to a happier PM, satisfied customers and consumers.

But where do I start, you may ask. It depends on what you are working on and what your needs are. There is a vast of testing frameworks out there: unittest, Jasmine, JUnit, NUnit, Jest and so much more, but today we will focus on testing JavaScript with Jest. So, let’s get started.

What is Jest?

Firstly, let’s talk a bit about Jest as a whole. Jest is a JavaScript testing framework developed by Facebook, or now known as Meta. It was designed with simplicity in mind, and their elegant and well documented API certainly attest that. The library works with any project using: TypeScript, Angular, Vue, React, JavaScript, Babel, Node and many more.

What makes Jest great, is that, in order to write tests, there is no need for configurations. Simply install jest into your projects and start writing tests. Furthermore, when you run a test, and it fails, Jest will highlight the exact place where it has failed and why.

Jest Testing Methods

Before going any further, let’s take a look at how to test with Jest by going over assertion methods. Three most important, and most used methods, are:

  • describe(name, fn) - this method helps organise related tests into groups by creating blocks. It accepts a string that describes the group and a function that returns all other describe blocks and test blocks.
  • test(name, fn) - this method runs the test. It also runs under the alias it. It accepts a string that describes the test and a function that contains test expectations.
  • expect(value) - this method allows you to confirm that the checked input has expected value. To test the input, expect provides us with numerous validation matchers. In this article we will focus on some basic and most used validation matchers. For the full list of methods and matchers, please take a look at the official Jest API documentation.

Expect matchers and examples

Before starting with the matchers, it is important to mention that each test file should contain imports of every method used. In our case, at the top of the test file we will have the following line:

/* global describe, it, expect */

Each test file is named in the following way: [name of the file tested].test.js. Tests can also be written in TypeScript, i.e. can have .ts extension instead of .js, but for the purpose of this blog article, we will stick with .js. So let’s start with our tests.

Testing primitives

When you want to test primitive values such as string or number you will test them with toBe matcher. However, while testing numbers we have to be careful. If the number has a floating point, we should use the matcher toBeCloseTo(value), as rounding in JS is not exactly the same as in real-life. Let’s see it in action. Suppose we have a function that calculates the area of a square. If the width is an integer, we know that the result will be an integer as well, so we can test it with toBe.

const calculateSquareArea = (width) => width * width;

Pretty simple, right? Now, if we input a floating number, we need to test it with toBeCloseTo, because the result will also be a floating point.

describe('square area', () => {
	it('should be 4 if width is 2', () => {
		const squareArea = calculateSquareArea(2);
		expect(squareArea).toBe(4);
	});
});

The second number (5) is the precision point. It makes sure that the number will match up to 5 decimal points.

describe('square area', () => {
	it('should be 0.04 if width is 0.2', () => {
		const squareArea = calculateSquareArea(0.2);
		expect(squareArea).toBeCloseTo(0.04, 5);
	});
});

Testing objects

Now that we know how to test primitive values, let’s talk about object instances. When testing object instances, we use toEqual matcher. The toEqual matcher checks that the two object instances have the same properties. Let’s say we have a function that creates an object with first name, last name, and age of some person,

const createPerson = (firstName, lastName, age) => { firstName, lastName, age };

and we want the output to look like this:

const mockPerson = {
	firstName: 'John',
	lastName: 'Doe',
	age: 24,
};

To test this function we write the following:

describe('create person', () => {
	it('should return an object with first and last name and age', () => {
		const person = createPerson('John', 'Doe', 24);
		expect(person).toEqual(mockPerson);
	});
});

Testing arrays

So far we have tested primitives and objects, so it’s time to test arrays as well. When testing arrays containing primitives, we can use toContain. toContain will check that the value is present in the array. If we have, for example, a function that returns an array of months, and we want to check that the month April is present in the array, we would write:

describe('[function]: getMonths', () => {
	it('should return an array containing April', () => {
		const months = getMonths();
		expect(months).toContain('April');
	});
});

If we want to test arrays containing objects, we will use toContainEqual. In the following example, the function getMonths returns an array of objects that have a name of the month and length of each month in days. This time we will check that the array contains the appropriate object.

describe('[function]: getMonths', () => {
	it('should return an array containing object', () => {
		const months = getMonths();
		const april = { name: 'April', length: 30 };
		expect(months).toContain(april);
	});
});

To match or not to match, that is the question

In the past examples we saw how to test exact values. But what about when we want to check that something does not match the value provided? Checking that something does not match can be done by adding a simple not before the matcher.

expect('John').not.toBe('Jane');

.toBeDefined() / .toBeUndefined()

When you want to check that the function simply returns some value, but you don’t care which value it is, you can use toBeDefined, or toBeUndefined when you want to check that it doesn’t return a value.

.toBeTruthy() / .toBeFalsy()

When you want to check that the value is true or false in boolean sense, you can use .toBeTruthy or toBeFalsy. A falsy value is any of the following values: 0, false, null, undefined, NaN, and ''. Everything else is considered a truthy value.

Best practices

Writing tests is not just scribbling a few test blocks and leaving it like that. Tests, same as any other code, needs to be organised and well-written. This part gives you four tips for writing the best tests.

Tip 1 - Write mock data in separate file

Most of the time, your functions will accept objects, and passing objects directly in the function will make your test hard to read. You could create an object inside the test file, that you can then pass into the function. However, this will only add more unnecessary lines to the test file, and make the tests unorganised once again. So, instead of writing this:

it('test', () => {
  expect(testFunction({ name: 'test', value: 0 ...})).toBe(...);
});

or

it('test', () => {
  const testObject = { name: 'test', value: 0 ...};
  expect(testFunction(testObject)).toBe(...);
});

write this:

// dataMocks.js
...
export const mockObject = {name: 'test', value: 0 ...};
// example.test.js
import { mockObject } from './dataMocks.js';
...
it('test', () => {
  expect(testFunction(mockObject)).toBe(...);
});

Tip 2 - Organize tests in describe blocks

Do not just write test blocks, line after line. Like each function test should be wrapped in one describe block, the same should be done for every test case. For example, a function has two return cases, one when the condition is met, and the second when it isn't. So the test should look like this:

describe('function test', () => {
  describe('function meets condition', () => {
    it('test', () => { ... });
    ...
  });
  describe('function does not meet condition', () => {
    it('test', () => { ... });
  ...
  });
});

Tip 3 - Test every case, expected and unexpected

You know that joke, the one with the tester walking into a bar:

A software QA engineer walks into a bar.
He orders a beer. Orders 0 beers. Orders 99999999999 beers. Orders a lizard. Orders -1 beers. Orders a ueicbksjdhd. First real customer walks in and asks where the bathroom is. The bar bursts into flames, killing everyone.

Well, this joke pretty much sums up what you should do. Test the expected and the unexpected. It is not enough to only check that the function returns a correct result when receiving proper values. We also have to cover the cases when faulty values are passed. Testing for faulty values will show weak spots in the code, and by eliminating the weak spots, the code will become more unbreakable, leading to fewer bugs and stronger applications.

Tip 4 - Independent tests

Last tip, the most important one, is to write independent tests. What does this mean? It means that each test, for the same function, should be independent of the previous one. Each new test should call the function again, even if the data passed into it is the same. This ensures that everytime any of the tests is run, the test will give the correct feedback whether it has passed or not, and there will be no faulty positives or negatives.

If the function will be called with the same parameters for very test, then we can use beforeEach block, which allows us to write code block which will be called before each test starts. There are also:

  • afterEach - which runs code block after each test
  • beforeAll - which runs code block before every test inside one describe block (runs only once)
  • afterAll - similar to beforeAll, but runs code block after all tests have been executed inside of one describe block.

Pretty neat, if you ask me.

Conclusion

Writing tests may feel dreadful, boring and time-consuming when just starting out. But as more tests you write, the more natural and meaningful it will become. Using testing libraries, such as Jest will make it even easier. The well documented library offers many APIs for every possible test case, which also help keep tests well organised, readable and in turn make tested code stronger. So next time you see a "Write Tests" ticket on your jira board, do not panic you know exactly what to do.

To recap, write mock data in a separate file for more readable code, organise tests in describe blocks, test for every use-case no mater how unexpected, and above all don't run away from independent tests.

Keep it memorised and your team - and future self - will thank you for it.

References:

[1] Jest. 2022. Jest · 🃏 Delightful JavaScript Testing. [online] Available at: https://jestjs.io/ [Accessed 24 April 2022].