Skip to content
End To End Tester

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_valid

The 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:

  1. The subject — what is being exercised
  2. The condition — under what circumstances
  3. The expected outcome — what should be true
csharp
// C#: the Given_When_Then convention in identifier form
public void Withdraw_WhenBalanceIsInsufficient_LeavesTheBalanceUnchanged()
typescript
// 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
# Python: one long snake_case sentence, which reads well in pytest output
def test_withdrawing_more_than_the_balance_leaves_the_balance_unchanged():
java
// 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.tssrc/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 them

Organising 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 ↗

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?