HT
How Things Work
System Online

Unit Testing Fundamentals

Verifying one piece of behavior in isolation — the Arrange-Act-Assert structure, test doubles, the FIRST properties, and the test pyramid that balances speed with confidence.

How It Works

Unit tests verify small, isolated pieces of behavior quickly and deterministically. Structured with Arrange-Act-Assert and kept FIRST (fast, isolated, repeatable, self-validating, timely), they form the broad base of the test pyramid — giving developers instant feedback, documenting intent, and catching regressions the moment they're introduced.

1
What a Unit Test Is

A unit test verifies a single small piece of behavior — usually one method or class — in isolation from the rest of the system. Good unit tests are FIRST: Fast (milliseconds), Isolated (no shared state), Repeatable (deterministic), Self-validating (clear pass/fail), and Timely (written close to the code).

2
Arrange–Act–Assert

The AAA pattern structures every test into three clear phases: Arrange the inputs and the object under test, Act by calling exactly one thing, and Assert the expected outcome. One logical assertion per test keeps failures unambiguous — you know immediately what broke.

3
Isolation & Test Doubles

A unit test should exercise the unit, not its dependencies. Replace collaborators (databases, HTTP, clock) with test doubles — stubs (canned return values), mocks (verify interactions), and fakes (lightweight working implementations) — so the test is fast, deterministic, and focused on the unit's own logic.

4
The Test Pyramid

Unit tests form the broad base of the test pyramid: many fast, cheap unit tests; fewer integration tests; and a small number of slow, end-to-end tests at the top. This distribution gives fast feedback and pinpoints failures, while still verifying that components work together.

Key Concepts

🧪Unit

The smallest testable piece — typically one method or class — tested in isolation from its collaborators.

📐Arrange–Act–Assert

The three-phase structure of a readable test: set up, do one thing, verify the outcome.

FIRST

Fast, Isolated, Repeatable, Self-validating, Timely — the properties of good unit tests.

🎭Test Double

A stand-in for a real dependency: stub (returns data), mock (verifies calls), fake (working lite impl), spy.

🔺Test Pyramid

Many fast unit tests, fewer integration tests, very few slow E2E tests — balanced feedback and confidence.

🎯Behavior, Not Implementation

Assert observable outcomes, not internal details, so refactoring doesn't break correct tests.

Arrange–Act–Assert test
tsx
1// A focused unit test: Arrange–Act–Assert, one behavior per test.
2describe("applyDiscount", () => {
3 it("subtracts the percentage of the price", () => {
4 // Arrange — set up inputs and the system under test
5 const price = 100, rate = 0.2;
6
7 // Act — invoke exactly ONE thing
8 const result = applyDiscount(price, rate);
9
10 // Assert — verify the observable outcome
11 expect(result).toBe(80);
12 });
13
14 // Name tests by behavior, and cover edge cases as separate tests:
15 it("throws on a negative rate", () => {
16 expect(() => applyDiscount(100, -0.1)).toThrow();
17 });
18});
💡
Why This Matters

Unit tests are the foundation of a maintainable codebase and a confident refactoring culture, and they're a baseline expectation in interviews and professional teams. Knowing how to write isolated, behavior-focused tests — and how they fit the test pyramid — is what makes fast, safe iteration possible.

Common Pitfalls

Testing implementation details instead of behavior — brittle tests that break on every refactor.
Over-mocking until tests just verify the mocks, not real logic (tests that can't fail meaningfully).
Multiple unrelated assertions per test, so a failure doesn't tell you what actually broke.
Skipping edge cases (boundaries, nulls, errors) and only testing the happy path.
Slow or order-dependent 'unit' tests that touch real I/O, shared state, or the clock — they're not really unit tests.
Real-World Use Cases

1The Edge Case That Slipped to Production

Scenario

A discount calculation worked for normal inputs but silently misbehaved for a 100% discount and for negative quantities, causing wrong totals that customers noticed before the team did.

Problem

Manual testing only exercised the happy path. The edge cases (boundaries, zero, negatives, nulls) were never checked, and there was no automated safety net to catch them or prevent regressions.

Solution

Write focused unit tests for the calculation: the normal case, the boundaries (0% and 100%), and invalid inputs (negative rate/quantity throwing). Each is a tiny AAA test. They run in milliseconds on every commit, documenting the rules and catching regressions instantly.

💡

Takeaway: Unit tests shine on logic with branches and edge cases. Cover the boundaries and invalid inputs explicitly — that's where bugs hide, and where a fast red test pays for itself many times over.

2Tests That Broke on Every Refactor

Scenario

A team had unit tests, but every internal refactor — even one that didn't change behavior — turned dozens of tests red, so developers started avoiding refactoring and eventually deleting tests.

Problem

The tests asserted implementation details (which private methods were called, internal field values) instead of observable behavior. They were coupled to how the code worked, not what it did, making them brittle.

Solution

Rewrite tests to assert outcomes through the public API — given these inputs, expect this result or this thrown error — and replace interaction-heavy mocking with checking the actual output. Now refactors that preserve behavior keep the tests green.

💡

Takeaway: Test behavior, not implementation. Tests coupled to internals punish refactoring; tests that assert observable outcomes give you the freedom to improve the code safely.

Join the discussion

Comments

?

Loading comments...

Cookie preferences

We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.