Skip to main content

Command Palette

Search for a command to run...

Mastering Jest: A Comprehensive Guide to Automated Testing in JavaScript

Published
9 min readView as Markdown
A

I am Aryan Mankame, SDE-2 at Deutsche Bank CSE 2024 Batch passout at Maulana Azad National Institute of Technology, Bhopal. With a strong passion for technology and a focus on full stack development, I possess a diverse skill set that includes: Frontend: React.js: I am proficient in building captivating and interactive user interfaces using React.js, ensuring a seamless user experience. Redux.js: I am adept at utilizing Redux.js for efficient state management, enabling smooth data flow within applications. HTML5 & CSS3: I have a keen eye for crafting visually appealing and responsive web pages using the latest HTML5 and CSS3 techniques. Backend: Node.js & Express.js: With expertise in Node.js and Express.js, I develop scalable and robust server-side applications. RESTful APIs: I am skilled in designing and implementing RESTful APIs that facilitate seamless communication between frontend and backend systems. Database: MongoDB, Firebase, PostgreSQL, MySQL: I have hands-on experience working with both SQL and NoSQL databases, ensuring efficient data storage and retrieval. My commitment to innovation and problem-solving has been demonstrated through my active participation in various hackathons. Notable achievements include securing the 2nd Runners up position in Wittyhacks 3.0, attaining the 7th position in the Ecell NITB Hackathon, and ranking among the top 15 participants in BitsHackathon. Driven by a desire to make a meaningful impact in the field of technology, my goal is to leverage my skills and enthusiasm to contribute to cutting-edge projects and drive the advancement of software development. Overall, I am a talented and dedicated full stack developer with a proven track record in both frontend and backend technologies. My strong technical foundation, coupled with my passion for innovation, positions me as an invaluable asset to any software development team.

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');
});

More from this blog

We Think Big

10 posts