Test Anti-Pattern Detection
Quick, pragmatic analysis of test code in any supported language for anti-patterns and quality issues that undermine test reliability, maintainability, and diagnostic value.
Language-specific guidance: Try
test-analysis-extensionsonce. If it is unavailable, continue immediately with this skill's built-in framework rules; never block the audit on the helper.
When to Use
- User asks to review test quality or find test smells
- User wants to know why tests are flaky or unreliable
- User asks "are my tests good?" or "what's wrong with my tests?"
- User requests a test audit or test code review
- User wants diagnostic findings before deciding what to improve
When Not to Use
- User wants to write new tests from scratch (use
code-testing) - User wants direct implementation fixes rather than a diagnostic review (use the relevant write/edit skill)
- User asks to fix swapped
Assert.AreEqualargument order in MSTest (usewriting-mstest-tests) - User asks to convert MSTest
DynamicDatafromIEnumerable<object[]>toValueTuple(usewriting-mstest-tests) - User wants to run or execute tests (use
run-testsfor .NET) - User wants to migrate between test frameworks or versions (use migration skills)
- User wants raw .NET coverage collection (use
run-tests), non-.NET coverage collection or analysis (use native tooling), project-wide .NET coverage/CRAP metrics (usecoverage-analysis), or named-target .NET CRAP (usecrap-score) - User asks whether tests would catch a bug or wants behavioral/pseudo-mutation gaps (use
test-gap-analysis) - User wants test-mix or happy-vs-error-path classification, standardized tagging, or trait/category distributions (use
test-tagging) - User wants a deep formal test smell audit with academic taxonomy and extended catalog (use
test-smell-detection)
Inputs
Workflow
Step 1: Detect language and load extension
Resolve the named test path from the current workspace before asking for input.
When no path is supplied, discover test files under the current directory using
the repository manifests and conventional test markers. The skill context's
Base directory is documentation storage, not the user's workspace; never
resolve target files relative to it.
If one reader says a path is missing but a workspace glob/search finds it,
normalize that exact path and retry. Use a shell text reader (sed/cat on
Unix, Get-Content on PowerShell) only for a confirmed reader availability,
transport, or path-normalization failure and only after verifying the canonical
path remains inside the current workspace. Stop on content-exclusion,
permission/policy, workspace-boundary, or unknown failures. Audit any discovered
file that a permitted reader can access; never ask the user to paste it. If
every permitted reader fails, report the exact blocker without bypassing
security boundaries.
Identify the language and framework. Try the matching
test-analysis-extensions guidance once; if unavailable, use the catalog below.
Step 2: Gather the test code
Inventory the resolved scope before reading bodies. For one file or class, read that scope directly. For a project or suite, discover test files once, batch independent reads where tools allow, and stop when every discovered test and class-level fixture has a ledger disposition.
Use extension discovery markers when loaded; otherwise use the built-in markers
in this skill (attributes such as [TestClass]/[Fact]/[Test],
test_*.py, *.test.*, *_test.go, *_spec.rb, #[test],
*.Tests.ps1, TEST(...), and TEST_CASE(...)).
Do not read unrelated production code wholesale. Open the production symbol corresponding to every suspicious test needed to decide whether an assertion, transformation, identity contract, or adjacent gap is real. For a systematic facade/surface-area pattern, every invoked member is relevant: read the entire small production type or inspect each invoked member, then map each weak test to the exact observable result, exception, state change, or boundary it should verify.
Step 3: Scan for anti-patterns
Check each test file against the anti-pattern catalog below. Report findings grouped by severity. Use extension mappings when loaded; otherwise use the cross-framework examples in the catalog.
Before drafting the report, make a private completeness ledger with one row for every test method and every class-level fixture/resource. Record its oracle (or absence), exception handling, state/time dependencies, concurrency safety, precondition/assertion order, and disposition. Do not publish until every row is either attached to a finding or explicitly judged sound. In particular:
actual != oldValueis a weak mutation oracle: it accepts every wrong new value. Require the exact expected value.- Include unused or undisposed class-level resources; method-only scans miss
fields such as a static
HttpClient. - Treat an unsynchronized static/global collection as both order-coupled and parallel-unsafe when tests read and write it. Also flag dereferencing a nullable result before the assertion intended to prove it non-null.
- When production code is supplied, note obvious untested contracts adjacent to
a finding, but do not perform exhaustive branch or mutation analysis. Route
that broader question to
test-gap-analysis.
Critical -- Tests that give false confidence
High -- Tests likely to cause pain
Medium -- Maintainability and clarity issues
Low -- Style and hygiene
Step 4: Calibrate severity honestly
Before reporting, re-check each finding against these severity rules:
- Critical/High: Only for issues that cause tests to give false confidence or be unreliable. A test that always passes regardless of correctness is Critical. Shared mutable state is High when it is a latent isolation risk, but Critical when the user reports actual order-dependent failures or the code proves one test requires another to run first. Missing-await on async assertions is Critical (silent pass).
- Medium: Only for issues that actively harm maintainability -- 5+ nearly-identical tests, truly meaningless names like
Test1/test/it1. - Low: Cosmetic naming mismatches, minor style preferences, assertion messages that could be better. When in doubt, rate Low.
- Use the caller's severity vocabulary consistently. If the caller asks for Critical / Warning / Info, map latent reliability risks to Warning and maintenance/cosmetic concerns to Info. Keep a demonstrated false-confidence or current order-dependency root cause Critical; do not downgrade it merely to make every requested tier non-empty. Severity describes the demonstrated failure mode, not how much prose a finding receives.
- Separate a systemic finding from its instances. Coverage touching across a
facade is one Critical systemic finding whose evidence lists every affected
test. All assertion-free instances, including the last facade method, retain
the same false-confidence severity. Report
1 finding / 6 affected tests, not six findings plus a seventh summary finding, and do not downgrade one instance merely to manufacture multiple tiers. - Do not severity-rank ordinary missing cases as anti-patterns. Adjacent untested branches, exception paths, and boundaries may be useful coverage opportunities, but list them separately from the anti-pattern counts unless a weak existing test specifically creates the gap. They are not Critical merely because the suite has a systemic Critical issue.
- Not an issue (per-language nuance):
- Go and Rust table-driven loops with sub-tests (
t.Run/for case in cases { ... }) are idiomatic, not "Conditional Test Logic". Do NOT flag. - pytest bare
assertis the canonical assertion form, not a missing assertion library. Do NOT flag. - Go tests use
if got != want { t.Errorf(...) }as canonical equality. Do NOT flag as ad-hoc. - Separate tests for distinct boundary conditions (zero vs. negative vs. null). Do NOT flag as duplicates.
- Explicit per-test setup instead of
[TestInitialize]/beforeEach(this improves isolation). - Tests that are short and clear but could theoretically be consolidated.
- Round-trip or serialization equality with non-trivial input. It is valid metamorphic evidence, not a self-comparison; still recommend one independent representation when producer and consumer could share a defect.
- A transformation tested only with an already-transformed input. Keep it out of the tautology count, but report the weak oracle when removing the transformation would still pass. Use an input that must change and pin its independently expected output.
- Clone value equality. Keep it, and add distinct-reference or mutation- independence evidence when the contract promises a deep copy.
- A validator or accessor returning the original value when pass-through is the production contract. Missing invalid-input cases are a coverage gap, not proof that the existing assertion is tautological.
- Go and Rust table-driven loops with sub-tests (
IMPORTANT: If the tests are well-written, say so clearly up front. Do not inflate severity to justify the review. A review that finds zero Critical/High issues and only minor Low suggestions is a valid and valuable outcome. Lead with what the tests do well.
Step 5: Report findings
Depth bar — a tidy report that is shallower than an unassisted review is a failure. Before writing, satisfy all five:
- Account for every test in scope. Build the complete method/field inventory
before summarizing. For a systematic pattern such as coverage touching,
enumerate every affected test at least once rather than giving representative
examples. A finding table that silently skips tests (or fixtures like an
unused
static HttpClientfield) is incomplete. State the number reviewed. - Verify the production contract before judging the oracle. Inspect the actual transformation, DTO fields, and promised identity/clone semantics. Never invent fields or require lossless round-tripping when production is intentionally lossy. For every suspicious equality, write down the independently known oracle before assigning a finding. If the assertion compares a transformed output with non-trivial input, clone state, snapshot, mock verification, or a framework-native assertion context, explain why it can fail before calling it tautological or assertion-free. Conversely, when a transformation test uses an already-normalized input, call out that the input cannot distinguish the real transformation from a no-op and provide a changing input plus exact expected output. For paired producer/consumer APIs, retain the round-trip test and add one independent representation oracle rather than replacing valid metamorphic evidence.
- Make every Critical/High fix complete and specific. Give the replacement assertion with the exact expected value (the computed discount, the exact CSV line, the full expected object), not a
// assert something hereplaceholder. - Name obvious adjacent gaps without widening into mutation analysis —
when production code is supplied, note directly related untested throws,
null results, boundary values, and round-trip/culture-sensitivity risks in an
Adjacent coverage gaps section. Use
test-gap-analysisfor exhaustive branch-by-branch behavioral gaps. - Keep the report internally consistent. Summary counts must equal the enumerated findings. Publish a settled conclusion: do all reconsidering before you write, and never leave "wait, that's wrong" / "this should fail but doesn't" reasoning in the output.
- Make non-findings decisive. For a clean or mostly clean small suite, name the suspicious constructs you cleared and the framework rule that makes each valid. Do not bury a clean verdict under a generic checklist or speculative improvements.
Present findings in this structure:
- Summary -- Total issues found, broken down by severity (Critical / High / Medium / Low). If tests are well-written, lead with that assessment.
- Critical and High findings -- List each with:
- The anti-pattern name
- The specific location (file, method name, line)
- A brief explanation of why it's a problem
- A concrete fix (show before/after code when helpful)
- Medium and Low findings -- Summarize in a table unless the user wants full detail
- Positive observations -- Call out things the tests do well (sealed class, specific exception types, data-driven tests, clear AAA structure, proper use of fakes, good naming). Don't only report negatives.
Before publishing, assign each finding a stable identity. A grouped row counts
as one finding regardless of how many methods it lists; separate rows count
separately. Recompute the summary from those rows. Keep affected tests as a
different number so a bundled finding cannot create a hidden count mismatch.
Step 6: Prioritize recommendations
If there are many findings, recommend which to fix first:
- Critical -- Fix immediately, these tests may be giving false confidence
- High -- Fix soon, these cause flakiness or maintenance burden
- Medium/Low -- Fix opportunistically during related edits
Validation
- Every test method in scope is accounted for (reviewed count stated; none silently skipped)
- Identity and round-trip findings match the production contract and use only real fields
- Every finding includes a specific location (not just a general warning)
- Every Critical/High finding includes a concrete fix with exact expected values
- Adjacent untested error paths and boundary values are called out
- Summary counts match the enumerated findings
- Grouped findings distinguish finding count from affected-test count
- Adjacent coverage opportunities are not inflated into Critical anti-pattern findings
- Report covers all categories (assertions, isolation, naming, structure)
- Positive observations are included alongside problems
- Recommendations are prioritized by severity


