Day 2: Test Structure and Basic Patterns
What You'll Learn Today
- Grouping tests with
describe - The difference between
testandit - Setup and teardown with
beforeEach/afterEach - Using
beforeAll/afterAll - The AAA (Arrange-Act-Assert) pattern
- Test naming conventions and best practices
Grouping Tests with describe
In Day 1, we wrote tests as flat test() calls. As tests grow, organization becomes essential. describe groups related tests together.
// calculator.test.js
const { add, subtract, multiply, divide } = require('./math');
describe('add', () => {
test('adds two 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 two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
test('throws error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
TypeScript version:
// calculator.test.ts
import { add, subtract, multiply, divide } from './math';
describe('add', () => {
test('adds two 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 two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
test('throws error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
Output:
PASS ./calculator.test.js
add
β adds two positive numbers (1 ms)
β adds negative numbers
β adds zero
divide
β divides two numbers
β throws error when dividing by zero (1 ms)
With describe, output is indented and grouped for readability.
Nesting describe Blocks
describe blocks can be nested for finer organization.
describe('Calculator', () => {
describe('basic operations', () => {
test('adds numbers', () => {
expect(add(1, 2)).toBe(3);
});
test('subtracts numbers', () => {
expect(subtract(5, 3)).toBe(2);
});
});
describe('edge cases', () => {
test('handles negative results', () => {
expect(subtract(3, 5)).toBe(-2);
});
test('handles decimal results', () => {
expect(divide(10, 3)).toBeCloseTo(3.333);
});
});
});
flowchart TB
subgraph Structure["Test Structure"]
D1["describe('Calculator')"]
D2["describe('basic operations')"]
D3["describe('edge cases')"]
T1["test('adds numbers')"]
T2["test('subtracts numbers')"]
T3["test('handles negative results')"]
T4["test('handles decimal results')"]
end
D1 --> D2
D1 --> D3
D2 --> T1
D2 --> T2
D3 --> T3
D3 --> T4
style D1 fill:#3b82f6,color:#fff
style D2 fill:#8b5cf6,color:#fff
style D3 fill:#8b5cf6,color:#fff
style T1 fill:#22c55e,color:#fff
style T2 fill:#22c55e,color:#fff
style T3 fill:#22c55e,color:#fff
style T4 fill:#22c55e,color:#fff
test vs. it
In Jest, test() and it() are functionally identical. You can use either.
// Using test()
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
// Using it()
it('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
it reads more naturally as an English sentence.
describe('add function', () => {
it('returns the sum of two numbers', () => {
expect(add(1, 2)).toBe(3);
});
it('handles negative numbers', () => {
expect(add(-1, -2)).toBe(-3);
});
});
The output reads as "add function > returns the sum of two numbers"βa natural sentence.
| Function | Reading Style | Best Used When |
|---|---|---|
test() |
"test that X does Y" | Standalone test cases |
it() |
"it does Y" | Combined with describe |
This book primarily uses test(), but either is fine as long as your project is consistent.
The AAA Pattern (Arrange-Act-Assert)
Good tests follow a consistent structure. The most widely adopted is the AAA pattern.
flowchart LR
subgraph AAA["AAA Pattern"]
A["Arrange\nSet up"]
B["Act\nExecute"]
C["Assert\nVerify"]
end
A --> B --> C
style A fill:#3b82f6,color:#fff
style B fill:#f59e0b,color:#fff
style C fill:#22c55e,color:#fff
| Phase | Description | Example |
|---|---|---|
| Arrange | Prepare data and objects needed for the test | Set up input values, initialize objects |
| Act | Execute the code under test | Call the function |
| Assert | Verify the result matches expectations | expect().toBe() |
Practical Example
// user.js
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hi, I'm ${this.name}!`;
}
isAdult() {
return this.age >= 18;
}
}
module.exports = User;
TypeScript version:
// user.ts
export class User {
constructor(
public name: string,
public age: number
) {}
greet(): string {
return `Hi, I'm ${this.name}!`;
}
isAdult(): boolean {
return this.age >= 18;
}
}
// user.test.js
const User = require('./user');
describe('User', () => {
test('greet returns a greeting message with the name', () => {
// Arrange
const user = new User('Alice', 25);
// Act
const result = user.greet();
// Assert
expect(result).toBe("Hi, I'm Alice!");
});
test('isAdult returns true for users aged 18 or older', () => {
// Arrange
const user = new User('Bob', 18);
// Act
const result = user.isAdult();
// Assert
expect(result).toBe(true);
});
test('isAdult returns false for users under 18', () => {
// Arrange
const user = new User('Charlie', 17);
// Act
const result = user.isAdult();
// Assert
expect(result).toBe(false);
});
});
Tip: For simple tests, each AAA phase may be just one line. In that case, you can skip the comments and write it as a single line:
expect(add(1, 2)).toBe(3);
beforeEach and afterEach
When multiple tests share the same setup, beforeEach consolidates the common preparation.
describe('User', () => {
let user;
beforeEach(() => {
user = new User('Alice', 25);
});
test('greet returns a greeting message', () => {
expect(user.greet()).toBe("Hi, I'm Alice!");
});
test('isAdult returns true', () => {
expect(user.isAdult()).toBe(true);
});
});
flowchart TB
subgraph Lifecycle["Test Lifecycle"]
BE["beforeEach()\nRuns before each test"]
T1["test 1"]
AE1["afterEach()\nRuns after each test"]
BE2["beforeEach()"]
T2["test 2"]
AE2["afterEach()"]
end
BE --> T1 --> AE1 --> BE2 --> T2 --> AE2
style BE fill:#3b82f6,color:#fff
style BE2 fill:#3b82f6,color:#fff
style T1 fill:#22c55e,color:#fff
style T2 fill:#22c55e,color:#fff
style AE1 fill:#f59e0b,color:#fff
style AE2 fill:#f59e0b,color:#fff
Cleanup with afterEach
Use afterEach to release resources after each test.
describe('Database connection', () => {
let db;
beforeEach(() => {
db = new Database();
db.connect();
});
afterEach(() => {
db.disconnect();
});
test('saves a record', () => {
db.save({ name: 'Alice' });
expect(db.count()).toBe(1);
});
test('starts with empty database', () => {
expect(db.count()).toBe(0);
});
});
beforeAll and afterAll
beforeAll / afterAll run once per describe block. Use them for expensive initialization.
describe('API tests', () => {
let server;
beforeAll(() => {
server = startTestServer();
});
afterAll(() => {
server.close();
});
test('responds to GET /users', () => {
// ...
});
test('responds to POST /users', () => {
// ...
});
});
| Hook | When It Runs | Use Case |
|---|---|---|
beforeEach |
Before each test | Create test data |
afterEach |
After each test | Clean up data |
beforeAll |
Once before all tests in the block | Start server, connect to DB |
afterAll |
Once after all tests in the block | Stop server, disconnect DB |
Hook Execution Order with Nested describe
describe('outer', () => {
beforeEach(() => console.log('outer beforeEach'));
afterEach(() => console.log('outer afterEach'));
describe('inner', () => {
beforeEach(() => console.log('inner beforeEach'));
afterEach(() => console.log('inner afterEach'));
test('example', () => {
console.log('test');
});
});
});
Execution order:
outer beforeEach
inner beforeEach
test
inner afterEach
outer afterEach
Outer beforeEach runs first; afterEach runs inner-first.
Test Naming Conventions
Test names should make it immediately clear what's being tested.
Good Naming Patterns
// Pattern 1: Describe the behavior
test('returns the sum of two positive numbers', () => { ... });
test('throws an error when the input is empty', () => { ... });
// Pattern 2: State the condition and result
test('isAdult returns true when age is 18', () => { ... });
test('isAdult returns false when age is 17', () => { ... });
// Pattern 3: describe + it reads as a sentence
describe('User.isAdult', () => {
it('returns true for age 18 or older', () => { ... });
it('returns false for age under 18', () => { ... });
});
Names to Avoid
// β No clue what's being tested
test('test1', () => { ... });
test('it works', () => { ... });
// β Tied to implementation details
test('calls Math.max internally', () => { ... });
| Rule | Good | Bad |
|---|---|---|
| Describe behavior | returns null for invalid input |
test case 3 |
| Include conditions | throws error when array is empty |
error test |
| Keep it concise | formats date as YYYY-MM-DD |
should correctly format the given date object into YYYY-MM-DD string format |
Parameterized Tests with test.each
When testing the same logic with different inputs, test.each reduces duplication.
const { add } = require('./math');
describe('add', () => {
test.each([
[1, 2, 3],
[0, 0, 0],
[-1, 1, 0],
[100, 200, 300],
])('add(%i, %i) = %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
});
TypeScript version:
import { add } from './math';
describe('add', () => {
test.each([
[1, 2, 3],
[0, 0, 0],
[-1, 1, 0],
[100, 200, 300],
])('add(%i, %i) = %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
});
Output:
PASS ./math.test.js
add
β add(1, 2) = 3
β add(0, 0) = 0
β add(-1, 1) = 0
β add(100, 200) = 300
Object-Style test.each
For better readability, you can use an array of objects.
describe('isAdult', () => {
test.each([
{ age: 18, expected: true, label: 'exactly 18' },
{ age: 25, expected: true, label: 'over 18' },
{ age: 17, expected: false, label: 'under 18' },
{ age: 0, expected: false, label: 'zero' },
])('returns $expected when age is $age ($label)', ({ age, expected }) => {
const user = new User('Test', age);
expect(user.isAdult()).toBe(expected);
});
});
Skipping and Focusing Tests
During development, you may want to run only specific tests or temporarily skip some.
// Run only this test (others are ignored)
test.only('this test runs', () => {
expect(1 + 1).toBe(2);
});
// Skip this test
test.skip('this test is skipped', () => {
expect(1 + 1).toBe(2);
});
// Works with describe too
describe.only('only this group runs', () => {
test('test 1', () => { ... });
});
describe.skip('this group is skipped', () => {
test('test 2', () => { ... });
});
| Modifier | Effect | When to Use |
|---|---|---|
.only |
Runs only that test/group | Debugging, focusing on a specific test |
.skip |
Skips that test/group | Temporarily disabling, unimplemented tests |
Warning: Don't commit
.onlyor.skipto your repository. Use the ESLintno-only-testsrule to prevent accidentally committing.only.
Summary
| Concept | Description |
|---|---|
describe |
Groups tests together. Can be nested |
test / it |
Defines a test case (identical functionality) |
| AAA Pattern | Arrange β Act β Assert for structured tests |
beforeEach / afterEach |
Runs before/after each test |
beforeAll / afterAll |
Runs once per describe block |
test.each |
Parameterized tests to reduce duplication |
.only / .skip |
Focus on or skip specific tests |
Key Takeaways
- Use
describeto organize tests into logical groups for readability - The AAA pattern makes test intent clear and consistent
beforeEachis convenient but overuse can reduce test readability- Test names should clearly communicate what's being tested
Exercises
Exercise 1: Basics
Write tests for the following ShoppingCart class, grouped with describe.
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(name, price) {
this.items.push({ name, price });
}
getTotal() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
getItemCount() {
return this.items.length;
}
clear() {
this.items = [];
}
}
Exercise 2: Intermediate
Rewrite the ShoppingCart tests above using beforeEach to initialize the cart before each test.
Challenge
Use test.each to test the following fizzbuzz function with multiple inputs.
function fizzbuzz(n) {
if (n % 15 === 0) return 'FizzBuzz';
if (n % 3 === 0) return 'Fizz';
if (n % 5 === 0) return 'Buzz';
return String(n);
}
References
Next up: In Day 3, we'll "Master Matchers." Beyond toBe, Jest offers a rich set of matchers like toEqual, toContain, toThrow, and more. You'll learn which matcher to use for every situation!