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