--- type: convention title: TDD and Test Isolation Methodology summary: Rules for writing isolated, non-flaky tests — setup-based test data over fixtures, ENV cleanup, and avoiding config caching that breaks test overrides. Answers "why is this test flaky / coupled to other tests?" tags: - type/convention - domain/testing scope: global last_updated: 2026-07-13 date: 2026-07-13 related: - phlex-component-design - ai-agent-rules source: hyperthrive_dev --- # TDD and Test Isolation Methodology ## Purpose Governs how to set up test data and handle process-global state (ENV, constants) so tests stay isolated, parallel-safe, and non-flaky. ## Core Principles **1. Build test data explicitly in `setup`, not via fixtures.** Fixtures hide what data exists behind a separate file, creating coupling between unrelated tests that happen to share a fixture. Explicit `setup` blocks make dependencies visible in the test itself, isolate each test's data, and work cleanly under parallel test execution. **2. Never cache dynamic/environment values in constants.** A constant like `COMPANY_NAME = ENV.fetch(...).freeze` is captured once at class-load time — tests can't override it afterward because it's frozen before the test process even starts. Read ENV (or any config source that a test might need to vary) dynamically via a method call, not a frozen constant, so tests can verify fallback and override behavior. **3. Any test that mutates process-global state must restore it, unconditionally.** Tests run in parallel processes; a leaked ENV change or global mutation from one test can flake an unrelated test. Always restore in an `ensure` block, not just at the end of the happy path — restoration must run even when the test body raises. ## Patterns **Setup-based fixtures.** ```ruby setup do @company = Company.create!(name: "Test Company") @contact = Contact.create!(name: "John Doe", company: @company) end ``` **ENV mutation with guaranteed restore.** ```ruby test "with custom ENV" do original = ENV["MY_VAR"] ENV["MY_VAR"] = "test value" # ... ensure original ? ENV["MY_VAR"] = original : ENV.delete("MY_VAR") end ``` **Dynamic config reads.** ```ruby # GOOD — tests can override via ENV at runtime def self.company_name = ENV.fetch("COMPANY_NAME", "Default") ``` ## Anti-Patterns - **Fixture files for test data** → hidden coupling across tests; use `setup` blocks instead - **`CONST = ENV.fetch(...).freeze` in a service/class body** → frozen at load time, untestable; wrap in a method - **ENV mutation without `ensure`** → leaks into parallel test runs, causes flaky failures unrelated to the actual change - **Testing an ActiveJob without `ActiveJob::TestHelper`** → can't assert `assert_enqueued_with`/perform behavior correctly ## Related - [[phlex-component-design]] — the ≥90% coverage bar these test patterns support - [[ai-agent-rules]] — the "test after every change" rule this methodology implements