Unit testing is a fundamental practice in software development that helps ensure code quality, catch bugs early, and make refactoring safer. In this module, you'll learn how to write effective unit tests using Jest, a popular JavaScript testing framework.
Understand the principles of unit testing, test-driven development (TDD), and the testing pyramid.
Master Jest's features including test organization, assertions, mocks, and test coverage reporting.
Learn common testing patterns and best practices for writing maintainable and effective tests.
Testing is an essential skill for a web developer to have. It's hard to anticipate every way that a user might interact with your site, not to mention it is incredibly time-consuming to test all of those options manually.
If we don't have tests, it's safe to assume the following:
There are many testing tools available for JavaScript applications. Examples include Jest, Mocha/Chai, Jasmine, Qunit, Enzyme, Supertest, Istanbul, Karma, and Cypress.
For this module, we'll focus on Jest, a JavaScript testing framework that works particularly well with React applications.
Jest is a test runner and command-line interface npm package. It was originally made by Facebook and is included out-of-the-box with create-react-app. Jest can run asynchronous tests, snapshot testing, and produce coverage reports.
Instead of running tests manually, Jest has a built-in watch mode feature that will run tests automatically as files change. Jest detects these changes automatically and only runs the tests related to the changes.
Test-Driven Development is a software development approach where tests are written before the code that needs to be tested. The TDD cycle is:
Benefits of TDD include:
To set up Jest in a Node.js project, follow these steps:
npm install -D jest
// In package.json { "scripts": { "test": "jest --watch" } }
Jest will find your tests in two ways:
.js
files inside a folder called __tests__
.test.js
or .spec.js
npm test
Here's an example of a basic test file structure:
// math.js function sum(a, b) { return a + b; } function multiply(a, b) { return a * b; } module.exports = { sum, multiply }; // math.test.js const { sum, multiply } = require('./math'); describe('Math module', () => { // Test suite for the sum function describe('sum function', () => { test('adds 1 + 2 to equal 3', () => { expect(sum(1, 2)).toBe(3); }); test('adds negative numbers correctly', () => { expect(sum(-1, -2)).toBe(-3); }); }); // Test suite for the multiply function describe('multiply function', () => { test('multiplies 2 * 3 to equal 6', () => { expect(multiply(2, 3)).toBe(6); }); test('multiplies with zero to equal zero', () => { expect(multiply(5, 0)).toBe(0); }); }); });
Jest provides several functions to organize your tests effectively:
describe()
: Group related tests in a test suitetest()
or it()
: Define individual test casesbeforeEach()
: Run code before each test in a suiteafterEach()
: Run code after each test in a suitebeforeAll()
: Run code once before all tests in a suiteafterAll()
: Run code once after all tests in a suitedescribe('User Authentication', () => { let db; // Setup - runs once before all tests beforeAll(() => { db = connectToTestDatabase(); }); // Cleanup - runs once after all tests afterAll(() => { disconnectFromDatabase(db); }); // Reset for each test beforeEach(() => { resetTestUser(db); }); // Test cases test('registers a new user successfully', () => { // Test registration }); test('fails to register with invalid email', () => { // Test invalid registration }); test('logs in an existing user', () => { // Test login }); });
Jest provides a rich set of matchers to test different types of values:
// Equality expect(value).toBe(exactValue); // Exact equality (===) expect(value).toEqual(obj); // Deep equality for objects // Truthiness expect(value).toBeTruthy(); // Truthy value expect(value).toBeFalsy(); // Falsy value expect(value).toBeNull(); // Null value expect(value).toBeUndefined(); // Undefined value expect(value).toBeDefined(); // Not undefined // Numbers expect(value).toBeGreaterThan(number); // > number expect(value).toBeGreaterThanOrEqual(number); // >= number expect(value).toBeLessThan(number); // < number expect(value).toBeLessThanOrEqual(number); // <= number // Strings expect(string).toMatch(/regex/); // Match regex // Arrays and Iterables expect(array).toContain(item); // Contains item expect(array).toHaveLength(number); // Has length // Objects expect(object).toHaveProperty(keyPath); // Has property
Mocks allow you to replace real implementations with test doubles. This is particularly useful for:
// userService.js const axios = require('axios'); async function getUsers() { const response = await axios.get('https://api.example.com/users'); return response.data; } module.exports = { getUsers }; // userService.test.js const axios = require('axios'); const { getUsers } = require('./userService'); // Mock the axios module jest.mock('axios'); test('fetches users successfully', async () => { // Setup mock response const users = [{ id: 1, name: 'John' }, { id: A, name: 'Jane' }]; axios.get.mockResolvedValue({ data: users }); // Call the function const result = await getUsers(); // Verify axios.get was called correctly expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users'); // Verify result expect(result).toEqual(users); });
Jest provides several ways to test asynchronous code:
// Using async/await (recommended) test('async function test with async/await', async () => { const data = await fetchData(); expect(data).toBe('expected data'); }); // Using promises test('async function test with promises', () => { return fetchData().then(data => { expect(data).toBe('expected data'); }); }); // Using the done callback (older style) test('async function test with done callback', done => { fetchData().then(data => { expect(data).toBe('expected data'); done(); }); });
test('handles errors in async functions', async () => { // Using .rejects with async/await await expect(fetchDataWithError()).rejects.toThrow('Network Error'); // Alternative approach try { await fetchDataWithError(); // If we reach this line, the test should fail expect(true).toBe(false); // This will never pass } catch (error) { expect(error.message).toBe('Network Error'); } });