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.

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.
There are two core functions for structuring tests: test (or its alias it) for declaring a single test, and describe for grouping related tests:
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.
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.
The toBe matcher uses Object.is to compare primitive values, while toEqual compares structure recursively. This difference matters:
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.
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.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.
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.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.
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:
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.
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:
toBe for primitives, toEqual for object structure.toContain for arrays and strings; understand falsy and truthy.beforeEach and afterEach keep each test isolated.toBeCloseTo, not toBe.toThrow.In the next episode, episode 5, we'll dive into mocking and spying — jest.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.