Mastering Jest: A Comprehensive Guide to Automated Testing in JavaScript

1. Understand the Basics of Testing

Before diving into Jest, make sure you understand why testing is important and the different types of testing:

  • Why Testing Matters: Ensure your code works as expected, prevent bugs, facilitate refactoring, and improve code quality.

  • Types of Testing:

    • Unit Testing: Testing individual components or functions.

    • Integration Testing: Testing how different components work together.

    • End-to-End Testing: Testing the application flow from start to finish.S

2. Fundamentals of Jest

What is Jest?

  • Definition: Jest is a JavaScript testing framework maintained by Facebook, designed for simplicity, performance, and compatibility with large projects.

  • Features: Provides built-in matchers, mocks, and assertions; zero-config setup for many projects; supports parallel test execution.

Core Concepts

  • Test Suites and Test Cases: Organize your tests into suites (groups) and cases (individual tests).

  • Matchers: Assertions to validate the expected outcomes.

  • Mocking: Create mock functions and modules to isolate units of code.

  • Setup and Teardown: Lifecycle hooks for preparing the environment before and cleaning up after tests.

3. Setting Up Jest

Installation

  • Installation:
%% Local %%
npm install --save-dev jest
%% Global %%
npm install -g jest

Configuration

Method 1: create a file names jest.config.js and add the config parameters to it. Ex:

module.exports = { 
testEnvironment: 'node', 
verbose: true, 
collectCoverage: true,
coverageDirectory: 'coverage',
testMatch: [ '**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)' ]
// Add more configuration options as needed 
};

Method 2: Add a jest field in the package.json and inside that field add the config parameters. Ex:

{
  "name": "your-project",
  "version": "1.0.0",
  "scripts": {
    "test": "jest"
  },
  "jest": {
    "testEnvironment": "node",
    "verbose": true,
    "collectCoverage": true,
    "coverageDirectory": "coverage",
    "testMatch": [
      "**/__tests__/**/*.js?(x)",
      "**/?(*.)+(spec|test).js?(x)"
    ]
  }
}

Note: The most important is the testMatch its used to identify which files are test files

4. Writing your first test

// sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;

// sum.test.js
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

5. Exploring Matchers:

Matchers in Jest are functions that allow you to assert specific conditions about your code. They form the core of your test expectations, enabling you to check whether the results of your functions or components meet the expected outcomes. Jest provides a wide range of matchers to handle various types of assertions.

Here's a detailed exploration of Jest matchers, including their usage and examples.

Basic Matchers

- toBe

  • Purpose: Checks for strict equality using ===.
test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

- toEqual

  • Purpose: Checks for deep equality. Useful for comparing objects or arrays.
test('object assignment', () => {
  const data = { one: 1 };
  data['two'] = 2;
  expect(data).toEqual({ one: 1, two: 2 });
});

Truthiness Matchers

toBeNull

  • Purpose: Checks if a value is null.

  • Example:

test('null', () => {
  const n = null;
  expect(n).toBeNull();
});

toBeDefined

  • Purpose: Checks if a value is defined.
test('defined', () => {
  const n = null;
  expect(n).toBeDefined();
});

Other Matchers: toBeUndefined

Number Matchers:

Matchers include :

  • toBeGreaterThan

  • toBeGreaterThanOrEqual

  • toBeLessThan

  • toBeLessThanOrEqual

String Matchers:

Matchers include:

  • toMatch

Array and Iterable Matchers

  • toContain

Object Matchers

  • toHaveProperty

Exception Matchers

  • toThrow Purpose: Checks if a function throws an error when called

Asychronous Matchers

  • toResolve/toReject Purpose: Checks if a promise resolves or rejects.

Custom Matcher

expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    if (pass) {
      return {
        message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${received} to be within range ${floor} - ${ceiling}`,
        pass: false,
      };
    }
  },
});

test('numeric ranges', () => {
  expect(100).toBeWithinRange(90, 110);
  expect(200).not.toBeWithinRange(90, 110);
});

6. Mocking Function and Modules

Mocking functions and modules in Jest is a powerful technique that allows you to isolate the code you are testing by replacing dependencies with mock versions. This is useful for controlling the behavior of dependencies, reducing the complexity of tests, and improving test performance.

Mock Functions

Mock functions (or spies) are functions that allow you to test the interactions between different parts of your code. They can track how they are called and what they return.

  • Basic Mock Function
const mockfn = jest.fn();
  • Mock Implementation
const mockFunction = jest.fn().mockImplementation((a, b) => a + b);

test('adds two numbers', () => {
  expect(mockFunction(1, 2)).toBe(3);
  expect(mockFunction).toHaveBeenCalledWith(1, 2);
});
  • Mock Return Value Its used to define which value will be returned when the function is called here, here the value returned will depend on what time the function is called.
const mockFunction = jest.fn()
  .mockReturnValueOnce('first call')
  .mockReturnValueOnce('second call')
  .mockReturnValue('default value');

test('returns specific values for first and second calls, then default', () => {
  expect(mockFunction()).toBe('first call');
  expect(mockFunction()).toBe('second call');
  expect(mockFunction()).toBe('default value');
  expect(mockFunction()).toBe('default value');
});

Mocking modules

Jest can mock entire modules, which is useful when you want to replace a module dependency in your tests.

- Automatic Mocking: You can automatically mock a module using jest.mock('moduleName'):

jest.mock('axios');

const axios = require('axios');

axios.get.mockResolvedValue({ data: 'mock data' });

test('fetches data', async () => {
  const data = await axios.get('https://api.example.com/data');
  expect(data).toEqual({ data: 'mock data' });
});

- Manual Mocking: You can manually create a mock for a module by placing a mock file in a __mocks__ directory.

Ex: Let's say we have a file structure like this

myModule.js
__mocks__/
  myModule.js

Original Module myModule.js:

module.exports = {
  fetchData: async () => {
    // Actual implementation
  }
};

Mock Module:

module.exports = {
  fetchData: jest.fn(() => Promise.resolve('mock data'))
};

Using Mock in Tests

jest.mock('./myModule');
const myModule = require('./myModule');

test('fetches mock data', async () => {
  const data = await myModule.fetchData();
  expect(data).toBe('mock data');
});

Mocking Node Modules

You can mock Node modules just like any other module. For example, to mock the fs module:

jest.mock('fs');

const fs = require('fs');

fs.readFileSync.mockReturnValue('file content');

test('reads file content', () => {
  const content = fs.readFileSync('/path/to/file');
  expect(content).toBe('file content');
});

Advanced Mocking Techniques

  • Mocking Timers: Jest provides functions to control the timers in your tests.
jest.useFakeTimers();

test('delays the execution', () => {
  const callback = jest.fn();

  setTimeout(callback, 1000);
  jest.advanceTimersByTime(1000);

  expect(callback).toHaveBeenCalled();
});
  • Mocking Date: You can mock the Date object to control the current date and time in your tests:
const RealDate = Date;

global.Date = jest.fn(() => new RealDate('2020-01-01T00:00:00Z'));

test('uses mocked date', () => {
  const now = new Date();
  expect(now.toISOString()).toBe('2020-01-01T00:00:00.000Z');
});

global.Date = RealDate; // Restore original Date object after the test
  • Mocking Factories: You can create mock implementations using factories:
jest.mock('axios', () => {
  return {
    get: jest.fn(() => Promise.resolve({ data: 'mock data' })),
    post: jest.fn(() => Promise.resolve({ data: 'mock post data' }))
  };
});

const axios = require('axios');

test('fetches mock data', async () => {
  const data = await axios.get('/endpoint');
  expect(data).toEqual({ data: 'mock data' });
});

test('posts mock data', async () => {
  const data = await axios.post('/endpoint');
  expect(data).toEqual({ data: 'mock post data' });
});

Resetting and Clearing Mocks:

%% Clear Mock Function Calls: %%
mockFunction.mockClear();
%% Reset Mock Function State: %%
mockFunction.mockReset();
%% Restore Original Implementation: %%
mockFunction.mockRestore();

7. Setup and Teardown Functions

In Jest, setup and teardown functions are used to configure the environment for your tests and clean up after tests have run. These functions help ensure that your tests run in isolation and do not interfere with each other.

  • beforeAll(fn): Runs a function once before all the tests in the test file.

  • afterAll(fn): Runs a function once after all the tests in the test file.

  • beforeEach(fn): Runs a function before each test in the test file.

  • afterEach(fn): Runs a function after each test in the test file.

Example Scenario

Consider a simple example where we are testing a database module. We want to connect to the database before all tests run, and disconnect from the database after all tests are completed. Additionally, we might want to reset the database state before and after each test to ensure test isolation.

Database Module (database.js)

const mongoose = require('mongoose');

const connectDB = async () => {
  await mongoose.connect('mongodb://localhost:27017/testdb', {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });
};

const disconnectDB = async () => {
  await mongoose.disconnect();
};

module.exports = { connectDB, disconnectDB };

Test File(database.test.js)

const { connectDB, disconnectDB } = require('./database');
const mongoose = require('mongoose');

beforeAll(async () => {
  await connectDB();
});

afterAll(async () => {
  await disconnectDB();
});

beforeEach(async () => {
  // Reset the database state if necessary
  await mongoose.connection.db.dropDatabase();
});

afterEach(async () => {
  // Clean up any data if necessary
  await mongoose.connection.db.dropDatabase();
});

test('example test', async () => {
  // Your test code here
  const Example = mongoose.model('Example', new mongoose.Schema({ name: String }));
  await Example.create({ name: 'Test' });

  const result = await Example.findOne({ name: 'Test' });
  expect(result.name).toBe('Test');
});
Use Cases for Setup and Teardown
  • Database Connections: Connecting to and disconnecting from a database before and after tests.

  • API Servers: Starting and stopping an API server.

  • Mocking APIs: Setting up and tearing down mock APIs or endpoints.

  • Resetting State: Resetting application state, such as clearing caches or resetting global variables.

  • File System Operations: Creating and deleting files or directories needed for tests.

Advanced Usage

Scoped Setup and Teardown

Jest also allows you to define setup and teardown for specific test suites or describe blocks:

describe('User tests', () => {
  beforeAll(() => {
    // Runs once before all tests in this describe block
  });

  afterAll(() => {
    // Runs once after all tests in this describe block
  });

  beforeEach(() => {
    // Runs before each test in this describe block
  });

  afterEach(() => {
    // Runs after each test in this describe block
  });

  test('should create a user', () => {
    // Test code
  });

  test('should delete a user', () => {
    // Test code
  });
});
Asynchronous Setup and Teardown

If you need to perform asynchronous operations in your setup or teardown functions, you can return a promise or use async/await:

 beforeAll(async () => {
  await asyncSetupFunction();
});

afterAll(async () => {
  await asyncTeardownFunction();
});

8. Advanced Topics:

Snapshot Testing:

// Link.js
const React = require('react');
const renderer = require('react-test-renderer');

test('Link changes the class when hovered', () => {
  const component = renderer.create(
    <Link page="http://www.facebook.com">Facebook</Link>
  );
  let tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});

Testing Asynchronous Code:

// async.js
function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('peanut butter');
    }, 1000);
  });
}
module.exports = fetchData;

// async.test.js
const fetchData = require('./async');

test('the data is peanut butter', async () => {
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

Did you find this article valuable?

Support We Think Big by becoming a sponsor. Any amount is appreciated!