ConstructiCat Logo
CodeBust.
Browse section ▾

For Testers Only.

Production code contains methods, state accessors, or seams that exist only to be used by tests, polluting the real API and inviting tests that verify internals instead of behavior.

##Signs and Symptoms

You find members in production code whose only callers live in test files. Tell-tale signs:

  • Methods, getters/setters, exports, or constructor parameters used exclusively from *.test.ts / *.spec.ts.
  • Test-flavored names or markers: getStateForTest, resetForTesting, __getInternal, FTO_*, forTest, comments like // only used by tests, or annotations such as @VisibleForTesting / @TestOnly / @internal.
  • Visibility relaxed (a field made public/exported, #private turned protected) purely so a test can reach internal state.
  • Extra "seams" — a setClock(...), setRandom(...), or reset() — added only because a test needed them, not because the real design calls for them.
// payment-service.ts  (PRODUCTION code)
export class PaymentService {
  #ledger: Entry[] = [];

  charge(amount: number) { /* ... */ }

  // Nothing in production ever calls these — only the tests do:
  getLedgerForTest() { return this.#ledger; }          // exposes internals
  setClockForTest(now: () => Date) { this.now = now; } // test-only seam
  FTO_reset() { this.#ledger = []; }                   // "For Tests Only"
}

A quick check: grep -rn 'forTest\|ForTesting\|FTO_' src/ that returns hits in production source, or a dead-code pass (e.g. Knip in production mode) that reports an export as unused while a test clearly imports it.

##Reasons for the Problem

Why it happens

  • Retrofitting tests onto untestable code. When legacy code wasn't designed for testability, the fastest way to assert on a result is to punch a hole into the SUT and read its internals — Meszaros lists this as the prime cause of For Tests Only.
  • Asymmetric APIs. Real clients use an object one way (write); tests use it symmetrically (write then read back to verify), so testers "need" accessors that no production caller needs.
  • Schedule pressure. Adding a back door is cheaper in the moment than refactoring to a design where behavior is observable through the public contract.

Why it hurts

  • Readability. The public API no longer tells the truth — maintainers can't distinguish the real contract from test scaffolding, and every reader has to wonder "is this method actually used?"
  • Encapsulation & reliability. Internal state becomes reachable and mutable in shipping code. A stray call to FTO_reset() or a leaked setter can corrupt state in production; the extra surface also bloats the bundle and widens the attack surface.
  • False confidence. Tests that poke private state assert on implementation, not behavior. They can stay green while the public contract is broken, and they break on harmless refactors — fragile tests that test the wrong thing.
  • Maintainability. The test-only members look like dead code but can't be deleted; every change has to account for phantom callers, and the smell tends to multiply as more tests reuse the back door.

##Treatment

Treat the back door as a design signal, not a fixture detail.

  1. Test through observable behavior first. Assert on return values, emitted events, persisted output, or interactions with collaborators (via test doubles) instead of reaching into internals. Most getXForTest accessors disappear once you verify what the object does, not what it holds.
  2. Use a Test-Specific Subclass when you genuinely need internal access. Extend the class in the test to expose a protected member, instead of widening production visibility.
  3. Make seams part of the real design, not test-only hatches. Injecting a clock or RNG through the normal constructor is legitimate dependency injection; a setClockForTest() mutator is a smell. If a seam only makes sense for tests, push the behavior into a Strategy/Null Object that production installs by default and the test swaps out.
  4. If exposure is truly unavoidable, label it loudly and fence it off. Mark it @VisibleForTesting / @internal (or an FTO_ naming convention) and enforce that production code never calls it (see detectors). A marked, guarded seam beats a silent one.
  5. Hunt existing offenders. grep for forTest/ForTesting/FTO_, and run a usage/dead-code tool such as Knip in production mode to surface exports referenced only by test files.
// BEFORE — production carries a test-only accessor
export class Cart {
  #items: Item[] = [];
  add(i: Item) { this.#items.push(i); }
  getItemsForTest() { return this.#items; } // for testers only
}
// test
expect(cart.getItemsForTest()).toHaveLength(1);
// AFTER — assert behavior through the real contract
export class Cart {
  #items: Item[] = [];
  add(i: Item) { this.#items.push(i); }
  get count() { return this.#items.length; }
  get total() { return this.#items.reduce((s, i) => s + i.price, 0); }
}
// test
cart.add({ price: 10 });
expect(cart.count).toBe(1);
expect(cart.total).toBe(10);

If you still need internal access, narrow the seam to tests with a subclass rather than the whole world:

// production stays clean: queue is protected, not public
export class Scheduler {
  protected queue: Job[] = [];
  enqueue(j: Job) { this.queue.push(j); }
}
// test file only
class TestScheduler extends Scheduler {
  peek() { return this.queue; }
}

##Detected by

  • sonar java:S5803"@VisibleForTesting" members should not be accessed from production code