Mastering Unit Tests for Developers Using Jest - Part 1

Mastering Unit Tests for Developers Using Jest - Part 1

Introduction

In the fast-paced world of software development, ensuring the quality of code is essential for delivering robust and reliable applications. Unit testing plays a pivotal role in this process, allowing developers to validate individual code components in isolation.

In this blog, we will explore the significance of unit testing and delve into the capabilities of Jest, highlighting why it has become a go-to tool for developers.

Unit Testing - isn’t it a tester’s job?

Well, the answer is No, Unit testing is primarily for developers because it is a testing approach focused on verifying the correctness and reliability of individual units or components of a software application.

What is Jest?

Among the numerous testing frameworks available, Jest has emerged as a popular choice due to its simplicity, speed, and extensive feature set. Jest is an open-source JavaScript testing framework developed by Facebook. It is specifically designed for testing React applications but can be used for any JavaScript codebase.

Jest documentationhttps://jestjs.io/docs/getting-started

Getting Started

Installation

Use npm or yarn to install Jest as a development dependency in your project.

npm install -D jest

This package is only for use in development therefore use -D

Configuration

Add the following section to your package.json:

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

Run watch mode:

jest --watch #runs jest -o by default
jest --watchAll #runs all tests

Writing your first test

Tests can be defined using it or test. I prefer to use it. You can choose your way.

//add.js

const add = (a, b) => a + b;
module.exports = add;
// add.test.js <- It is a Test file with test.js extension

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

it("should add two numbers", () => {
  expect(add(1, 2)).toBe(3);
});

Run the test

Run the command npm test in your terminal

# Output
PASS  ./add.test.js
  √ should add two numbers (2 ms)

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

Using Matchers

Expect

  • expect(value): The expect function accepts a single argument, which is the value you want to make assertions about.

    It gives you access to a number of matchers that allow you to validate different things

      expect(result).toBe(expected);
      expect(array).toHaveLength(length);
      expect(value).toBeTruthy();
    

Modifiers

  • .not: Lets you test its opposite

      expect(value).not.toBeTruthy();
    
  • .resolves: Use resolves to unwrap the value of a fulfilled promise so any other matcher can be chained. If the promise is rejected the assertion fails. More detailhere

      it('should resolves to lemon', () => {
        // make sure to add a return statement
        return expect(Promise.resolve('lemon')).resolves.toBe('lemon');
      });
    
  • .rejects: Use .rejects to unwrap the reason of a rejected promise so any other matcher can be chained. If the promise is fulfilled the assertion fails. More detailhere

       it('should rejects to octopus', () => {
        // make sure to add a return statement
        return expect(Promise.reject(new Error('octopus'))).rejects.toThrow(
          'octopus',
        );
      });
    

Commonly used Matchers

Matchers are set functions used to compare the test value. See list here

Simple Assertions

Use expect() methods chained with toBe() method. It accepts a value as a matcher

it("should add 2 with 2", () => {
    expect(2 + 2).toBe(4);
})

Object / Array Assertion

Use expect() method chained with toEqual() method. It accepts an object or array as a matcher

// Proper way
it('should assign new key value pair in object', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2});
});

// Best way
it('should assign new key value pair in object', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toMatchObject({one: 1, two: 2});
});

toEqual ignores object keys with undefined properties, undefined array items, array sparseness, or object type mismatch. use toStrictEqual instead.

Truthiness

You sometimes need to distinguish between undefined, null, and false

  • toBeNull matches only null

  • toBeUndefined matches only undefined

  • toBeDefined is the opposite of toBeUndefined

  • toBeTruthy matches anything that an if statement treats as true

  • toBeFalsy matches anything that an if statement treats as false

it('should be null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});
// Outputnull (3 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

You should use the matcher that most precisely corresponds to what you want your code to be doing.

Numbers

Most ways of comparing numbers have matcher equivalents.

  • .toBe(value)

  • .toEqual(value/object)

  • .toBeGreaterThan(num)

  • .toBeGreaterThanOrEqual(num)

  • .toBeLessThan(num)

  • .toBeLessThanOrEqual(num)

  • .toBeCloseTo(float)

it('should check two numbers', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

it('should add floating point numbers', () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3);           This won't work because of rounding error
  expect(value).toBeCloseTo(0.3); // This works.
});

Strings

You can check strings against regular expressions with toMatch

it('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

it('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

Arrays and iterables

You can check if an array or iterable contains a particular item using toContain

const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'milk',
];

// Proper way
test('the shopping list has milk on it', () => {
  expect(shoppingList).toContain('milk');
  expect(new Set(shoppingList)).toContain('milk');
});

// Best way
test("the shopping list has milk on it", () => {
  expect(shoppingList).toEqual(expect.arrayContaining(["milk", "diapers"]));
});

Exceptions

If you want to test whether a particular function throws an error when it's called, use toThrow

function compileAndroidCode() {
  throw new Error('you are using the wrong JDK!');
}

test('compiling android goes as expected', () => {
  expect(() => compileAndroidCode()).toThrow();
  expect(() => compileAndroidCode()).toThrow(Error);

  // You can also use a string that must be contained in the error message or a regexp
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
  expect(() => compileAndroidCode()).toThrow(/JDK/);

  // Or you can match an exact error message using a regexp like below
  expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK$/); // Test fails
  expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK!$/); // Test pass
});

The function that throws an exception needs to be invoked within a wrapping function otherwise the toThrow assertion will fail.

That’s it for this blog. There are a lot of topics to cover such as before, after Test Methods, Setup, Order of Execution and Mock functions which I will be covering in part 2 of this blog.

Wrapup 🥱

Thanks for reading! I really hope you enjoyed reading this short blog. If you are also a passionate developer like me then please give it a thumps-up that will keep me motivated.

If you have any query/suggetions/feedback please connect with me on twitter or ask me in the comment section.

Like and Follow 😊

Meet you on the next blog. Enjoy Coding ❤

Did you find this article valuable?

Support Ashish Jaiswar by becoming a sponsor. Any amount is appreciated!