Mystery Guest¶
| Slug | Severity | Detection Scope | Protects |
|---|---|---|---|
mystery-guest |
Low | per-test | Understandable, Clear failure message |
Summary¶
A test's meaning depends on external data the reader can't see — a fixture file, a long heredoc, a shared factory — without any inline hint of which part of that data matters. The assertion compares against a magic number or string whose origin requires opening three other files.
Aliases¶
- "mystery guest"
- "magic numbers in tests"
- "external fixture file with no summary"
- "fixture-coupled magic numbers"
- "obscure test"
- "heredoc without naming"
- "count == n with no comment"
Description¶
A classical xUnit test smell,1 still relevant. Covers two variants:
- Classical mystery guest —
File.read('fixture.jsonl')thenassert len(rows) == 6with no hint why 6. - Fixture-coupled magic numbers — exact counts tied to fixture file size or order, brittle to any fixture edit.
The semantic judgment: summarize in one sentence the relevant shape of the fixture for this assertion — not the whole shape — and surface that as a comment or derived constant.
This is the lowest-risk, highest-readability entry in the catalog; the transform adds documentation and never changes assertions.
Signals¶
assert count == <n>with no adjacent comment and<n>not derived from anything named in the test.File.read(path)/fs.readFileSync(path)followed by assertions with no inline fixture summary.- Long
<<~HEREDOCblocks asletinputs with no naming. - Test imports a fixture module but shadows part of its contents silently.
- Magic constant (
6,"abc123") compared against parsed fixture output.
False-positive guards¶
External-data signals over-trigger in three shapes the audit must not flag:
- Inlined derivation cures the magic number.
assert count == EXPECTED_USER_MSGS + EXPECTED_ASSISTANT_MSGS(or any expression that names the arithmetic) is not mystery-guest — a reader can see why the answer is what it is without leaving the test. Flag only unnamed magic literals (assert count == 6with no derivation in scope, no comment, no symbolic constant). - Real-world fixtures are the input domain. Parser, codec, schema-validation, and migration tests legitimately read large fixture files because the file is the SUT's input contract. The fixture is not a hidden coupling; it is the test subject. Flag only when the assertion depends on an unstated property of the file (a magic count, a magic offset); do not flag tests that read a fixture and assert on a specifically-described, comment-summarized property of it.
- Named factories with descriptive identifiers. A
let user_with_two_orders = ...ormake_session(messages=3)factory call carries the relevant fixture shape in its name. The shape is not hidden; the identifier states it. Flag unnamed heredocs, unnamedletbindings, and bareFile.read(path)patterns whose product is never named — not the named-factory pattern, even when the underlying fixture data is large.
Prescribed Fix¶
- Describe-before-edit: identify which fixture property the assertion really depends on.
- Add a ≤3-line comment stating the relevant fixture shape, not the whole shape.
- Where an assertion expects a derived count, make the derivation explicit:
expected = USERS + ASSISTANTSrather thanassert count == 6. - Where heredocs have no name, bind them to a
let/ constant with a name that expresses the fixture's role. - Where fixtures are copy-pasted across related repos, note the opportunity for a shared support module — this is a cross-repo refactor, not a per-pass move.
- Gate: preservation of regression-detection power. Zero AST changes to assertions; only surrounding comments and constants change.
Example¶
Before¶
def test_parses_cursor_session_rollout(tmp_path):
shutil.copytree(FIXTURES / 'cursor-rollout', tmp_path, dirs_exist_ok=True)
rows = sync(tmp_path)
assert len(rows) == 6
After¶
def test_parses_cursor_session_rollout(tmp_path):
# Fixture shape: 3 user + 3 assistant messages across 2 sessions.
# `sync` returns one row per message.
shutil.copytree(FIXTURES / 'cursor-rollout', tmp_path, dirs_exist_ok=True)
rows = sync(tmp_path)
EXPECTED_USER_MSGS = 3
EXPECTED_ASSISTANT_MSGS = 3
assert len(rows) == EXPECTED_USER_MSGS + EXPECTED_ASSISTANT_MSGS
A reader can now see why 6 is the right answer without opening the fixture. A new contributor editing the fixture would edit the constants, not chase a magic number.
Related modes¶
shared-state— shared fixtures are often also mystery guests.presentation-coupled— both pin on incidental details of an external artifact.
Polyglot notes¶
Universal. The mechanical transform is language-agnostic (comment insertion plus symbolic constant naming).
-
The "Mystery Guest" name comes from Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code (Addison-Wesley, 2007). See the online catalog at http://xunitpatterns.com/Obscure%20Test.html#Mystery%20Guest. ↩