Learn Jest - Writing Unit Tests
Series/Learn Jest/Episode 4
Episode 4 of 23

Learn Jest - Writing Unit Tests

This core episode teaches unit test structure: declaring with test and describe, basic matchers such as toBe, toEqual, toContain, and toBeTruthy, the setup teardown lifecycle with beforeEach and friends, and how to test pure functions and edge cases.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

This is the episode you'll practice most often in your testing career: writing unit tests. Episode 4 teaches you how to structure clear tests, use matchers correctly, manage the setup and teardown lifecycle, and test pure functions all the way to their edge cases.

A good unit test isn't just "code that passes." It's a living document: reading its name should explain the behavior it guarantees, and its failure should point directly to the problematic part of the code. Let's build that habit starting now.

Declaring Tests with test and describe

Basic Structure

There are two core functions for structuring tests: test (or its alias it) for declaring a single test, and describe for grouping related tests:

JStest and describe
describe("add function", () => {
  test("adds two positive numbers", () => {
    expect(1 + 2).toBe(3);
  });
 
  test("adds zero", () => {
    expect(5 + 0).toBe(5);
  });
});

describe("add function", () => {}) creates a grouping block, and test("...", () => {}) declares a single test with a descriptive name. The test name is a behavior contract — write it as if someone else will read it.

Descriptive Naming Patterns

Avoid names like "test one" or "test two." Use a pattern that describes the condition and the result: "adds two positive numbers", "throws an error when the argument is empty". Good names make --verbose output feel like a spec list.

Basic Matchers

toBe and toEqual

The toBe matcher uses Object.is to compare primitive values, while toEqual compares structure recursively. This difference matters:

JSDifference between toBe and toEqual
test("toBe for primitives", () => {
  expect(2 + 2).toBe(4);
  expect("hello").toBe("hello");
});
 
test("toEqual for objects", () => {
  const data = { name: "arif", age: 30 };
  expect(data).toEqual({ name: "arif", age: 30 });
});
 
test("toBe fails for objects", () => {
  expect({ a: 1 }).not.toBe({ a: 1 });
});

expect(data).toEqual({ name: "arif", age: 30 }) compares the contents of the object, not its reference. Two object instances with identical contents are toEqual but not toBe, because toBe compares memory references.

toContain and toBeTruthy

Other frequently used matchers:

  • toContain: checks for an element in an array or a substring in a string.
  • toBeTruthy and toBeFalsy: check whether a value is considered true or false in a boolean context.
  • toBeNull, toBeDefined, toBeGreaterThan, toHaveLength: specific matchers as needed.
JSOther basic matchers
test("toContain on arrays and strings", () => {
  expect(["apple", "mango"]).toContain("mango");
  expect("welcome").toContain("come");
});
 
test("toBeTruthy and toBeFalsy", () => {
  expect("text").toBeTruthy();
  expect(0).toBeFalsy();
  expect(null).toBeFalsy();
});

Note expect(0).toBeFalsy() — zero is considered falsy, as are empty strings and null. Understanding this boolean conversion prevents a lot of confusion when writing assertions.

Setup and Teardown

The Four Lifecycle Functions

Jest provides four functions for managing the test environment:

  • beforeAll: runs once before all tests in a block.
  • beforeEach: runs before every test.
  • afterEach: runs after every test.
  • afterAll: runs once after all tests finish.
JSSetup teardown lifecycle
describe("database tests", () => {
  let list;
 
  beforeEach(() => {
    list = [];
  });
 
  afterEach(() => {
    list = null;
  });
 
  test("adds one item", () => {
    list.push("item");
    expect(list).toHaveLength(1);
  });
 
  test("starts empty", () => {
    expect(list).toHaveLength(0);
  });
});

Because beforeEach(() => { list = []; }) re-runs the initialization before each test, the execution order between tests doesn't affect each other — the primary foundation for deterministic tests.

Testing Pure Functions and Edge Cases

Pure Functions Are Easiest to Test

A pure function — one whose output depends only on its input and has no side effects — is an ideal candidate for unit tests. Every interesting input combination can be one test:

JSPure function and edge cases
function divide(a, b) {
  if (b === 0) {
    throw new Error("divisor must not be zero");
  }
  return a / b;
}
 
describe("divide function", () => {
  test("normal division", () => {
    expect(divide(10, 2)).toBe(5);
  });
 
  test("fractional result", () => {
    expect(divide(1, 4)).toBeCloseTo(0.25);
  });
 
  test("zero divisor throws error", () => {
    expect(() => divide(1, 0)).toThrow("divisor must not be zero");
  });
});

Three things are interesting here. First, expect(divide(1, 4)).toBeCloseTo(0.25) is used because JavaScript's float arithmetic isn't precise enough for a toBe comparison. Second, to test errors, expect(() => divide(1, 0)).toThrow(...) wraps the call in a function — without that, the error would be caught and the test would wrongly pass. Third, the zero-divisor edge case is actually the most important behavior to test.

Wrap Up

Episode 4 is the backbone of the entire series: you can now structure tests with test and describe, choose the right matcher between toBe, toEqual, toContain, and toBeTruthy, manage the test environment with the setup teardown lifecycle, and test pure functions along with their edge cases.

Key takeaways:

  • A test name is a behavior contract — write descriptive ones.
  • toBe for primitives, toEqual for object structure.
  • toContain for arrays and strings; understand falsy and truthy.
  • beforeEach and afterEach keep each test isolated.
  • Floating point is tested with toBeCloseTo, not toBe.
  • Errors are tested by wrapping the call in a function and using toThrow.

In the next episode, episode 5, we'll dive into mocking and spyingjest.fn() and jest.spyOn(), the difference between manual mocks and automatic mocks, mocking modules with jest.mock(), and how to control implementations and reset behavior. This is the key to testing code that depends on external systems.

Learn Jest - Writing Unit Tests | Learn Jest