{"id":"3bc81756-4341-43fe-9161-d3ae4a3532e5","shortId":"vmjU6L","kind":"skill","title":"testing-discipline","tagline":"Use when writing or reviewing tests, designing test data, naming a test, choosing what to assert, or writing test helpers/mocks/fixtures","description":"# Testing Discipline\n\n## Overview\n\n**A good test pins the contract, uses concrete examples, names the scenario in domain language, and uses test data safe to show a customer.** Run the checklist in order when writing or reviewing a test.\n\n## When to invoke\n\nInvoke when you're about to:\n\n- Write a new test (unit, integration, end-to-end, characterization)\n- Name or rename a test method, test class, or test file\n- Design a fixture, mock, stub, or shared test helper\n- Choose test data — names, IDs, sample strings, sample numbers\n- Decide what to assert in a test (exact value? structural property? both?)\n- Schedule a long-running suite (soak, perf, cross-platform matrix)\n- Hand off requirements between programmers and testers, or work on acceptance tests\n- Review existing tests for quality, coverage gaps, or test smells\n- Evaluate whether a test suite adequately covers a contract\n\n### Non-triggers — do NOT invoke for\n\n- Running the existing test suite (`npm test`, `pytest`)\n- Fixing a one-line broken assertion where the intent is unchanged\n- Adding a docstring or comment to an existing test\n- Reordering imports in a test file\n- Updating a snapshot file when the underlying intentional change is already understood\n\nIf you're not sure whether a change counts as \"writing a test,\" **invoke anyway** — the checklist is short and skipping it produces tests that pass forever while protecting nothing.\n\n## Test quality checklist\n\nRun every step in order when writing a new test. When reviewing, use the same checks to evaluate quality.\n\n1. **Tests are part of the change, not optional polish.** Software's only practical pre-deployment validation is execution under realistic conditions. A change without tests is an incomplete change. *(Ford, 97/83.)*\n2. **Assert the required behavior, not an incidental of the current implementation.** Ask: what does the contract actually promise? For a 3-way comparator the contract is \"negative if less, positive if greater, zero if equal\" — not \"exactly -1 or +1.\" Asserting ±1 nails an incidental and will go red the day someone returns -2. Tests that mirror code structure end up asserting that the code does what the code does. *(Henney, 97/80.)*\n3. **Be precise *and* accurate. Use concrete examples.** \"Result is sorted and same length\" passes for `[3,3,3,3,3,3]` against an input of `[3,1,4,1,5,9]`. The full postcondition is sorted *and a permutation of the input* — but expressing that as a generic checker is often more code than the function under test. Prefer a concrete example pair: input `[3,1,4,1,5,9]`, expected `[1,1,3,4,5,9]`. \"Adding to an empty collection\" is not \"now non-empty\" — it's \"now contains exactly one item, and that item is X.\" *(Henney, 97/81.)*\n4. **Write the test for the next person who has to read it.** A good test is documentation. Make three parts visible in this order: the context/preconditions, the call into the system, the expected result. Hide trivia behind named helpers (Extract Method) so the reader sees the scenario, not the scaffolding. Give the test a name describing the scenario *and* the entry point — `Stack_pop_on_empty_throws` reads better than `test17`. Then test the test: introduce a deliberate bug into the code under test on a private branch and verify the failure message tells you what went wrong. *(Meszaros, 97/95.)*\n5. **Choose test data that is safe in front of a customer.** Placeholder names, fake company names, sample log strings, mock error dialogs, and seeded database rows have a habit of surfacing in screenshots, demos, leaked source, and production logs. Do not use real people's names you mean to mock, do not use band names or song titles as a private joke, do not write `\"don't click that again, you moron\"` as a placeholder dialog, do not use four-letter words as fake stock tickers. Use boring, obviously-fake, professional data. *(Begbie, 97/25.)*\n6. **Accept acceptance tests as requirements input.** When acceptance tests exist for a behavior (from QA, from the customer, from a spec), use them as input to the work — they surface ambiguities cheaper than defect tickets. *(Hufnagel, 97/60; Gregory, 97/92.)*\n\n## Red Flags\n\nThese thoughts mean STOP — restart the checklist:\n\n| Thought | Reality |\n|---|---|\n| \"I'll just assert the function returns exactly -1.\" | The contract says \"negative\" — ±1 is an implementation incidental. The test will go red on a valid refactor and tell you nothing about the requirement. (97/80) |\n| \"Same length, all elements in range — that's enough for a sort test.\" | `[3,3,3,3,3,3]` satisfies that and is wrong. Use a concrete input/output pair so the only correct answer is the one in the assertion. (97/81) |\n| \"I'll seed the database with band members and song titles — funnier than `User1`.\" | Cute test data ends up in customer screenshots, demos, and leaked source. Use boring, obviously-fake, professional data. (97/25) |\n| \"No time to write the test — we'll add it later.\" | \"Later\" rarely arrives, and shipping without verification is professionally irresponsible. The test is part of the change. (97/83) |\n| \"I know what this test means — I just wrote it.\" | You will not, in six months. Tests are read more than they are written. Name the scenario, structure as context/act/assert, hide scaffolding. (97/95) |\n| \"`test17` is a fine name — the body explains it.\" | Test names are scanned to verify coverage and to read failure reports. Encode the scenario and the entry point in the name. (97/95) |\n| \"The test setup is fifty lines, then a one-line assert — but it works.\" | Test pain is design pressure. If the setup dwarfs the body, reshape the production code. Do not mock harder. (`GOOS/ListenToTestPain`) |\n| \"I'll assert that `repo.save` was called exactly three times.\" | Over-specifying mock interactions makes the test red on innocent refactors. Assert on the observable contract, not the call shape. (`xUnit/FragileTest`) |\n| \"The test reads from `/tmp/fixtures/users.json` set up in `conftest.py`.\" | Mystery Guest. The test cannot be read in isolation. Build the fixture in the test or in a function named for what it returns. (`xUnit/MysteryGuest`) |\n| \"I'll branch on the return value and assert different things in each branch.\" | One test, two scenarios fighting for one name. Split into two tests, or use named parameterized cases. (`xUnit/ConditionalTestLogic`) |\n\n## What \"done\" looks like\n\nA single well-written test is done when **all** of the following are true:\n\n- [ ] The assertion targets the contract, not an incidental of the current implementation.\n- [ ] The expected result is concrete enough that a reader can check it by eye in under thirty seconds.\n- [ ] The test name describes the scenario and the entry point in domain language.\n- [ ] Context, action, and expected result are visible as three readable sections; scaffolding is behind named helpers.\n- [ ] Test data (names, IDs, strings, log lines, error messages) is safe to appear in a customer screenshot.\n- [ ] You verified the test fails for the right reason — by introducing a deliberate bug on a private branch and reading the failure message.\n\nIf any box is unchecked, the test is not done. Either finish, or delete it and start over.\n\n## Principles in this skill\n\n| # | Principle | Author |\n|---|---|---|\n| 97/25 | Don't Be Cute with Your Test Data | Rod Begbie |\n| 97/60 | News of the Weird: Testers Are Your Friends | Burk Hufnagel |\n| 97/80 | Test for Required Behavior, Not Incidental Behavior | Kevlin Henney |\n| 97/81 | Test Precisely and Concretely | Kevlin Henney |\n| 97/82 | Test While You Sleep (and over Weekends) | Rajith Attapattu |\n| 97/83 | Testing Is the Engineering Rigor of Software Development | Neal Ford |\n| 97/92 | When Programmers and Testers Collaborate | Janet Gregory |\n| 97/95 | Write Tests for People | Gerard Meszaros |\n| `GOOS/ListenToTestPain` | Listen to Test Pain | Steve Freeman & Nat Pryce |\n| `xUnit/ObscureTest` | Obscure Test | Gerard Meszaros |\n| `xUnit/FragileTest` | Fragile Test | Gerard Meszaros |\n| `xUnit/MysteryGuest` | Mystery Guest | Gerard Meszaros |\n| `xUnit/ConditionalTestLogic` | Conditional Test Logic | Gerard Meszaros |\n\nSee `principles.md` for the long-form distillations, citations, and source links.","tags":["testing","discipline","oribarilan","agent-skills","ai-agents","best-practices","claude-code","claude-code-plugin","claude-code-skills","coding-agents","copilot-cli","copilot-cli-plugin"],"capabilities":["skill","source-oribarilan","skill-testing-discipline","topic-agent-skills","topic-ai-agents","topic-best-practices","topic-claude-code","topic-claude-code-plugin","topic-claude-code-skills","topic-coding-agents","topic-copilot-cli","topic-copilot-cli-plugin","topic-opencode","topic-opencode-plugin","topic-programming-principles"],"categories":["97"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/oribarilan/97/testing-discipline","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add oribarilan/97","source_repo":"https://github.com/oribarilan/97","install_from":"skills.sh"}},"qualityScore":"0.460","qualityRationale":"deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 21 github stars · SKILL.md body (8,230 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T19:05:33.179Z","embedding":null,"createdAt":"2026-05-08T13:06:24.313Z","updatedAt":"2026-05-18T19:05:33.179Z","lastSeenAt":"2026-05-18T19:05:33.179Z","tsv":"'+1':345 '-1':343,738 '-2':359 '/tmp/fixtures/users.json':1005 '1':272,347,405,407,444,446,450,451,743 '2':305 '3':326,378,394,395,396,397,398,399,404,443,452,778,779,780,781,782,783 '4':406,445,453,481 '5':408,447,454,582 '6':679 '9':409,448,455 '97/25':678,839,1209 '97/60':716,1220 '97/80':377,764,1231 '97/81':480,805,1241 '97/82':1248 '97/83':304,868,1258 '97/92':718,1269 '97/95':581,901,933,1277 'accept':145,680,681,687 'accur':382 'action':1130 'actual':322 'ad':193,456 'add':848 'adequ':162 'alreadi':218 'ambigu':710 'answer':798 'anyway':234 'appear':1157 'arriv':853 'ask':317 'assert':19,114,187,306,346,367,733,804,945,971,991,1043,1087 'attapattu':1257 'author':1208 'band':636,812 'begbi':677,1219 'behavior':309,692,1235,1238 'behind':518,1142 'better':550 'bodi':908,959 'bore':671,833 'box':1187 'branch':569,1037,1048,1179 'broken':186 'bug':560,1175 'build':1019 'burk':1229 'call':509,975,998 'cannot':1014 'case':1065 'chang':216,227,278,296,302,867 'character':81 'cheaper':711 'check':268,1108 'checker':427 'checklist':53,236,252,727 'choos':16,102,583 'citat':1322 'class':89 'click':650 'code':363,370,374,431,563,963 'collabor':1274 'collect':460 'comment':197 'compani':597 'compar':328 'concret':34,384,439,791,1102,1245 'condit':294,1309 'conftest.py':1009 'contain':470 'context':1129 'context/act/assert':898 'context/preconditions':507 'contract':32,165,321,330,740,995,1090 'correct':797 'count':228 'cover':163 'coverag':152,917 'cross':132 'cross-platform':131 'current':315,1096 'custom':50,593,697,826,1160 'cute':820,1213 'data':12,45,104,585,676,822,838,1146,1217 'databas':607,810 'day':356 'decid':111 'defect':713 'delet':1198 'deliber':559,1174 'demo':616,828 'deploy':288 'describ':537,1119 'design':10,93,952 'develop':1266 'dialog':604,658 'differ':1044 'disciplin':3,25 'distil':1321 'docstr':195 'document':498 'domain':40,1127 'done':1068,1078,1194 'dwarf':957 'either':1195 'element':768 'empti':459,466,547 'encod':923 'end':78,80,365,823 'end-to-end':77 'engin':1262 'enough':773,1103 'entri':542,928,1124 'equal':340 'error':603,1152 'evalu':157,270 'everi':254 'exact':118,342,471,737,976 'exampl':35,385,440 'execut':291 'exist':148,175,200,689 'expect':449,514,1099,1132 'explain':909 'express':422 'extract':521 'eye':1111 'fail':1166 'failur':573,921,1183 'fake':596,667,674,836 'fifti':938 'fight':1053 'file':92,207,211 'fine':905 'finish':1196 'fix':181 'fixtur':95,1021 'flag':720 'follow':1083 'ford':303,1268 'forev':246 'form':1320 'four':663 'four-lett':662 'fragil':1299 'freeman':1290 'friend':1228 'front':590 'full':411 'function':434,735,1028 'funnier':817 'gap':153 'generic':426 'gerard':1282,1296,1301,1306,1312 'give':532 'go':353,751 'good':28,495 'goos/listentotestpain':968,1284 'greater':337 'gregori':717,1276 'guest':1011,1305 'habit':611 'hand':135 'harder':967 'helper':101,520,1144 'helpers/mocks/fixtures':23 'henney':376,479,1240,1247 'hide':516,899 'hufnagel':715,1230 'id':106,1148 'implement':316,746,1097 'import':203 'incident':312,350,747,1093,1237 'incomplet':301 'innoc':989 'input':402,420,442,685,704 'input/output':792 'integr':76 'intent':190,215 'interact':983 'introduc':557,1172 'invok':64,65,171,233 'irrespons':860 'isol':1018 'item':473,476 'janet':1275 'joke':644 'kevlin':1239,1246 'know':870 'languag':41,1128 'later':850,851 'leak':617,830 'length':391,766 'less':334 'letter':664 'like':1070 'line':185,939,944,1151 'link':1325 'listen':1285 'll':731,807,847,970,1036 'log':600,621,1150 'logic':1311 'long':126,1319 'long-form':1318 'long-run':125 'look':1069 'make':499,984 'matrix':134 'mean':630,723,874 'member':813 'messag':574,1153,1184 'meszaro':580,1283,1297,1302,1307,1313 'method':87,522 'mirror':362 'mock':96,602,632,966,982 'month':884 'moron':654 'mysteri':1010,1304 'nail':348 'name':13,36,82,105,519,536,595,598,628,637,893,906,912,932,1029,1056,1063,1118,1143,1147 'nat':1291 'neal':1267 'negat':332,742 'new':73,261 'news':1221 'next':487 'non':167,465 'non-empti':464 'non-trigg':166 'noth':249,760 'npm':178 'number':110 'obscur':1294 'observ':994 'obvious':673,835 'obviously-fak':672,834 'often':429 'one':184,472,801,943,1049,1055 'one-lin':183,942 'option':280 'order':55,257,505 'over-specifi':979 'overview':26 'pain':950,1288 'pair':441,793 'parameter':1064 'part':275,501,864 'pass':245,392 'peopl':626,1281 'perf':130 'permut':417 'person':488 'pin':30 'placehold':594,657 'platform':133 'point':543,929,1125 'polish':281 'pop':545 'posit':335 'postcondit':412 'practic':285 'pre':287 'pre-deploy':286 'precis':380,1243 'prefer':437 'pressur':953 'principl':1203,1207 'principles.md':1315 'privat':568,643,1178 'produc':242 'product':620,962 'profession':675,837,859 'programm':139,1271 'promis':323 'properti':121 'protect':248 'pryce':1292 'pytest':180 'qa':694 'qualiti':151,251,271 'rajith':1256 'rang':770 'rare':852 're':68,222 'read':492,549,887,920,1003,1016,1181 'readabl':1138 'reader':525,1106 'real':625 'realist':293 'realiti':729 'reason':1170 'red':354,719,752,987 'refactor':756,990 'renam':84 'reorder':202 'repo.save':973 'report':922 'requir':137,308,684,763,1234 'reshap':960 'restart':725 'result':386,515,1100,1133 'return':358,736,1033,1040 'review':8,59,147,264 'right':1169 'rigor':1263 'rod':1218 'row':608 'run':51,127,173,253 'safe':46,588,1155 'sampl':107,109,599 'satisfi':784 'say':741 'scaffold':531,900,1140 'scan':914 'scenario':38,528,539,895,925,1052,1121 'schedul':123 'screenshot':615,827,1161 'second':1115 'section':1139 'see':526,1314 'seed':606,808 'set':1006 'setup':936,956 'shape':999 'share':99 'ship':855 'short':238 'show':48 'singl':1072 'six':883 'skill':1206 'skill-testing-discipline' 'skip':240 'sleep':1252 'smell':156 'snapshot':210 'soak':129 'softwar':282,1265 'someon':357 'song':639,815 'sort':388,414,776 'sourc':618,831,1324 'source-oribarilan' 'spec':700 'specifi':981 'split':1057 'stack':544 'start':1201 'step':255 'steve':1289 'stock':668 'stop':724 'string':108,601,1149 'structur':120,364,896 'stub':97 'suit':128,161,177 'sure':224 'surfac':613,709 'system':512 'target':1088 'tell':575,758 'test':2,9,11,15,22,24,29,44,61,74,86,88,91,100,103,117,146,149,155,160,176,179,201,206,232,243,250,262,273,298,360,436,484,496,534,554,556,565,584,682,688,749,777,821,845,862,873,885,911,935,949,986,1002,1013,1024,1050,1060,1076,1117,1145,1165,1191,1216,1232,1242,1249,1259,1279,1287,1295,1300,1310 'test17':552,902 'tester':141,1225,1273 'testing-disciplin':1 'thing':1045 'thirti':1114 'thought':722,728 'three':500,977,1137 'throw':548 'ticker':669 'ticket':714 'time':841,978 'titl':640,816 'topic-agent-skills' 'topic-ai-agents' 'topic-best-practices' 'topic-claude-code' 'topic-claude-code-plugin' 'topic-claude-code-skills' 'topic-coding-agents' 'topic-copilot-cli' 'topic-copilot-cli-plugin' 'topic-opencode' 'topic-opencode-plugin' 'topic-programming-principles' 'trigger':168 'trivia':517 'true':1085 'two':1051,1059 'unchang':192 'uncheck':1189 'under':214 'understood':219 'unit':75 'updat':208 'use':4,33,43,265,383,624,635,661,670,701,789,832,1062 'user1':819 'valid':289,755 'valu':119,1041 'verif':857 'verifi':571,916,1163 'visibl':502,1135 'way':327 'weekend':1255 'weird':1224 'well':1074 'well-written':1073 'went':578 'whether':158,225 'without':297,856 'word':665 'work':143,707,948 'write':6,21,57,71,230,259,482,647,843,1278 'written':892,1075 'wrong':579,788 'wrote':877 'x':478 'xunit/conditionaltestlogic':1066,1308 'xunit/fragiletest':1000,1298 'xunit/mysteryguest':1034,1303 'xunit/obscuretest':1293 'zero':338","prices":[{"id":"92294683-c795-489a-8a87-6a3ca12bc817","listingId":"3bc81756-4341-43fe-9161-d3ae4a3532e5","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"oribarilan","category":"97","install_from":"skills.sh"},"createdAt":"2026-05-08T13:06:24.313Z"}],"sources":[{"listingId":"3bc81756-4341-43fe-9161-d3ae4a3532e5","source":"github","sourceId":"oribarilan/97/testing-discipline","sourceUrl":"https://github.com/oribarilan/97/tree/main/skills/testing-discipline","isPrimary":false,"firstSeenAt":"2026-05-08T13:06:24.313Z","lastSeenAt":"2026-05-18T19:05:33.179Z"}],"details":{"listingId":"3bc81756-4341-43fe-9161-d3ae4a3532e5","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"oribarilan","slug":"testing-discipline","github":{"repo":"oribarilan/97","stars":21,"topics":["agent-skills","ai-agents","best-practices","claude-code","claude-code-plugin","claude-code-skills","coding-agents","copilot-cli","copilot-cli-plugin","opencode","opencode-plugin","programming-principles"],"license":"other","html_url":"https://github.com/oribarilan/97","pushed_at":"2026-05-15T21:32:54Z","description":"Agent skills distilled from the hard-won lessons of world-renowned programmers, in the spirit of \"97 Things Every Programmer Should Know\"","skill_md_sha":"e92a7a9c44194a62eaf04c89103e3b727d22e25e","skill_md_path":"skills/testing-discipline/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/oribarilan/97/tree/main/skills/testing-discipline"},"layout":"multi","source":"github","category":"97","frontmatter":{"name":"testing-discipline","description":"Use when writing or reviewing tests, designing test data, naming a test, choosing what to assert, or writing test helpers/mocks/fixtures"},"skills_sh_url":"https://skills.sh/oribarilan/97/testing-discipline"},"updatedAt":"2026-05-18T19:05:33.179Z"}}