Module 3: Testing with Jest
Learning Objectives
By the end of this module, you should be able to:
- Understand the importance of testing in software development
- Set up and configure Jest for a JavaScript project
- Write unit tests using Jest's testing framework
- Implement test-driven development (TDD) practices
- Use Jest matchers and assertions effectively
- Create test suites and organize tests properly
Video Lesson
Introduction to Testing
Testing is a critical part of software development that ensures your code functions as expected and helps prevent bugs. Automated testing allows you to verify your code's correctness continuously and catch issues early.
Types of Tests
- Unit Tests: Test individual functions or components in isolation
- Integration Tests: Test how multiple components work together
- End-to-End Tests: Test the entire application workflow
Why Jest?
Jest is a popular JavaScript testing framework maintained by Facebook. It's designed to be simple to set up with zero configuration and includes built-in functionality for:
- Test runners
- Assertion libraries
- Mocking capabilities
- Coverage reports
- Snapshot testing
Testing may seem like extra work initially, but it saves time in the long run by catching bugs early and making your code more maintainable.
Setting Up Jest
Let's start by setting up Jest in a Node.js project:
Installation
npm install --save-dev jest
Configuration
Add Jest to your package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Create a jest.config.js file for advanced configuration (optional):
// jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
collectCoverage: true,
collectCoverageFrom: [
'**/*.{js,jsx}',
'!**/node_modules/**',
'!**/vendor/**'
]
};
Writing Your First Jest Test
Let's write some basic tests to understand how Jest works.
Test File Naming Conventions
Jest automatically finds tests in files that match these patterns:
- Files with
.test.js
suffix - Files with
.spec.js
suffix - Files inside a
__tests__
directory
Basic Test Structure
// 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);
});
Running Tests
npm test
Test output should look like:
PASS ./sum.test.js
✓ adds 1 + 2 to equal 3 (3ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.789s
Ran all tests.
Jest Matchers and Assertions
Jest provides a variety of matchers to help you verify different types of conditions:
Common Matchers
// Equality
expect(value).toBe(exactValue); // exact equality (===)
expect(object).toEqual(anotherObject); // deep equality
// Truthiness
expect(value).toBeTruthy(); // truthy value
expect(value).toBeFalsy(); // falsy value
expect(value).toBeNull(); // null
expect(value).toBeUndefined(); // undefined
expect(value).toBeDefined(); // not undefined
// Numbers
expect(value).toBeGreaterThan(number);
expect(value).toBeGreaterThanOrEqual(number);
expect(value).toBeLessThan(number);
expect(value).toBeLessThanOrEqual(number);
expect(value).toBeCloseTo(number, numDigits); // for floating-point equality
// Strings
expect(string).toMatch(/regex/); // matches regex
// Arrays and iterables
expect(array).toContain(item); // contains item
expect(array).toHaveLength(number); // has length
// Objects
expect(object).toHaveProperty(keyPath); // has property
expect(function).toThrow(error); // throws error
Example with Different Matchers
test('demonstrates different matchers', () => {
// Primitive equality
expect(2 + 2).toBe(4);
// Object equality
expect({ name: 'Alice', age: 30 }).toEqual({ name: 'Alice', age: 30 });
// Truthiness
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
// Numbers
expect(10).toBeGreaterThan(5);
expect(5).toBeLessThanOrEqual(5);
// Strings
expect('Hello World').toMatch(/World/);
// Arrays
expect(['apple', 'banana', 'orange']).toContain('banana');
expect(['apple', 'banana', 'orange']).toHaveLength(3);
// Exceptions
const throwError = () => { throw new Error('This is an error'); };
expect(throwError).toThrow('This is an error');
});
Organizing Tests with Describe and Test Blocks
Jest provides ways to organize your tests into logical groups:
Describe and Test (or it)
// userService.test.js
const userService = require('./userService');
describe('User Service', () => {
// You can have setup and teardown for each describe block
beforeAll(() => {
// Run before all tests in this describe block
console.log('Setting up User Service tests');
});
afterAll(() => {
// Run after all tests in this describe block
console.log('Cleaning up after User Service tests');
});
beforeEach(() => {
// Run before each test in this describe block
// Great for resetting state
});
afterEach(() => {
// Run after each test in this describe block
});
describe('createUser', () => {
test('creates a user successfully with valid data', () => {
const userData = { username: 'testuser', email: 'test@example.com' };
const user = userService.createUser(userData);
expect(user).toHaveProperty('id');
expect(user.username).toBe('testuser');
expect(user.email).toBe('test@example.com');
});
test('throws error when missing required fields', () => {
const userData = { username: 'testuser' }; // Missing email
expect(() => {
userService.createUser(userData);
}).toThrow('Email is required');
});
});
describe('findUserById', () => {
test('returns user when found', () => {
const user = userService.findUserById(1);
expect(user).not.toBeNull();
});
test('returns null when user not found', () => {
const user = userService.findUserById(999);
expect(user).toBeNull();
});
});
});
Testing Asynchronous Code
Jest has several ways to test asynchronous code:
Promises
// With promises, return the promise
test('fetchUserData returns user data', () => {
return fetchUserData(1).then(data => {
expect(data).toHaveProperty('name');
});
});
// Using async/await
test('fetchUserData returns user data', async () => {
const data = await fetchUserData(1);
expect(data).toHaveProperty('name');
});
// Testing rejected promises
test('fetchUserData rejects when user does not exist', async () => {
expect.assertions(1); // Ensure the assertion in the catch block is called
try {
await fetchUserData(999);
} catch (error) {
expect(error).toMatch('User not found');
}
});
// Alternative way to test rejected promises
test('fetchUserData rejects when user does not exist', () => {
return expect(fetchUserData(999)).rejects.toMatch('User not found');
});
Callbacks
// For callback APIs, use the done parameter
test('fetchUserDataCallback returns user data', done => {
function callback(error, data) {
if (error) {
done(error);
return;
}
try {
expect(data).toHaveProperty('name');
done();
} catch (error) {
done(error);
}
}
fetchUserDataCallback(1, callback);
});
Mocking with Jest
Mocking is crucial for isolating units of code during testing. Jest provides powerful mocking capabilities:
Function Mocks
// Mock a specific function
const mockFn = jest.fn();
mockFn.mockReturnValue(42);
// or
mockFn.mockImplementation(() => 42);
// With arguments
mockFn.mockReturnValueOnce('first call')
.mockReturnValueOnce('second call')
.mockReturnValue('default');
// Testing a mock was called correctly
test('mock function example', () => {
mockFn(10, 'test');
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith(10, 'test');
});
Module Mocks
// Create a manual mock by creating a __mocks__ directory
// __mocks__/axios.js
module.exports = {
get: jest.fn(() => Promise.resolve({ data: {} }))
};
// In your test file:
jest.mock('axios');
const axios = require('axios');
test('fetches data with axios', async () => {
// Set up the mock return value for this test
axios.get.mockResolvedValueOnce({
data: { id: 1, name: 'Test User' }
});
const result = await fetchUser(1);
expect(axios.get).toHaveBeenCalledWith('/users/1');
expect(result).toEqual({ id: 1, name: 'Test User' });
});
Spying on Methods
// Spy on an object method
const user = {
setName(name) {
this.name = name;
}
};
test('spy on setName method', () => {
const spy = jest.spyOn(user, 'setName');
user.setName('Alice');
expect(spy).toHaveBeenCalledWith('Alice');
expect(user.name).toBe('Alice');
// Restore the original implementation
spy.mockRestore();
});
Additional Resources
Practice Assignment
Now it's time to practice writing tests with Jest! Create tests for the authentication and JWT functionality you've been working on:
- Write unit tests for user registration functions
- Test password hashing and verification
- Test JWT generation and verification
- Create mocks for database interactions
- Aim for at least 80% test coverage
Next Steps
In the next module, we'll explore testing backend applications, focusing on API route testing, database testing, and integration testing.
Go to Module 4: Backend Testing