Naming and Structuring Tests
Test names that say what broke without opening the file, the naming conventions worth adopting, and how to organise a suite so people can find things.
2 min read · updated 19 September 2026
Here is the only test-naming requirement that matters: when the build goes red, the name in the CI output should tell you what broke.
✗ PricingTests.Test3
✗ should work
✗ test_discount
✓ applies no discount below the ten pound threshold
✓ Withdraw_WhenBalanceIsInsufficient_LeavesTheBalanceUnchanged
✓ test_expired_token_is_rejected_even_if_the_signature_is_validThe first three require you to open a file. The second three do not. Over a year of builds, that difference is measured in days.
#What a good name contains
Three things, in whatever grammar your ecosystem prefers:
- The subject — what is being exercised
- The condition — under what circumstances
- The expected outcome — what should be true
// C#: the Given_When_Then convention in identifier form
public void Withdraw_WhenBalanceIsInsufficient_LeavesTheBalanceUnchanged()// TypeScript: describe supplies the subject, it supplies condition + outcome
describe('Account.withdraw', () => {
it('leaves the balance unchanged when there are insufficient funds', () => {});
it('applies the overdraft limit before refusing', () => {});
});# Python: one long snake_case sentence, which reads well in pytest output
def test_withdrawing_more_than_the_balance_leaves_the_balance_unchanged():// Java: @DisplayName lets the identifier stay short and the report stay readable
@Test
@DisplayName("leaves the balance unchanged when there are insufficient funds")
void insufficientFundsLeavesBalance() {}#Names to stop writing
testX, test1, shouldWork. No information. The word "test" is
redundant — the framework already knows.
Names that restate the implementation. calls_repository_save_once is
coupled to a collaboration rather than a behaviour, and it will be wrong
after the next refactor. See
unit testing with mocks.
Names with "and". rejects_overdraft_and_logs_a_warning is two tests.
The clue is in the conjunction.
Names that describe the setup rather than the outcome.
user_with_no_subscription tells you what was arranged and nothing about
what should happen.
#The "should" argument
Some people object to should on the grounds that a test asserts rather
than suggests. It is a real point and a small one. What matters far more is
that the name contains a condition and an outcome; should or not is a
house-style decision worth settling once in five minutes and never
revisiting.
#Structuring the suite
Unit tests: mirror the production tree. src/pricing/discount.ts →
src/pricing/discount.test.ts, colocated or in a parallel tests/
directory. Findability is the entire criterion: when you change a file, you
should know without searching where its tests are.
Integration tests: group by the seam. OrderRepositoryTests,
StripeGatewayTests. One file per thing-you-do-not-own, because that is the
unit of risk.
End-to-end tests: group by journey, not by page.
e2e/
checkout.spec.ts — the whole buy flow
signup-and-verify.spec.ts
admin-refund.spec.ts
support/
api.ts — data setup helpers
pages/ — page objects, if you use themOrganising browser tests by page (cart.spec.ts, product.spec.ts) seems
tidy and produces tests that each cover a fragment of a journey and none of
a behaviour. Failures are then hard to reason about: "cart is broken"
doesn't say whether anyone can still buy anything. See
end-to-end testing.
#Nesting
Two levels of describe is plenty. Four levels produces this:
Account
withdraw
when the balance is insufficient
and there is no overdraft
✗ should return a failure…which reads fine in the source and appears in CI as a single mangled line. Prefer flat names that stand alone.
#A convention worth having: name the flaky quarantine
When a test is quarantined because it is flaky,
put that in the name or a tag — @flaky, [Trait("Stability", "Flaky")].
The value is that the quarantine becomes countable and visible in reports,
rather than a skip with a comment that outlives the person who wrote it.
Common questions
- What is the best test naming convention?
- The one that makes a CI failure legible without opening the file. Whether you write "should_reject_overdraft" or "rejects an overdraft" matters far less than whether the name states the condition and the expected outcome rather than the method under test.
- Should test names include the method name?
- Only if the method name is the behaviour. "Withdraw_WithInsufficientFunds_ReturnsFailure" is fine; "TestWithdraw2" is not. The risk with method-first naming is that it encourages one test per method rather than one test per behaviour.
- How should I organise test files?
- Mirror the production structure for unit tests so they are findable, and organise end-to-end tests by user journey rather than by page, because that is how failures are reasoned about.
Runnable samples for this page
last test results ↗- TypeScript
typescript/src/practices/test-naming
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Arrange-Act-AssertThe three-part shape every readable test has, why the act step should be one line, and the smells that show up when a test will not fit the pattern.
- Given-When-ThenThe Gherkin vocabulary for describing behaviour, how it maps onto arrange-act-assert, and when a shared specification language is worth its cost.
- Unit TestingWhat a unit actually is, what belongs in a unit test and what does not, and the properties that separate a unit suite people run from one they skip.
- Flaky TestsWhy tests fail intermittently, the six root causes and how to fix each one, how to detect flakiness deliberately, and what to do with a test you cannot fix today.
- Behaviour-Driven DevelopmentBDD as a conversation practice rather than a tool choice — what the three amigos session produces, when Cucumber earns its place, and how it fails.
- RSpecRuby's specification-style framework — describe and context blocks, let and subject, matchers, and the readability trade it makes.
- Snapshot TestingRecording output and comparing it on every run — where snapshots earn their place, the approval reflex that destroys their value, and better alternatives.