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.
// 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
1. Which of the following best describes the 'Isolated' property in the FIRST principles?
2. In the Arrange-Act-Assert pattern, what happens during the 'Act' phase?
3. Why is it discouraged to assert on private implementation details rather than observable behavior?
4. A test suite that takes 45 minutes to run and is often skipped by developers before committing violates which FIRST property most directly?
5. What is the main benefit of testing exactly one logical behavior per test case?
Was this page helpful?
You May Also Like
Test Doubles: Mocks, Stubs, and Fakes
Understand the different kinds of test doubles — dummies, stubs, spies, mocks, and fakes — and when to reach for each one.
Assertion Styles and Best Practices
Compare classic, fluent, and matcher-based assertion styles and learn practical rules for writing assertions that fail with useful messages.
Test Coverage Explained
Understand how line, branch, and path coverage are measured, why 100% coverage doesn't guarantee correctness, and how to use coverage reports wisely.
Related Reading
Related Study Notes in Software Engineering
Browse all study notesMicroservices Study Notes
Software Architecture · 30 topics
Software EngineeringDesign Patterns Study Notes
Software Design · 30 topics
Software EngineeringSoftware Engineering Study Notes
Python · 40 topics
Software EngineeringGit & Version Control Study Notes
Bash · 40 topics
Software EngineeringSystem Design Study Notes
Architecture · 40 topics