Test Structure: describe, it, expect
As your test suite grows, you need ways to organize tests logically. Jest provides describe blocks to group related tests, and a clear pattern emerges: describe what you're testing, it should do something, and you expect a specific result.
The describe Block
describe groups related tests together:
const { add, subtract, multiply, divide } = require('./math');
describe('Math functions', () => {
test('adds numbers', () => {
expect(add(1, 2)).toBe(3);
});
test('subtracts numbers', () => {
expect(subtract(5, 3)).toBe(2);
});
});
Nesting describe Blocks
You can nest describe blocks for more organization:
describe('Math functions', () => {
describe('add', () => {
test('adds positive numbers', () => {
expect(add(1, 2)).toBe(3);
});
test('adds negative numbers', () => {
expect(add(-1, -2)).toBe(-3);
});
test('adds zero', () => {
expect(add(5, 0)).toBe(5);
});
});
describe('divide', () => {
test('divides evenly', () => {
expect(divide(10, 2)).toBe(5);
});
test('throws on divide by zero', () => {
expect(() => divide(10, 0)).toThrow();
});
});
});
Output becomes nicely nested:
PASS ./math.test.js
Math functions
add
✓ adds positive numbers
✓ adds negative numbers
✓ adds zero
divide
✓ divides evenly
✓ throws on divide by zero
The it Function
it is an alias for test, but reads more naturally in BDD (Behavior-Driven Development) style:
describe('User', () => {
it('should have a name', () => {
const user = new User('Alice');
expect(user.name).toBe('Alice');
});
it('should start with zero followers', () => {
const user = new User('Alice');
expect(user.followers).toBe(0);
});
});
Read it aloud: "User... it should have a name." Natural!
Setup and Teardown
Often tests need common setup. Jest provides hooks for this:
beforeEach and afterEach
Runs before/after each test in the block:
describe('ShoppingCart', () => {
let cart;
beforeEach(() => {
// Fresh cart for each test
cart = new ShoppingCart();
});
afterEach(() => {
// Clean up after each test
cart.clear();
});
it('starts empty', () => {
expect(cart.items).toHaveLength(0);
});
it('can add items', () => {
cart.addItem({ name: 'Apple', price: 1 });
expect(cart.items).toHaveLength(1);
});
it('calculates total', () => {
cart.addItem({ name: 'Apple', price: 1 });
cart.addItem({ name: 'Banana', price: 2 });
expect(cart.total).toBe(3);
});
});
beforeAll and afterAll
Runs once before/after all tests in the block:
describe('Database operations', () => {
let db;
beforeAll(async () => {
// Connect once before all tests
db = await connectToDatabase();
});
afterAll(async () => {
// Disconnect once after all tests
await db.disconnect();
});
it('can insert records', async () => {
const result = await db.insert({ name: 'Test' });
expect(result.id).toBeDefined();
});
it('can query records', async () => {
const records = await db.findAll();
expect(records.length).toBeGreaterThan(0);
});
});
Scoped Setup
Setup hooks are scoped to their describe block:
describe('outer', () => {
beforeAll(() => console.log('outer beforeAll'));
beforeEach(() => console.log('outer beforeEach'));
describe('inner', () => {
beforeAll(() => console.log('inner beforeAll'));
beforeEach(() => console.log('inner beforeEach'));
it('test', () => {
console.log('test runs');
});
});
});
// Output order:
// outer beforeAll
// inner beforeAll
// outer beforeEach
// inner beforeEach
// test runs
The expect Function
expect is how you make assertions. It wraps a value and provides matchers:
// Basic pattern
expect(actualValue).matcher(expectedValue);
// Examples
expect(2 + 2).toBe(4);
expect(user.name).toBe('Alice');
expect(items).toHaveLength(3);
Chaining with not
You can negate any matcher with .not:
expect(2 + 2).not.toBe(5);
expect(user.name).not.toBeUndefined();
expect(errors).not.toHaveLength(0);
Try It: Organize These Tests
Here's a messy test file. Reorganize it using describe blocks:
Skipping and Focusing Tests
During development, you might want to run only specific tests:
Skip Tests with .skip
describe('Feature', () => {
it('works correctly', () => {
// This runs
});
it.skip('is not yet implemented', () => {
// This is skipped
});
});
Focus Tests with .only
describe('Feature', () => {
it.only('I want to focus on this test', () => {
// Only this runs
});
it('this is skipped', () => {
// Skipped because .only is used above
});
});
Warning: Don't commit .only to your codebase! It will skip other tests in CI.
Test Naming Best Practices
Good test names describe the expected behavior:
// Pattern: "it should [expected behavior] when [condition]"
describe('LoginForm', () => {
it('should display an error when password is too short', () => {
// ...
});
it('should disable submit button when form is invalid', () => {
// ...
});
it('should redirect to dashboard after successful login', () => {
// ...
});
});
Or use the "given/when/then" pattern:
describe('ShoppingCart', () => {
describe('given an empty cart', () => {
it('should show zero total', () => {});
it('should display empty cart message', () => {});
});
describe('when adding an item', () => {
it('should increase item count', () => {});
it('should update the total', () => {});
});
});
Key Takeaways
- Use
describeto group related tests - Nest
describeblocks for logical organization - Use
it(alias fortest) for BDD-style test names beforeEach/afterEachrun before/after each testbeforeAll/afterAllrun once for the whole block- Use
.skipto temporarily skip tests - Use
.onlyto focus on specific tests (don't commit!) - Write descriptive test names that explain expected behavior
Next, we'll explore Jest's powerful matchers for making assertions!

