Testing & Code Quality

What Makes a Good Unit Test: A Complete Guide to the 5 FIRST Principles

Have you ever written tons of test code, only to miss the actual bugs—and watch tests break everywhere whenever you fix a feature?

4 min read
Cover image for What Makes a Good Unit Test: A Complete Guide to the 5 FIRST Principles

Have you ever written tons of test code, only to miss the actual bugs—and watch tests break everywhere whenever you fix a feature?

In short, good unit tests are judged not by their number or coverage, but by whether they follow the FIRST principles. FIRST stands for Fast, Isolated, Repeatable, Self-validating, and Timely.

Remember these five points, and tests become a reliable safety net instead of a burden. Today, I’ll walk through them one by one, with examples from my experience.

The FIRST Principles at a Glance

FIRST is a set of principles popularized by Robert Martin (Uncle Bob) in his book Clean Code.

Each letter represents one quality of a good unit test.

Letter Meaning Key point
F Fast Hundreds finish within seconds
I Isolated Tests do not interfere with one another
R Repeatable The same result anywhere, anytime
S Self-validating Pass or fail is determined automatically
T Timely Written at the right time

Let’s break them down one by one.


Fast: Why Do Tests Need to Be This Fast?

Unit tests need to be fast. Really.

If they are slow, developers stop running them. A test suite that takes three minutes per run eventually gets run only before commits, if at all.

Tests do their job when you can run hundreds of them without friction every time you change code.

The most common causes of slowness are access to a real DB, the network, and the file system. The standard approach is to replace these external dependencies with test doubles (Mock, Stub).

A good target is a few milliseconds per test, with hundreds of tests completing within seconds overall.

That relief when every test is green—you know it, right?
That relief when every test is green—you know it, right?

Isolated: Why Tests Must Not Interfere with One Another

Each test should be independent. It must not depend on another test’s result.

For example, if test A creates data that test B uses, B collapses along with A when A fails. The result also changes when the execution order changes.

That is why each test should prepare the data it needs and clean it up properly when it finishes.

Here is a common pattern that resets the state before every test.

struct CalculatorTests {
    let calculator: Calculator

    init() {
        // A new instance is created for every test → prevents shared state suite 
        calculator = Calculator()
    }
}

This keeps tests from affecting one another, regardless of which one runs first.

A Swift Testing test suite written with @Test and init() in Xcode, with the passing results screen
Thanks to init(), every test starts with a new instance and remains independent

Repeatable & Self-validating: Results Must Not Fluctuate

Let’s start with R, repeatability. A test should produce the same result no matter how many times or in what environment it runs.

A test that passes today but fails tomorrow is called a flaky test. It is a major source of lost trust.

Depending on the current time or random values makes this problem more likely. If you need to handle time, safely control it by injecting a fixed value.

S, self-validation, is just as important. A test should determine for itself whether it passes or fails.

Printing a value to the console and checking it by eye is not testing. Use assertions such as #expect to determine the result automatically.

A good test shows only one of two states: green or red. If it makes a person wonder, “Is this right?”, the test has already failed.


Timely: When Should You Write Tests?

The final T stands for timeliness. It means writing tests at the appropriate time.

In TDD (Test-Driven Development), tests are written before production code. Even if you do not follow that exact approach, the key is to write tests close to the time you implement the feature.

If you wait several weeks after finishing a feature and then try to write all the tests at once, the code has often hardened into a structure that is difficult to test.

The essence of Timely is not putting tests off until later.


Wrapping Up

FIRST is less a set of rules to memorize than a checklist that helps identify what is wrong when tests start feeling frustrating.

Is it slow? Are tests tangled together? Do results fluctuate? Does it require manual checking? Are you writing it too late? Checking these five points alone can dramatically improve test quality.

Starting today, bring FIRST to mind whenever you write a test. It will definitely become a dependable teammate.

Continue reading