ConstructiCat Logo
CodeBust.
Browse section ▾

Test Logic in Production.

Production code contains logic, branches, or members that exist only to support testing, blurring the line between what ships and what is merely tested.

##Signs and Symptoms

You find code in the system under test (SUT) that only matters when a test is running. Tell-tale signs:

  • Test hooks / mode flags — branches keyed off a testing, isTest, NODE_ENV === 'test', or mock flag that short-circuit the real behavior.
  • "For tests only" members — public setters, getters, reset(), or constructors added solely so a test can reach internal state, often tagged @VisibleForTesting / @TestOnly and then actually called from production.
  • Equality pollutionequals() / comparison logic added to a production class purely so an assertion can compare two objects.
  • Test dependency in production — production modules importing a test framework, fixture, or mock factory.
// SMELL: the shipped code behaves differently when "testing" is on
class PaymentService {
  charge(order: Order) {
    if (process.env.NODE_ENV === 'test' || this.isTesting) {
      return { status: 'ok', id: 'FAKE-TEST-ID' }; // real gateway never exercised
    }
    return this.gateway.charge(order); // <-- the path that actually ships
  }
}

The "real" branch is the one your customers hit, and it is exactly the branch your tests skip.

##Reasons for the Problem

Why it happens

  • The SUT is hard to test (it talks to a network, clock, payment gateway, or filesystem) and adding an if (testing) shortcut is faster than introducing a proper seam.
  • A test needs to observe or set internal state, so a developer exposes it "just for the test."
  • Stubbing/mocking is awkward, so canned data is hard-coded behind a flag.

Why it hurts

  • False confidence. Tests exercise the test-only branch, so the production path ships untested. Green tests prove the fake works, not the real thing.
  • Reliability / safety. Test-only code that survives into production can run in anger. Meszaros' canonical cautionary tale is Ariane 5: ground-only code left active in flight triggered the failure. An if (isTesting) left enabled is the software version of that.
  • Security. Test bypasses are backdoors — a flag that skips auth, payment, or validation is one config mistake away from being exploitable.
  • Readability & API bloat. Test-only setters/getters/reset() enlarge the public surface and mislead real clients about what the class is for.
  • Maintainability. Two behaviors live in one class; every change must reason about both the production and the test path, and the divergence quietly rots.

##Treatment

Get the test logic out of production code by introducing a proper seam instead of a flag.

  1. Inject the variation (Dependency Injection + Test Double). Replace the hard-coded branch with a collaborator that the test substitutes. Production wires the real implementation; the test wires a fake/stub/mock.
  2. Use a Test-Specific Subclass when you only need to override one method — override it in a subclass that lives in test code, not via an if in the base class.
  3. Apply the Humble Object pattern to pull hard-to-test logic (clock, I/O) behind a thin adapter, so the core logic becomes directly testable without hooks.
  4. Move comparison logic to the test side. Instead of polluting production with equals() for assertions, use a custom matcher/comparator or assert on the fields you care about.
  5. Keep test code out of the build. Use source-set / build-config separation so test helpers can't be compiled into the shipped artifact; mark genuinely test-visible members with @VisibleForTesting/@TestOnly and let a linter enforce that production never calls them.
// BEFORE: test hook inside production code
class PaymentService {
  charge(order: Order) {
    if (this.isTesting) return { status: 'ok', id: 'FAKE-TEST-ID' };
    return this.gateway.charge(order);
  }
}

// AFTER: one code path; the gateway is injected and faked in the test
class PaymentService {
  constructor(private gateway: PaymentGateway) {}
  charge(order: Order) {
    return this.gateway.charge(order); // same path in prod and test
  }
}

// test
const fakeGateway = { charge: () => ({ status: 'ok', id: 'FAKE-TEST-ID' }) };
const service = new PaymentService(fakeGateway);

Now the production path is the only path, and the test controls behavior from the outside.

##Detected by