100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Software Testing

Writing Good Unit Tests

Learn what separates a fast, trustworthy unit test from a brittle one, and the core principles (FIRST, AAA) that guide good test design.

Unit TestingBeginner9 min readJul 10, 2026
Analogies

What Makes a Unit Test 'Good'?

A unit test exercises the smallest testable piece of behavior in your code — usually a single function or method — in isolation from databases, networks, file systems, and other collaborators. A good unit test runs in milliseconds, produces the same result every time it runs, and fails only when the behavior it targets actually breaks. When tests are slow, flaky, or coupled to unrelated code, developers stop trusting them and start ignoring red builds, which defeats the entire purpose of having tests.

🏏

Cricket analogy: Just as a net session isolates a batsman facing throwdowns from a single bowling machine setting rather than a full live match with fielders and weather, a unit test isolates one function from the database, network, and other moving parts so only that one behavior is under examination.

The FIRST Principles

The FIRST acronym summarizes five properties every unit test should have: Fast (runs in milliseconds so a full suite completes in seconds), Isolated (no shared state or ordering dependency between tests), Repeatable (same result on any machine, any number of times), Self-validating (the test itself reports pass or fail — no manual log inspection), and Timely (written close to when the production code is written, ideally before it in TDD). Violating any one of these properties erodes the value of the test suite: a slow suite gets skipped, a flaky suite gets ignored, and a test that requires a human to eyeball output stops being automated verification at all.

🏏

Cricket analogy: A DRS review needs to be fast, give a repeatable decision from the same ball-tracking data, and self-validate with an out/not-out call rather than leaving the umpire to guess, exactly like FIRST demands from a unit test.

javascript
// Arrange-Act-Assert (AAA) structure with Jest
describe('calculateDiscount', () => {
  it('applies a 10% discount for orders over $100', () => {
    // Arrange
    const order = { subtotal: 150 };

    // Act
    const discounted = calculateDiscount(order);

    // Assert
    expect(discounted).toBe(135);
  });

  it('applies no discount for orders at or under $100', () => {
    const order = { subtotal: 80 };
    const discounted = calculateDiscount(order);
    expect(discounted).toBe(80);
  });
});

Arrange-Act-Assert and One Behavior Per Test

The Arrange-Act-Assert (AAA) pattern gives every test a predictable shape: set up the inputs and collaborators (Arrange), invoke the code under test exactly once (Act), then verify the outcome (Assert). Keeping these three phases visually separate — often with a blank line or comment between them — makes tests readable at a glance, even for someone unfamiliar with the code. A closely related discipline is testing one logical behavior per test case: a test named applies_10_percent_discount_over_100 should assert only facts related to that discount rule, not also check tax calculation or shipping cost, because a failure should point unambiguously at the broken behavior.

🏏

Cricket analogy: A bowling analysis breaks down into run-up (Arrange), delivery stride (Act), and the ball's outcome (Assert) as separate, reviewable phases, and good coaching video isolates one delivery type, like an off-cutter, rather than mixing yorkers and bouncers in the same clip.

Name tests as sentences describing behavior, such as returns_empty_list_when_no_matches_found, rather than vague names like test1 or testDiscount. A descriptive name doubles as living documentation and tells you what broke without opening the test body.

Avoid asserting on private implementation details (internal variable names, private method call counts) instead of observable behavior. Implementation-coupled tests break every time you refactor even when the public behavior is unchanged, which trains developers to distrust or delete tests rather than fix real bugs.

  • A good unit test isolates one unit of behavior from databases, networks, and other collaborators.
  • FIRST stands for Fast, Isolated, Repeatable, Self-validating, and Timely.
  • Arrange-Act-Assert gives every test a predictable, readable three-phase structure.
  • Test one logical behavior per test case so a failure points to exactly one cause.
  • Descriptive test names act as documentation and make failures self-explanatory.
  • Assert on observable behavior, not private implementation details, so refactors don't break tests unnecessarily.
  • Slow or flaky tests get skipped or ignored, which silently destroys the value of the whole suite.

Practice what you learned

Was this page helpful?

Topics covered

#SoftwareTesting#TestingTDDStudyNotes#SoftwareEngineering#WritingGoodUnitTests#Writing#Good#Unit#Tests#Testing#StudyNotes#SkillVeris