One Concept Per Test
If the name needs an and, split it.
A test that fails with “expected false 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. A test named “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 from the 2000s onwards. Bill Wake's Arrange-Act-Assert pattern1 implies one Act per test by structure. Robert C. Martin's FIRST in Clean Code2 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 Patterns3 names Verify One Condition per Test as a principle and Eager Test as the smell when you violate it. Beck's Test Desiderata4 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 go through 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, follows 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 goes out.
Origins
The discipline traces back to the early xUnit pattern literature. Bill Wake named the canonical three-part shape Arrange-Act-Assert in 2001. His 2011 write-up of it1 says multi-step tests are “usually better off being split into several tests.” Dave Astels's A New Look at Test-Driven Development7 from 2005 promoted the “should” naming convention that made the one-concept-per-test rule operational at the language level. Gerard Meszaros's xUnit Test Patterns3 in 2007 named both the principle (Verify One Condition per Test) and the smell (Eager Test, Conditional Test Logic) it prevents.
Robert C. Martin's Clean Code2 ch. 9 in 2008 codified the “Single Concept Per Test” line that the industry took as canon. Roy Osherove's The Art of Unit Testing8 repeated it in textbook form for the .NET community. Vladimir Khorikov's Unit Testing9 ch. 4 (“The Four Pillars of a Good Unit Test”) pulls it forward to a 2020-era idea.
Kent Beck's Test Desiderata4 packaged it as the Specific property, a failing test should point at exactly one cause. Dan North's Introducing BDD10 (2006) carried it up to story level: test names became sentences, and a story's acceptance criteria became scenarios. The “and” heuristic in the Prickles rule is borrowed from Adam's in-house code-review vocabulary. The published cousin closest to it is Erik Kuefler's Google post Test Behaviors, Not Methods11, which calls a test name that mirrors the method's name “a bad sign” and asks for “separate tests to verify separate behaviors”.
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 — if a test fails, the cause of the failure should be obvious.
Each Scripted Test should verify a single test condition.
Such multi-step unit tests are usually better off being split into several tests.
Evidence
14 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. A 2011 write-up of the pattern Wake named in 2001; Beck’s TDD by Example had already mentioned it in print.
- 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 principle (Verify One Condition per Test, a.k.a. Single Condition 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.
- 05The Art of Unit Testing, 3rd ed.SupportsThe textbook version of the rule, with examples in JavaScript. “Test only one thing.” Read for the language-agnostic chapter on test naming.
- 06Introduced the “should” naming convention that made one-concept-per-test operational at the language level: the test name is a single sentence.
- 07“When you write your tests, try to include only one Act task per test.” The vendor-canonical .NET version of the rule.
- 08Ch. 4 The Four Pillars of a Good Unit Test. Names the four pillars as Protection against Regressions, Resistance to Refactoring, Fast Feedback and Maintainability; none of them is Specific.
- 09“Programmer Test Principles”SupportsBeck’s earlier (June 2019) list of programmer-test principles; it does not name the Specific property. Pair-read with Test Desiderata.
- 10“Expect literals” / one-concept patterns. The book-length treatment of the assertion-shape side of the rule.
- 11“Vitest it.each”SupportsThe parametrised-assertion API. The applied tooling for the “same concept, many inputs” case without fragmenting the suite.
- 12One test per public method bundles behaviours, because “a single method can exhibit many behaviors”. The fix is “separate tests to verify separate behaviors”.
- 13The canonical critique of dogmatic Clean Code application. Useful as the steelman: enforcement without judgement produces brittle suites.
- 14“Test Smell: Eager Test”SupportsThe smell-side of the rule. Named, defined, and prescribed against in the canonical pattern catalogue.
Fourteen sources, three lineages. The xUnit-pattern tradition (Wake, Meszaros, Beck) names the discipline. The Clean Code tradition (Martin) writes it into the industry-standard chapter. The vendor and practitioner guidance further down (Microsoft, Google, Vitest) turns it into daily practice: one Act per test, separate tests for separate behaviours, one parametrised test for many inputs.
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"); });
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"); });
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"); });
Before one test, three concepts, the AND in the name was the smell.
def test_validates_and_saves_the_hedgehog_and_emits_an_event() -> None: result = register_hedgehog(name="Quill", weight=920) assert result.valid is True assert dict(name="Quill", weight=920) in reserve_registry.records assert "hedgehog-registered" in event_bus.emitted
After three tests, three names, three failure paths.
def test_rejects_a_hedgehog_with_no_name() -> None: result = register_hedgehog(weight=920) assert result.valid is False def test_persists_a_valid_hedgehog_to_the_reserve_registry() -> None: register_hedgehog(name="Quill", weight=920) assert dict(name="Quill", weight=920) in reserve_registry.records def test_emits_a_hedgehog_registered_event_on_success() -> None: register_hedgehog(name="Quill", weight=920) assert "hedgehog-registered" in event_bus.emitted
Before one test, three concepts, the AND in the name was the smell.
@Test void validatesAndSavesTheHedgehogAndEmitsAnEvent() { RegistrationResult result = register(new HedgehogIntake("Quill", 920)); assertTrue(result.valid()); assertTrue(reserveRegistry.records().contains(new HedgehogIntake("Quill", 920))); assertTrue(eventBus.emitted().contains("hedgehog-registered")); }
After three tests, three names, three failure paths.
@Test void rejectsAHedgehogWithNoName() { RegistrationResult result = register(new HedgehogIntake(null, 920)); assertFalse(result.valid()); } @Test void persistsAValidHedgehogToTheReserveRegistry() { register(new HedgehogIntake("Quill", 920)); assertTrue(reserveRegistry.records().contains(new HedgehogIntake("Quill", 920))); } @Test void emitsAHedgehogRegisteredEventOnSuccess() { register(new HedgehogIntake("Quill", 920)); assertTrue(eventBus.emitted().contains("hedgehog-registered")); }
Before one test, three concepts, the AND in the name was the smell.
public function testValidatesAndSavesTheHedgehogAndEmitsAnEvent(): void { $result = $this->registry->register(new HedgehogIntake(name: 'Quill', weight: 920)); $this->assertTrue($result->valid); $this->assertContains(new HedgehogIntake(name: 'Quill', weight: 920), $this->reserveRegistry->records); $this->assertContains('hedgehog-registered', $this->eventBus->emitted); }
After three tests, three names, three failure paths.
public function testRejectsAHedgehogWithNoName(): void { $result = $this->registry->register(new HedgehogIntake(name: null, weight: 920)); $this->assertFalse($result->valid); } public function testPersistsAValidHedgehogToTheReserveRegistry(): void { $this->registry->register(new HedgehogIntake(name: 'Quill', weight: 920)); $this->assertContains(new HedgehogIntake(name: 'Quill', weight: 920), $this->reserveRegistry->records); } public function testEmitsAHedgehogRegisteredEventOnSuccess(): void { $this->registry->register(new HedgehogIntake(name: 'Quill', weight: 920)); $this->assertContains('hedgehog-registered', $this->eventBus->emitted); }
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/) | @vitest/eslint-plugin | test titles containing the word “and” between behaviour clauses: the canonical violation of the rule. |
| vitest/max-expects | @vitest/eslint-plugin | tests with too many expects: usually the smell of multiple concepts bundled into one test. |
| vitest/no-conditional-expect | @vitest/eslint-plugin | expect() inside if blocks: the multi-path test that hides multiple concepts behind a single assertion site. |
| vitest/no-conditional-in-test | @vitest/eslint-plugin | if/switch in test bodies: the conditional-test smell. |
| vitest/expect-expect | @vitest/eslint-plugin | 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 { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import vitest from '@vitest/eslint-plugin';
import sonarjs from 'eslint-plugin-sonarjs';
export default defineConfig({
files: ['**/*.spec.{ts,tsx}', '**/*.test.{ts,tsx}'],
languageOptions: { parser: tseslint.parser },
plugins: { vitest, sonarjs },
rules: {
'vitest/expect-expect': 'error',
'vitest/max-expects': ['error', { max: 5 }],
'vitest/no-conditional-expect': 'error',
'vitest/no-conditional-in-test': 'error',
'vitest/valid-title': ['error', {
mustNotMatch: {
it: '\\sand\\s',
},
}],
'sonarjs/assertions-in-tests': 'error',
}
});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/) | @vitest/eslint-plugin | test titles containing the word “and” between behaviour clauses: the canonical violation of the rule. |
| vitest/max-expects | @vitest/eslint-plugin | tests with too many expects: usually the smell of multiple concepts bundled into one test. |
| vitest/no-conditional-expect | @vitest/eslint-plugin | expect() inside if blocks: the multi-path test that hides multiple concepts behind a single assertion site. |
| vitest/no-conditional-in-test | @vitest/eslint-plugin | if/switch in test bodies: the conditional-test smell. |
| vitest/expect-expect | @vitest/eslint-plugin | 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 { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import vitest from '@vitest/eslint-plugin';
import sonarjs from 'eslint-plugin-sonarjs';
export default defineConfig({
files: ['**/*.spec.{ts,tsx}', '**/*.test.{ts,tsx}'],
languageOptions: { parser: tseslint.parser },
plugins: { vitest, sonarjs },
rules: {
'vitest/expect-expect': 'error',
'vitest/max-expects': ['error', { max: 5 }],
'vitest/no-conditional-expect': 'error',
'vitest/no-conditional-in-test': 'error',
'vitest/valid-title': ['error', {
mustNotMatch: {
it: '\\sand\\s',
},
}],
'sonarjs/assertions-in-tests': 'error',
}
});Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| vitest/valid-title | @vitest/eslint-plugin | test titles containing 'and': the rule’s primary smell. |
| vitest/no-conditional-expect | @vitest/eslint-plugin | expect() inside if blocks. |
| vitest/expect-expect | @vitest/eslint-plugin | tests with no assertion. |
eslint.config.mjsconfiguration snippet
import vitest from '@vitest/eslint-plugin';
export default [
{
files: ['**/*.spec.{js,jsx}', '**/*.test.{js,jsx}'],
plugins: { vitest },
rules: {
'vitest/expect-expect': 'error',
'vitest/no-conditional-expect': 'error',
'vitest/valid-title': ['error', {
mustNotMatch: { it: '\\sand\\s', test: '\\sand\\s' },
}],
},
},
];Apply these rules in pyproject.toml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| PT018 (pytest-composite-assertion) | Ruff | assert a and b and c: the multi-concept assertion that hides which behaviour failed. |
| PT012 (pytest-raises-with-multiple-statements) | Ruff | pytest.raises blocks with more than one statement: several calls under one expected exception, so the test can pass when it should fail. |
| PT006 (pytest-parametrize-names-wrong-type) | Ruff | wrong shape on parametrise calls: keeps the parametrised cases readable when the same concept needs many inputs. |
| conftest: fail on _and_ in test name | pytest | test functions whose names contain _and_: the Python equivalent of the JS valid-title rule. |
pyproject.tomlconfiguration snippet
[tool.ruff.lint]
select = [
"PT",
"ANN",
]
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
[tool.pytest.ini_options]
python_functions = ["test_*"]
addopts = "--strict-markers"Apply these rules in config/pmd-tests.xml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| UnitTestContainsTooManyAsserts | PMD | test methods with too many assertions: usually the smell of multiple concepts bundled together. |
| UnitTestShouldIncludeAssert | PMD | @Test methods with no assertion at all. |
| custom XPath rule: TestNameContainsAnd | PMD | test method names matching ‘And’ between camel-case clauses: the Java equivalent of the “name contains and” heuristic. |
| S5778 (one method invocation when testing runtime exceptions) | SonarQube Java | test methods doing too much: the structural side of the rule. |
| S2699 (no assertion) | SonarQube Java | tests that exercise code without asserting. |
| S2187 (test class with no tests) | SonarQube Java | test classes whose methods were renamed away during a refactor. |
config/pmd-tests.xmlconfiguration snippet
<?xml version="1.0"?>
<ruleset name="ts8-one-concept-per-test"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0">
<description>Test methods verify one concept.</description>
<rule ref="category/java/bestpractices.xml/UnitTestContainsTooManyAsserts">
<properties>
<property name="maximumAsserts" value="5"/>
</properties>
</rule>
<rule ref="category/java/bestpractices.xml/UnitTestShouldIncludeAssert"/>
<rule name="TestNameContainsAnd"
language="java"
message="If the test name needs an 'and', split the test."
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<properties>
<property name="xpath">
<value><![CDATA[
//MethodDeclaration[
matches(@Name, '.*[Aa]nd[A-Z].*')
and ModifierList/Annotation[@SimpleName = 'Test']
]
]]></value>
</property>
</properties>
</rule>
</ruleset>Apply these rules in phpmd.tests.xml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| custom rule: TestNameContainsAnd | PHPMD | test methods named test_validates_and_submits or testValidatesAndSubmits: the canonical violation of the rule. |
| ExcessiveMethodLength | PHPMD | test methods longer than 40 lines: usually the smell of multiple Acts in one test. |
| S2699 (no assertion in test) | SonarQube PHP | tests that exercise code without asserting on a public outcome. |
| S3776 (cognitive complexity) | SonarQube PHP | test functions whose branching climbs: the shape of a test carrying several concepts. |
| S2187 (test class with no tests) | SonarQube PHP | test classes whose methods were renamed away during a refactor. |
phpmd.tests.xmlconfiguration snippet
<?xml version="1.0"?>
<ruleset name="TS8"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0">
<rule ref="rulesets/codesize.xml/ExcessiveMethodLength">
<properties>
<property name="maximum" value="40"/>
</properties>
</rule>
<rule name="TestNameContainsAnd"
message="If the test name needs an 'and', split the test."
class="Ts8\Rule\TestNameContainsAnd"
file="rules/TestNameContainsAnd.php">
<properties>
<property name="pattern" value="/_and_|And[A-Z]/"/>
</properties>
</rule>
</ruleset>AI rules
.cursor/rules/ts8-one-concept-per-test.mdc--- alwaysApply: true --- ## 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.5
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 idea in Test Desiderata4 asks for something else: if a test fails, “the cause of the failure should be obvious.” 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 and one test. Two outcomes, the form validated and the user got redirected, are two concepts and 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 deserves 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.each6 supplies what Beck's Specific property asks for4: 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: “3A – Arrange, Act, Assert” (xp123.com, 2011; pattern named 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). Verify One Condition per Test (a.k.a. Single Condition Test). Eager Test as the smell.
- [4]Kent Beck: Test Desiderata (kentbeck.github.io, 2019). The Specific property: a failing test should point at exactly one cause.
- [5]Sam Hughes (qntm): “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.
- [6]Vitest team: “Vitest it.each” (vitest.dev). The parametrised-assertion API. One concept, many inputs, one failure message that names which input broke.
- [7]Dave Astels: A New Look at Test Driven Development (blog essay, July 2005). Promoted the “should” naming convention that made one-concept-per-test operational at the language level.
- [8]Roy Osherove: The Art of Unit Testing, 3rd ed. (Manning, 2024, with Vladimir Khorikov). The textbook version of the rule, with examples in JavaScript.
- [9]Vladimir Khorikov: Unit Testing: Principles, Practices, and Patterns (Manning, 2020). Ch. 4's four pillars (protection against regressions, resistance to refactoring, fast feedback, maintainability) do not include the rule.
- [10]Dan North: “Introducing BDD” (Better Software, March 2006; dannorth.net). Test method names are sentences, and “you can only describe so much behaviour in a single sentence”. The same thinking turns a story’s acceptance criteria into scenarios.
- [11]Erik Kuefler: “Testing on the Toilet: Test Behaviors, Not Methods” (Google Testing Blog, April 2014). A test name that mirrors the method’s name is “a bad sign”; separate behaviours get separate tests.