test-isolation

from yanko-belov/code-craft

No description

4 stars0 forksUpdated Jan 22, 2026
npx skills add https://github.com/yanko-belov/code-craft --skill test-isolation

SKILL.md

Test Isolation

Overview

Each test must be independent. No shared state. No dependencies between tests.

Tests that depend on each other are brittle, hard to debug, and can't run in parallel. Every test should set up its own state and clean up after itself.

When to Use

  • Writing any test that uses shared data
  • Tests that must run in a specific order
  • Tests that fail randomly or when run alone
  • Test suites that can't run in parallel

The Iron Rule

NEVER let one test depend on another test's state or execution.

No exceptions:

  • Not for "it's more efficient"
  • Not for "the first test creates the data"
  • Not for "they always run in order"
  • Not for "it works on my machine"

Detection: Dependency Smell

If tests share mutable state or depend on order, STOP:

// ❌ VIOLATION: Tests depend on each other
describe('UserService', () => {
  let userService: UserService;
  let createdUserId: string;  // Shared state!
  
  it('creates a user', async () => {
    const user = await userService.create({ name: 'Alice' });
    createdUserId = user.id;  // First test sets state
    expect(user).toBeDefined();
  });
  
  it('finds the created user', async () => {
    const user = await userService.findById(createdUserId);  // Second test uses it
    expect(user.name).toBe('Alice');
  });
});

Problems:

  • Second test fails if first doesn't run
  • Can't run tests in parallel
  • Random failures when order changes

The Correct Pattern: Isolated Tests

Each test manages its own state:

// ✅ CORRECT: Each test is independent
describe('UserService', () => {
  let userService: UserService;
  
  beforeEach(() => {
    userService = new UserService(new InMemoryUserRepo());
  });
  
  afterEach(() => {
    // Clean up if needed
  });
  
  it('creates a user', async () => {
    const user = await userService.create({ name: 'Alice' });
    expect(user.id).toBeDefined();
    expect(user.name).toBe('Alice');
  });
  
  it('finds a user by id', async () => {
    // Arrange: Create own test data
    const created = await userService.create({ name: 'Bob' });
    
    // Act
    const found = await userService.findById(created.id);
    
    // Assert
    expect(found.name).toBe('Bob');
  });
  
  it('returns null for non-existent user', async () => {
    // No setup needed - tests the empty state
    const found = await userService.findById('non-existent');
    expect(found).toBeNull();
  });
});

Isolation Techniques

1. Fresh Instance Per Test

beforeEach(() => {
  service = new Service(new MockDependency());
});

2. Database Transactions

beforeEach(async () => {
  await db.beginTransaction();
});

afterEach(async () => {
  await db.rollback();  // Undo all changes
});

3. In-Memory Stores

beforeEach(() => {
  repository = new InMemoryRepository();  // Fresh empty store
});

4. Factory Functions

function createTestUser(overrides = {}) {
  return { id: uuid(), name: 'Test', ...overrides };
}

it('test 1', () => {
  const user = createTestUser({ name: 'Alice' });
});

it('test 2', () => {
  const user = createTestUser({ name: 'Bob' });  // Own data
});

Pressure Resistance Protocol

1. "It's More Efficient"

Pressure: "Creating data once and reusing is faster"

Response: Shared state causes random failures that waste hours debugging.

Action: Use beforeEach to create fresh state. The milliseconds saved aren't worth the debugging time.

2. "The First Test Creates Data"

Pressure: "Test 1 creates a user, Test 2 verifies it"

Response: This creates implicit coupling. Test 2 can't run alone.

Action: Each test creates its own data in Arrange phase.

3. "They Always Run In Order"

Pressure: "Our test runner executes sequentially"

Response: Test runners parallelize. CI environments differ. Order assumptions break.

Action: Write tests that pass regardless of order.

Red Flags - STOP and Reconsider

  • Variables declared outside tests and mutated inside
  • Tests that fail when run individually
  • Tests that fail when run in different order
  • beforeAll that creates data used by multiple tests
  • Comments like "run after test X"

All of these mean: Refactor for isolation.

Quick Reference

Shared State (Bad)Isolated (Good)
let userId outside testsCreate user in each test
beforeAll creates databeforeEach creates fresh data
Tests modify shared objectEach test has own instance
Order-dependent executionAny order works

Common Rationalizations (All Invalid)

ExcuseReality
"It's more efficient"Debugging flaky tests is inefficient.
"They run in order"Not in parallel mode or different environments.
"It works locally"It'll fail in CI.
"Just this one time"One coupling leads to more.
"The data is read-only"

...

Read full content

Repository Stats

Stars4
Forks0
LicenseMIT License