Most developers agree that testing matters. Far fewer stop to ask whether their tests are actually any good. A test suite that is slow, brittle, or tangled together can quietly do more harm than no tests at all. That is why the FIRST principles have stuck around as a popular checklist for unit testing.
FIRST is an acronym covering five traits of solid unit tests: Fast, Independent, Repeatable, Self-validating, and Thorough. Used well, it forces you to write tests that are easier to maintain and far less likely to break for the wrong reasons.
The five principles
Fast.** Tests should run in seconds, not minutes. If a suite takes ten minutes, developers stop running it. Speed usually breaks down in three ways: calling live APIs, building a dependency tree that is too wide, or testing a method that does too many things. Mock external dependencies for the first problem; refactor for the other two.
Independent.** Test methods should be able to run in any order, on their own, and still pass. The classic smell is one test setting up state that another test later depends on. Build a fresh instance of your object inside each test, or use a setup hook that runs before each test. Reset your mocks the same way. Anything that leaks across tests is a bug waiting to bite during a CI run.
Repeatable.** A test that depends on the current date or the contents of a local folder will pass today and fail next week. Inject fixed values, mock the clock, and keep file system interactions behind an interface you can stub. Instead of calling `DateTime.UtcNow`, create a fixed `DateTime` instance. That tiny change turns a flaky test into a stable one.
Self-validating.** A test should report its own result. If someone has to read a log file or eyeball a database to decide pass or fail, the test is broken. Modern frameworks make this easy with plain assertions.
Thorough.** Tests must cover happy paths and failure paths. For a single method that filters items by category, you need cases for null inputs, empty strings, negative counts, exceptions from dependencies, and unexpected nulls inside collections. Boring? Maybe. But that coverage is what catches real bugs.
Common violations and their fixes
| Violation | Fix |
|---|---|
| Test hits a real API | Mock the HTTP client or repository |
| Test A sets up state for Test B | Create fresh instances per test |
| Test uses `DateTime.UtcNow` | Inject a fixed datetime or clock abstraction |
| Test writes to disk and manually checks output | Assert programmatically |
| Test covers only the happy path | Add cases for null, empty, negative, and exceptions |
Why it matters
FIRST is not a silver bullet. It is a sanity check, and the principles sometimes pull in different directions. In a small, short-lived script, chasing every principle can feel like overengineering. The payoff scales with codebase longevity and team size.
The biggest wins are independence and speed. Tests that share hidden state cause more production incidents than untested code. A slow suite trains the team to skip it, which defeats the entire purpose of testing.
Where FIRST shines most is in codebases that will live for years and be touched by many hands. New team members can learn five words instead of decoding a tangled test suite. The cognitive load drops, and refactoring becomes safer.
When principles conflict
Sometimes speed and thoroughness trade off. You might want to test with real database data for accuracy, but that slows the suite. In that case, reserve real data for integration or end-to-end tests. Keep unit tests fast and mocked.
Independence can also conflict with thoroughness when you need to set up complex shared state. The answer is not to share state across tests but to make setup itself cheap. Extract factory methods or use test data builders to keep each test self-contained without duplication.
Bottom line
Write tests like production code: small, focused, and respectful of the next reader. FIRST gives you five words to remember when you forget that. Stick to them and your test suite will outlive your features.
Key takeaways
- Mock external dependencies rather than hitting real APIs or databases.
- Build a fresh instance inside each test; nothing should leak between tests.
- Replace `DateTime.UtcNow` with a fixed value or clock abstraction.
- Cover edge cases, null inputs, and failure paths, not just the happy route.
- If a principle conflicts with speed, push the slow variant to integration tests.
Frequently asked questions
What does FIRST stand for in unit testing?
Fast, Independent, Repeatable, Self-validating, and Thorough. They are five properties every unit test should aim for.
Why are independent tests important?
Independent tests can run in any order and still pass. Shared state between tests is a common source of flaky CI builds and unexpected failures.
How do I make date-dependent tests repeatable?
Inject a fixed DateTime value or use a clock abstraction you can mock. Avoid calling DateTime.UtcNow directly inside the test logic.
Is thoroughness the same as high code coverage?
Not really. Coverage measures lines executed. Thoroughness means you have covered edge cases, null inputs, and failure paths, not just the happy route.