TEST006-shared-mutable-state — Test Mutates Module-Level State Without Fixture Isolation
Dimension: TestSmell Default severity: Medium Languages: All (Python, JavaScript/TypeScript, Rust) CWE: CWE-820 Last reviewed: 2026-05-07
What it detects
For every test function (is_test == true), the analyzer checks whether the
function body mutates a module-level (file-scope) mutable variable and whether
the file contains any setUp/tearDown or equivalent lifecycle hook.
A finding is emitted when all of the following hold:
- The function is a test (
is_test == true). - The function body contains a mutation of a module-scope mutable name:
- Python:
global Xkeyword, orX =,X +=,X.append(,X.pop(,X.clear(,X[…], etc., whereXis declared at column 0. - JS/TS:
X =,X +=,X++,X--,X.push(,X.pop(,X[…], etc., whereXis declared withletorvarat column 0. - Rust:
unsafe {block (the canonical wrapper forstatic mutwrites), or direct assignment to a name declared withstatic mutat module scope.
- Python:
- The file has no setUp/tearDown/fixture hook (see below).
Fixture suppression — if the file contains any of the following, all findings are suppressed (conservative approach to avoid false positives):
- A function named
setUp,tearDown,setup,teardown,before,after,beforeEach,afterEach,before_each,after_each,setup_method,teardown_method. - Source tokens
beforeEach(,afterEach(,before(,after(,pytest.fixture,@fixture.
One finding is emitted per test function (the first mutated name is reported).
Why it matters
When tests share mutable module-level state, one test's side effects bleed into subsequent tests. This causes:
- Order-dependent failures — tests pass in isolation but fail when run together.
- Flaky CI — parallel test runners may interleave mutations unpredictably.
- Hard-to-diagnose bugs — the failing test may be blameless; the culprit mutated state in an earlier test.
CWE-820 ("Missing Synchronization") classifies shared mutable state accessed without adequate protection as a defect, even in single-threaded contexts where execution order is non-deterministic.
Example — flagged
Python
COUNTER = 0 # module-level mutable
def test_increment():
global COUNTER
COUNTER += 1 # ← mutates shared state, no setUp/tearDown → flagged
assert COUNTER > 0
JavaScript / TypeScript
let cache: Record<string, number> = {}; // module-level mutable
function test_mutates_cache(): void {
cache["foo"] = 1; // ← mutates shared state, no beforeEach → flagged
expect(cache["foo"]).toBe(1);
}
Rust
static mut TOTAL: i32 = 0;
#[test]
fn test_mutates_total() {
unsafe { TOTAL += 1; } // ← unsafe block on static mut, no fixture → flagged
unsafe { assert!(TOTAL > 0); }
}
Example — not flagged
Python (setUp provides isolation)
COUNTER = 0
class TestCounter:
def setUp(self): # ← lifecycle hook present → entire file suppressed
global COUNTER
COUNTER = 0
def test_increment(self):
global COUNTER
COUNTER += 1
assert COUNTER == 1
JavaScript / TypeScript (beforeEach resets state)
let cache: Record<string, number> = {};
beforeEach(() => { cache = {}; }); // ← lifecycle hook → entire file suppressed
function test_cache_mutation_safe(): void {
cache["key"] = 42;
expect(cache["key"]).toBe(42);
}
Rust (no static mut)
#[test]
fn test_pure_local() {
let local = 0;
assert_eq!(local + 1, 1); // only local state → not flagged
}
Fix guidance
-
Use setUp/tearDown (Python) — reset module-level state in
setUpandtearDownso every test starts with a known baseline. -
Use beforeEach/afterEach (JS/TS) — reset shared objects before each test to prevent state leakage between tests.
-
Use a Drop-based fixture (Rust) — create a RAII guard that resets the
static muton drop, or — better — refactor to avoidstatic mutentirely by passing state as function arguments (dependency injection). -
Prefer dependency injection — design functions to accept their dependencies as parameters rather than reading from global mutable state. This makes tests hermetic by construction.
-
Use fresh-per-test state — construct the object under test inside the test function body so each test gets its own independent instance.
Implementation
- Source:
crates/zuit-analyzers/src/shared_mutable_state.rs - Fixtures:
fixtures/python/shared_mutable_state/main.py— Python positivefixtures/python/not_shared_mutable_state/main.py— Python negativefixtures/js/shared_mutable_state/main.ts— JS/TS positivefixtures/js/not_shared_mutable_state/main.ts— JS/TS negativefixtures/rust/shared_mutable_state/lib.rs— Rust positivefixtures/rust/not_shared_mutable_state/lib.rs— Rust negative