Jest Test Quarantine Pattern: Isolate Flaky Tests Without Losing Coverage

Jest Test Quarantine Pattern: Isolate Flaky Tests Without Losing Coverage

When a flaky test blocks your CI pipeline, you have three bad options: delete it (lose the coverage), skip it (test.skip, forget it forever), or fix it immediately (takes time you don't have). The quarantine pattern is the fourth option: isolate it, run it separately, and track it until it's fixed.

Quarantine means the flaky test doesn't block your main pipeline. It still runs. You still know if it fails consistently. But a transient failure doesn't break your deploy.

The Problem with test.skip

The instinctive response to a flaky test is test.skip:

// "I'll fix this later"
test.skip("processes refunds correctly", async () => {
  // ...
});

test.skip silently removes the test from the results. Six months later, the test is still skipped. Nobody remembers why. The coverage gap is invisible. The "fix it later" never happens.

Quarantine is different: the test still runs, in a separate job that you actively monitor.

Setting Up the Quarantine Suite

The quarantine pattern uses a custom tag or marker to separate flaky tests from the stable suite.

Option 1: File-based separation

Move flaky tests to a dedicated directory:

tests/
  stable/          # main suite, blocks CI
    auth.test.js
    checkout.test.js
  quarantine/      # separate suite, non-blocking
    refund.test.js  # known flaky

In your CI config, run them separately:

jobs:
  test-stable:
    runs-on: ubuntu-latest
    steps:
      - run: jest tests/stable --ci
    # This blocks the merge

  test-quarantine:
    runs-on: ubuntu-latest
    steps:
      - run: jest tests/quarantine --ci || true
    # This does NOT block the merge (|| true)
    # But it runs, and you can see the results

Option 2: Tag-based separation

Use @quarantine in test names and filter with --testNamePattern:

// refund.test.js
test("@quarantine processes refunds correctly", async () => {
  // ...
});

test("@quarantine handles partial refunds", async () => {
  // ...
});
jobs:
  test-stable:
    steps:
      - run: jest --testNamePattern="^(?!.*@quarantine)"

  test-quarantine:
    steps:
      - run: jest --testNamePattern="@quarantine" || true

Option 3: Custom test tag with jest-circus

For a more structured approach, use jest-circus's todo state or a custom environment:

// jest.setup.js
global.quarantine = (name, fn) => {
  if (process.env.RUN_QUARANTINE === "true") {
    test(name, fn);
  } else {
    test.todo(`[quarantine] ${name}`);
  }
};
// refund.test.js
quarantine("processes refunds correctly", async () => {
  // ...
});

Normal CI run: jest — quarantined tests appear as todo (acknowledged but not running). Quarantine CI run: RUN_QUARANTINE=true jest — quarantined tests execute.

Tracking Quarantined Tests

A test in quarantine without a tracker is a test that will live in quarantine forever. Every quarantined test needs:

  1. A bug ticket (or GitHub issue) linked in a comment
  2. A deadline or review date
  3. Ownership
// @quarantine @issue: HEL-342 @owner: alice @review: 2026-06-01
test("processes refunds correctly", async () => {
  // Known flaky due to Stripe webhook timing — see HEL-342
});

Add a CI job that lists quarantined tests and their ages:

// scripts/quarantine-report.js
const { execSync } = require("child_process");
const output = execSync(
  'grep -rn "@quarantine" tests/ --include="*.test.js"'
).toString();

const tests = output.split("\n").filter(Boolean);
console.log(`\n=== Quarantine Report ===`);
console.log(`${tests.length} tests in quarantine:\n`);
tests.forEach((line) => {
  const [file, ...rest] = line.split(":");
  console.log(`  ${file}`);
  // Extract and display @issue and @owner metadata
});

Run this weekly and post the results to Slack or add it to your CI summary.

Graduating Tests Out of Quarantine

A test leaves quarantine when it's fixed. The process:

  1. Fix the root cause (timing, shared state, network mock, etc.)
  2. Run the test 20+ times locally: jest --testNamePattern="refund" --repeat=20
  3. If it passes consistently, remove the quarantine marker
  4. Move it back to the stable directory (or remove the tag)
  5. Close the tracking issue

Don't graduate a test based on "it passed twice." Flaky tests by definition fail intermittently. Prove it's stable before removing the quarantine.

Quarantine Budget

Track your quarantine count as a metric. Set a budget — say, no more than 10 tests in quarantine at any time. When you add a test to quarantine, you commit to graduating or deleting one other quarantined test.

This prevents quarantine from becoming a permanent parking lot for ignored tests.

# .github/workflows/quarantine-check.yml
name: Quarantine Budget Check
on: [push]
jobs:
  check-budget:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Count quarantined tests
        run: |
          COUNT=$(grep -rn "@quarantine" tests/ --include="*.test.js" | wc -l)
          echo "Quarantined tests: $COUNT"
          if [ $COUNT -gt 10 ]; then
            echo "::error::Quarantine budget exceeded ($COUNT > 10). Graduate or delete quarantined tests."
            exit 1
          fi

Running Quarantine Tests on a Schedule

Quarantined tests should still run regularly so you know if they're consistently failing (real bug) or intermittently failing (actual flakiness):

name: Quarantine Suite
on:
  schedule:
    - cron: "0 6 * * *" # Daily at 6am
  workflow_dispatch: # Manual trigger
jobs:
  quarantine:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - name: Run quarantine suite (5 times each)
        run: |
          RUN_QUARANTINE=true jest --testNamePattern="@quarantine" \
            --repeat=5 \
            --json --outputFile=quarantine-results.json || true
      - name: Upload results
        uses: actions/upload-artifact@v4
        with:
          name: quarantine-results
          path: quarantine-results.json

Review the daily quarantine run results weekly. A quarantined test that now consistently passes is ready to graduate. A test that now consistently fails has a real bug that should be prioritized.

The Full Workflow

  1. Flaky test detected in CI → investigate root cause
  2. Can't fix immediately → quarantine: add @quarantine tag, create tracking issue, set review date
  3. Remove from stable suite → CI is no longer blocked by this test
  4. Quarantined test runs daily in non-blocking job → you maintain visibility
  5. Root cause fixed → run 20x locally, graduate from quarantine, close issue
  6. Weekly: review quarantine count → enforce budget

The quarantine pattern keeps your main suite trustworthy (always green on clean code) while keeping the coverage and visibility you'd lose with test.skip. Flaky tests are a known, tracked problem — not invisible technical debt.

For teams that want automated flakiness detection and tracking, HelpMeTest monitors test results over time and identifies tests that show inconsistent behavior, giving you the quarantine candidate list automatically.

Read more