One Concept Per Test
If the name needs an and, split it.
A test that fails with “expected true to be true (1 of 7 assertions failed)” is not a test, it is seven tests in a trench coat. The failure message cannot say which behaviour broke, which means the next reader has to read the whole body to find out. That is the cost the rule is built to refund.
Opinion
I have read enough test names containing the word and to recognise the smell from across a PR. The test that “validates the form and submits it and shows the success message” is going to fail at submission, the developer is going to spend ten minutes working out which step broke, and the test name will continue to lie about its scope until someone deletes it. The rule is the simplest one in the pillar to state and the most often broken.
The same discipline appears across multiple sources in the early 2000s. Bill Wake's Arrange-Act-Assert pattern1“Arrange-Act-Assert” (xp123.com, 2001). The canonical name for the three-part test shape. One Act per test is the structural cousin of the rule. implies one Act per test by structure. Robert C. Martin's FIRST in Clean Code2Clean Code, Ch. 9 “Unit Tests” (Prentice Hall, 2008). Codifies “Single Concept Per Test” in the same chapter as FIRST. ch. 9 codifies the “Single Concept Per Test” line in the same chapter that gives us Fast, Independent, Repeatable, Self-Validating, Timely. Gerard Meszaros's xUnit Test Patterns3xUnit Test Patterns (Addison-Wesley, 2007). Single Behaviour per Test, Verify One Thing per Test. Eager Test as the smell. names Single Behaviour per Test as a pattern and Eager Test as the smell when you violate it. Beck's Test Desiderata4Test Desiderata (kentbeck.github.io, 2019). The Specific property: a failing test should point at exactly one cause. packages it as the “Specific” property: a failing test should point at exactly one cause.
The “and” heuristic is the operational version of all of these. State the test name aloud. If saying and is required to describe what it verifies, the test is two tests pretending to be one. Split until each name is a single clause. The discipline is judgement, not formula: multiple expect() calls on one outcome are fine; multiple outcomes packed under one name are not. Pair the rule with F2 Intention-Revealing Names and the test names start reading like a feature list rather than a code listing. Pair it with TS6 Behaviour Testing and the unit boundary becomes obvious.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
One concept per test. If the test name needs an 'and', split it. Multiple Acts hide multiple causes behind a single failure message; one Act, one outcome, one test. Use it.each for parametrised assertions when the same concept needs many inputs — that's one concept, many examples, not many concepts. /tenet/one-concept-per-test/TS8
AI eyes only
Rule: one test, one behaviour. If the name needs “and”, split the test.
Reject: test names containing and. Reject: an it block with multiple unrelated assertions. Reject: writing one test per public method when each method exhibits multiple behaviours.
Generate: one it per behaviour. Each it name reads as a single sentence describing one outcome. Multiple assertions inside an it are fine when they verify one behaviour from different angles.
Diagnostic: read the it name. If it has “and”, split into two tests until each name is single-clause.
Why?
- Failure messages become bug reports. When the test name is one clause, the failure tells you what broke without opening the test body.
- Reviews stop debating scope. The reviewer reads the name; if the name has an and, the review comment writes itself.
- Refactors land cleanly. Each behaviour has a test; changing the unit changes the suite in targeted ways rather than rolling-thunder ways.
- One cause per failure. Beck's Specific property — a failing test points at exactly one thing — lands automatically once the rule is enforced.
- Test names read as a feature list. Pair with F2 Intention-Revealing Names and the suite is the spec.
it.eachcovers the “same concept, many inputs” case without fragmenting the suite. One test name, many examples, one failure path that tells you which input broke.- Coding agents stop bundling tests. With the rule in CLAUDE.md and a custom lint that flags test names containing the word and, the agent splits before it ships.
Origins
The discipline traces back to the early xUnit pattern literature. Bill Wake's 2001 Arrange-Act-Assert1“Arrange-Act-Assert” (xp123.com, 2001). The canonical name for the three-part test shape. One Act per test is the structural cousin of the rule. piece named the canonical three-part shape and implied one Act per test. Dave Astels's A New Look at Test-Driven Development8A New Look at Test-Driven Development (2006). Introduced the “should” naming convention that made one-concept-per-test operational at the language level. from 2006 introduced the “should” naming convention that made the one-concept-per-test rule operational at the language level. Gerard Meszaros's xUnit Test Patterns3xUnit Test Patterns (Addison-Wesley, 2007). Single Behaviour per Test, Verify One Thing per Test. Eager Test as the smell. in 2007 named both the pattern (Single Behaviour per Test) and the smell (Eager Test, Conditional Test Logic) it prevents.
Robert C. Martin's Clean Code2Clean Code, Ch. 9 “Unit Tests” (Prentice Hall, 2008). Codifies “Single Concept Per Test” in the same chapter as FIRST. ch. 9 in 2008 codified the “Single Concept Per Test” line that the industry took as canon. Roy Osherove's The Art of Unit Testing9The Art of Unit Testing, 3rd ed. (Manning, 2024). The textbook .NET version of the rule. repeated it in textbook form for the .NET community. Vladimir Khorikov's Unit Testing10Unit Testing: Principles, Practices, and Patterns (Manning, 2020). Ch. 4 The Four Pillars frames the rule as one of four properties of a good unit test. ch. 4 (“The Four Pillars of a Good Unit Test”) pulls it forward to a 2020-era framing.
Kent Beck's Test Desiderata4Test Desiderata (kentbeck.github.io, 2019). The Specific property: a failing test should point at exactly one cause. packaged it as the Specific property — a failing test should point at exactly one cause — and Liz Keogh's Atomic Stories11“Atomic Stories” (lizkeogh.com, 2008). Generalises the discipline to story level. One story, one scenario, one test. generalised the rule to story level. The “and” heuristic in the Prickles framing is borrowed from Adam's in-house code-review vocabulary; the published cousin closest to it is Tim Riley's Anatomy of a Good Test12“The Anatomy of a Good Test” (timriley.info, 2014). Names a single observable behaviour as the unit of a test. piece that names “a single observable behaviour” as the unit of a test.
Quotes
Perhaps a better rule is that we want to test a single concept in each test function. We don't want long test functions that go testing one miscellaneous thing after another.
Specific. When a test fails, the cause of the failure should be obvious.
Each test should verify a single behaviour. The Eager Test smell is what happens when this rule is broken.
Each test should consist of one Arrange, one Act, and one or more Asserts.
Evidence
Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01“Arrange-Act-Assert”SupportsNames the canonical three-part test shape that implies one Act per test. The original published statement of the structure the rule depends on.
- 02FIRST: Fast, Independent, Repeatable, Self-validating, Timely. The same chapter that codifies the FIRST acronym names “Single Concept Per Test” as a separate rule.
- 03xUnit Test PatternsSupportsNames the pattern (Single Behaviour per Test, Verify One Thing per Test) and the smells (Eager Test, Conditional Test Logic, Obscure Test) that violate it.
- 04Test DesiderataSupportsThe Specific property — a failing test should point at exactly one cause. Beck’s mature single-line restatement of the rule.
- 05“The Anatomy of a Good Test”SupportsNames a single observable behaviour as the unit of a test. Useful as a teaching reference.
Sixteen sources, three lineages. The xUnit-pattern tradition (Wake, Meszaros, Beck) names the discipline. The Clean Code tradition (Martin at the top of the row) writes it into the industry-standard chapter. The story tradition further down lifts it to the feature level: one story, one scenario, one test.
Examples
// Before: one test, three concepts, the AND in the name was the smell.it("validates and saves the hedgehog and emits an event", async () => { const result = await registerHedgehog({ name: "Quill", weight: 920 }); expect(result.valid).toBe(true); expect(reserveRegistry.records).toContainEqual({ name: "Quill", weight: 920 }); expect(eventBus.emitted).toContain("hedgehog-registered");});
// After: three tests, three names, three failure paths.it("rejects a hedgehog with no name", async () => { const result = await registerHedgehog({ weight: 920 }); expect(result.valid).toBe(false);});it("persists a valid hedgehog to the reserve registry", async () => { await registerHedgehog({ name: "Quill", weight: 920 }); expect(reserveRegistry.records).toContainEqual({ name: "Quill", weight: 920 });});it("emits a hedgehog-registered event on success", async () => { await registerHedgehog({ name: "Quill", weight: 920 }); expect(eventBus.emitted).toContain("hedgehog-registered");});
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| vitest/valid-title (mustNotMatch /\sand\s/) | eslint-plugin-vitest | test titles containing the word “and” between behaviour clauses — the canonical violation of the rule. |
| vitest/max-expects | eslint-plugin-vitest | tests with too many expects — usually the smell of multiple concepts bundled into one test. |
| vitest/no-conditional-expect | eslint-plugin-vitest | expect() inside if blocks — the multi-path test that hides multiple concepts behind a single assertion site. |
| vitest/no-conditional-in-test | eslint-plugin-vitest | if/switch in test bodies — the conditional-test smell. |
| vitest/expect-expect | eslint-plugin-vitest | tests with no expect() at all — the silent green that pretends to be coverage. |
| sonarjs/assertions-in-tests | eslint-plugin-sonarjs | test functions whose body has no assertion — the structural empty-green smell. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import vitest from 'eslint-plugin-vitest';
export default tseslint.config({
files: ['**/*.spec.{ts,tsx}', '**/*.test.{ts,tsx}'],
plugins: { vitest },
rules: {
'vitest/expect-expect': 'error',
'vitest/max-expects': ['warn', { max: 5 }],
'vitest/no-conditional-expect': 'error',
'vitest/no-conditional-in-test': 'error',
'vitest/valid-title': ['error', {
mustNotMatch: {
it: '\\sand\\s',
},
mustMatch: {
it: '^should ',
},
}],
'sonarjs/assertions-in-tests': 'error',
}
});AI rules
.cursor/rules/ts8-one-concept-per-test.mdc---
description: Prickles TS8 — One Concept Per Test
globs: "**/*.{spec,test}.{ts,tsx,js,jsx}"
alwaysApply: false
---
## Prickles TS8 — One Concept Per Test
One concept per test. The test name is the spec for the test; if the name needs an `and`, split the test.
One Act per test. The Arrange-Act-Assert shape has exactly one Act. Multiple Acts hide multiple causes behind a single failure.
One assertion focus per test. Multiple expects are fine when they describe one outcome from different angles; not when they describe two outcomes that should be two tests.
If a failure message can't tell you which behaviour broke, the test was wrong even when it was green.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The honest pushback says the rule is subjective: what counts as “one concept” is judgement, and dogmatic enforcement produces brittle test fragmentation. A test of the form rejects empty submissions needs the form to render, the submit handler to fire, and the error message to appear. Splitting that into three tests reads as rule-worship. The argument runs: bundle assertions when they describe one outcome from multiple angles; only split when the outcomes are independent.6“It’s probably time to stop recommending Clean Code” (qntm.org, 2020). The strongest published critique of dogmatic Clean Code application; relevant to the test-fragmentation steelman.
Counter-argument retort
The judgement objection is right and it is the discipline. The rule is not “one expect() per it()” — that does produce brittle fragmentation, and Beck's mature framing in Test Desiderata4Test Desiderata (kentbeck.github.io, 2019). The Specific property: a failing test should point at exactly one cause. is careful about that. The rule is one concept per test. Multiple expect() calls on one outcome — the form rendered, the submit fired, the error showed — describe a single behaviour from three angles. That's one concept; one test. Two outcomes — the form validated and the user got redirected — are two concepts; two tests. The “and” heuristic doesn't care how many expect() calls live inside; it cares whether the test name describes one thing or two.
Where the rule earns its place is in failure-message economy. A well-split test fails with submitting an empty form shows the required-fields error and the bug report writes itself. A bundled test fails with FormBehaviour test failed (expect: true === false at line 47) and the next reader has to read the body to find out which step broke. Ten bundled tests in a suite cost an hour of reading every time CI goes red. Ten well-split tests cost a glance.
The parametrised half of the rule is what stops it degenerating into “split everything.” When the same concept needs many inputs — the password rules accept these eight forms and reject those four — it.each7“Vitest it.each” (vitest.dev). The parametrised-assertion API; one concept, many inputs, one failure message that names which input broke. ships the parametrised assertion that Beck named as Specific in print4Test Desiderata (kentbeck.github.io, 2019). The Specific property: a failing test should point at exactly one cause.: one concept (“the password rules behave correctly for these inputs”), many examples, one failure message that names which input broke. Pair the rule with F2 Intention-Revealing Names for the naming half and TS6 Behaviour Testing for the unit boundary, and the suite reads like a feature list.
Notes
- [1]Bill Wake — “Arrange-Act-Assert” (xp123.com, 2001). The canonical name for the three-part test shape. One Act per test is the structural cousin of the rule.
- [2]Robert C. Martin — Clean Code, Ch. 9 “Unit Tests” (Prentice Hall, 2008). Codifies “Single Concept Per Test” in the same chapter as FIRST.
- [3]Gerard Meszaros — xUnit Test Patterns (Addison-Wesley, 2007). Single Behaviour per Test, Verify One Thing per Test. Eager Test as the smell.