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.
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.
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).
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.
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.
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
The smallest testable piece β typically one method or class β tested in isolation from its collaborators.
The three-phase structure of a readable test: set up, do one thing, verify the outcome.
Fast, Isolated, Repeatable, Self-validating, Timely β the properties of good unit tests.
A stand-in for a real dependency: stub (returns data), mock (verifies calls), fake (working lite impl), spy.
Many fast unit tests, fewer integration tests, very few slow E2E tests β balanced feedback and confidence.
Assert observable outcomes, not internal details, so refactoring doesn't break correct tests.
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 test5 const price = 100, rate = 0.2;67 // Act β invoke exactly ONE thing8 const result = applyDiscount(price, rate);910 // Assert β verify the observable outcome11 expect(result).toBe(80);12 });1314 // 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});
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
1The Edge Case That Slipped to Production
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.
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.
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
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.
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.
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.