Learn Jest in 10 DaysDay 2: Test Structure and Basic Patterns
Chapter 2Learn Jest in 10 Days

Day 2: Test Structure and Basic Patterns

What You'll Learn Today

  • Grouping tests with describe
  • The difference between test and it
  • 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 .only or .skip to your repository. Use the ESLint no-only-tests rule 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

  1. Use describe to organize tests into logical groups for readability
  2. The AAA pattern makes test intent clear and consistent
  3. beforeEach is convenient but overuse can reduce test readability
  4. 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!