Introduction to Jest
Jest is a delightful JavaScript testing framework developed by Facebook. It's designed to ensure correctness of any JavaScript codebase with a focus on simplicity and developer experience. Think of Jest as your quality assurance partner, constantly checking that your code does exactly what you expect it to do.
Real-world analogy: If software development were cooking, Jest would be like having a team of food critics constantly tasting your dishes to ensure they meet the expected flavor profile, texture, and presentation before they reach customers.
Getting Started with Jest
Installation
Setting up Jest in your project is straightforward:
// Initialize npm in your project (if not already done)
npm init -y
// Install Jest as a development dependency
npm install --save-dev jest
// Update package.json to use Jest for testing
// In "scripts" section add:
// "test": "jest"
Creating Your First Test
Let's create a simple function to test. Create a file called sum.js:
// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
Now, create a test file called sum.test.js:
// sum.test.js
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
To run the test, simply execute:
npm test
You should see output indicating that your test passed!
Jest Test Structure
Jest tests follow a clear and intuitive structure:
// Basic test structure
test('description of what the test should verify', () => {
// Setup - Prepare the data/environment
// Exercise - Call the function being tested
// Verify - Check that the result is what you expect
// Teardown (optional) - Clean up any resources
});
For grouping related tests, Jest provides the describe block:
// Grouping related tests
describe('Math utility functions', () => {
test('sum adds numbers correctly', () => {
expect(sum(2, 3)).toBe(5);
});
test('multiply works properly', () => {
expect(multiply(3, 4)).toBe(12);
});
});
For setup and teardown operations, Jest provides special functions:
// Setup and teardown
describe('Database operations', () => {
beforeAll(() => {
// Run once before all tests in this describe block
// Often used to set up database connections
});
afterAll(() => {
// Run once after all tests in this describe block
// Typically used to close connections or clean up resources
});
beforeEach(() => {
// Run before each test in this describe block
// Perfect for resetting to a known state
});
afterEach(() => {
// Run after each test in this describe block
// Good for cleaning up after each test
});
test('can save a record', () => {
// Test implementation
});
});
Matchers: Verifying Results
Jest provides a rich set of matchers to verify different types of values and conditions:
Common Matchers
// Exact equality
expect(2 + 2).toBe(4); // Checks with ===
// Deep equality for objects and arrays
expect({ name: 'John' }).toEqual({ name: 'John' });
// Truthiness
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('defined').toBeDefined();
Number Matchers
// Comparison
expect(10).toBeGreaterThan(9);
expect(10).toBeGreaterThanOrEqual(10);
expect(10).toBeLessThan(11);
expect(10).toBeLessThanOrEqual(10);
// Floating point
expect(0.1 + 0.2).toBeCloseTo(0.3);
String Matchers
// String matching
expect('hello world').toMatch(/world/);
expect('hello world').toContain('world');
Array Matchers
// Array contents
const shoppingList = ['milk', 'bread', 'eggs'];
expect(shoppingList).toContain('milk');
expect(shoppingList).toHaveLength(3);
Exception Matchers
// Testing that a function throws an error
function throwError() {
throw new Error('This is an error');
}
expect(() => throwError()).toThrow();
expect(() => throwError()).toThrow('This is an error');
Real-world example: Think of matchers like quality inspectors in a factory. Different inspectors (matchers) specialize in checking different aspects of the product—dimensions, weight, appearance, functionality, etc.
Practical Example: Testing a Shopping Cart
Let's implement tests for a simple shopping cart module that allows adding items, removing items, and calculating the total.
First, our implementation (shoppingCart.js):
// shoppingCart.js
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(item) {
if (!item.name || !item.price || item.price <= 0) {
throw new Error('Invalid item');
}
// Check if item already exists
const existingItemIndex = this.items.findIndex(i => i.name === item.name);
if (existingItemIndex >= 0) {
// Increment quantity if item exists
this.items[existingItemIndex].quantity += item.quantity || 1;
} else {
// Add new item with default quantity of 1 if not specified
this.items.push({
...item,
quantity: item.quantity || 1
});
}
return this.items;
}
removeItem(itemName) {
const initialLength = this.items.length;
this.items = this.items.filter(item => item.name !== itemName);
// Return true if an item was removed
return this.items.length < initialLength;
}
getTotal() {
return this.items.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
}
clear() {
this.items = [];
}
}
module.exports = ShoppingCart;
Now, let's write tests for this cart (shoppingCart.test.js):
// shoppingCart.test.js
const ShoppingCart = require('./shoppingCart');
describe('Shopping Cart', () => {
let cart;
beforeEach(() => {
// Reset the cart before each test
cart = new ShoppingCart();
});
test('should start with an empty cart', () => {
expect(cart.items).toEqual([]);
expect(cart.getTotal()).toBe(0);
});
test('should add an item to the cart', () => {
const item = { name: 'Keyboard', price: 50 };
cart.addItem(item);
expect(cart.items).toHaveLength(1);
expect(cart.items[0].name).toBe('Keyboard');
expect(cart.items[0].price).toBe(50);
expect(cart.items[0].quantity).toBe(1);
});
test('should increase quantity when adding the same item', () => {
const item = { name: 'Mouse', price: 25 };
cart.addItem(item);
cart.addItem(item);
expect(cart.items).toHaveLength(1);
expect(cart.items[0].quantity).toBe(2);
});
test('should allow adding an item with a specific quantity', () => {
const item = { name: 'Headphones', price: 100, quantity: 2 };
cart.addItem(item);
expect(cart.items[0].quantity).toBe(2);
});
test('should throw error when adding invalid item', () => {
const invalidItem1 = { price: 50 }; // Missing name
const invalidItem2 = { name: 'Keyboard' }; // Missing price
const invalidItem3 = { name: 'Keyboard', price: -10 }; // Negative price
expect(() => cart.addItem(invalidItem1)).toThrow();
expect(() => cart.addItem(invalidItem2)).toThrow();
expect(() => cart.addItem(invalidItem3)).toThrow();
});
test('should remove an item from the cart', () => {
cart.addItem({ name: 'Keyboard', price: 50 });
const result = cart.removeItem('Keyboard');
expect(result).toBe(true);
expect(cart.items).toHaveLength(0);
});
test('should return false when removing an item that does not exist', () => {
const result = cart.removeItem('NonExistent');
expect(result).toBe(false);
});
test('should calculate the correct total', () => {
cart.addItem({ name: 'Keyboard', price: 50 });
cart.addItem({ name: 'Mouse', price: 25 });
cart.addItem({ name: 'Headphones', price: 100, quantity: 2 });
expect(cart.getTotal()).toBe(275); // 50 + 25 + (100 * 2)
});
test('should clear all items', () => {
cart.addItem({ name: 'Keyboard', price: 50 });
cart.addItem({ name: 'Mouse', price: 25 });
cart.clear();
expect(cart.items).toHaveLength(0);
expect(cart.getTotal()).toBe(0);
});
});
This comprehensive test suite ensures that each feature of our shopping cart works as expected. By running these tests, we can confidently make changes to our implementation knowing that we'll catch any regressions.
Advanced Jest Features
Watch Mode
Jest can watch for file changes and automatically run tests:
npm test -- --watch
Test Coverage
Jest includes built-in code coverage reporting:
npm test -- --coverage
This generates a comprehensive report showing which parts of your code are covered by tests and which aren't.
Test Filtering
You can run specific tests by name pattern:
// Run only tests with "total" in the name
npm test -- -t "total"
Snapshot Testing
Snapshot testing is great for testing UI components or data structures that you expect to remain consistent:
test('product card renders correctly', () => {
const product = {
id: 1,
name: 'Mechanical Keyboard',
price: 79.99,
imageUrl: '/images/keyboard.png'
};
const card = renderProductCard(product);
// This creates/verifies a snapshot of the rendered output
expect(card).toMatchSnapshot();
});
When you run this test the first time, Jest creates a snapshot file. On subsequent runs, it compares the output with the saved snapshot to ensure consistency.
Best Practices for Jest Testing
File Organization
- Tests alongside source files: Place test files in the same directory as the source files they test (e.g.,
component.jsandcomponent.test.js) - Dedicated test directory: Alternatively, use a
__tests__directory in your project structure
Testing Strategy
- Test behavior, not implementation: Focus on what your code does, not how it does it
- Test one thing per test: Each test should verify a single aspect of functionality
- Use descriptive test names: Test names should clearly indicate what is being tested and the expected outcome
- Arrange-Act-Assert pattern: Structure tests with clear setup, execution, and verification phases
Avoiding Common Pitfalls
- Avoid test interdependence: Tests should not depend on each other
- Beware of over-mocking: Excessive mocks can lead to tests that pass but don't verify actual functionality
- Maintain test quality: Poorly written tests can be worse than no tests
Real-world example: At Airbnb, they organize their Jest tests alongside the components they test. They've found that this proximity makes developers more likely to update tests when they change the component code, leading to fewer broken tests and better test coverage.
When to Use Jest
Jest is particularly well-suited for:
- React applications - Jest was built by Facebook with React in mind
- Node.js backend code - Great for testing APIs, utilities, and business logic
- Any JavaScript/TypeScript project - Works well with vanilla JS, Vue, Angular, etc.
Real-world applications: Companies like Facebook, Twitter, Airbnb, Instagram, and Spotify use Jest for their JavaScript testing needs. For example, Spotify rebuilt their web player with React and uses Jest for testing to ensure consistent playback behavior across browsers and devices.
Practical Exercise
Exercise: Testing a Form Validator
Let's practice by writing tests for a form validation utility. The utility should validate:
- Email addresses (must be valid format)
- Passwords (minimum 8 characters, at least one uppercase, one lowercase, one number)
- Username (alphanumeric, 3-20 characters)
Use this skeleton:
// formValidator.js
const formValidator = {
validateEmail(email) {
// Implement email validation
},
validatePassword(password) {
// Implement password validation
},
validateUsername(username) {
// Implement username validation
}
};
module.exports = formValidator;
// formValidator.test.js
const validator = require('./formValidator');
describe('Form Validator', () => {
// Write your tests here
});
Summary
- Jest is a powerful JavaScript testing framework that provides a complete solution with minimal configuration
- Key components include the test runner, assertion library, mocking capabilities, and snapshot testing
- Basic test structure involves
describeandtestblocks with expectations using matchers - Jest provides a rich set of matchers for verifying different types of values and behaviors
- Advanced features include watch mode, code coverage reporting, and snapshot testing
- Best practices include organizing tests effectively, focusing on behavior rather than implementation, and maintaining test independence
Remember: The goal of testing is not just to identify bugs but to give you confidence in your code and enable fearless refactoring.
Assignment
Create a complete Jest test suite for a URL shortener utility:
- Create a
urlShortener.jsmodule with the following functions:shortenUrl(url)- Generate a short code for a URLexpandUrl(code)- Retrieve the original URL for a short codevalidateUrl(url)- Check if a URL is validgetStats(code)- Get usage statistics for a short code
- Write a comprehensive test suite in
urlShortener.test.js - Include edge cases like invalid URLs, non-existent codes, etc.
- Generate a code coverage report and aim for at least 90% coverage
- Include a brief explanation of your testing strategy
Bonus challenge: Implement mock persistence (using Jest mocks) to simulate storing URLs in a database.