Tests that depend on each other are like dominoes - knock one over and watch your entire test suite collapse.
You know you have test isolation problems when:
// 😱 BAD: Module-level shared state let database; let currentUser; test('create user', () => { currentUser = createUser('alice'); database = new Database(); database.save(currentUser); }); test('update user', () => { // Depends on currentUser from previous test! currentUser.name = 'Alice Smith'; database.update(currentUser); // Also depends on database! });
// 😱 BAD: Tests must run in specific order test('1. initialize system', () => { System.initialize(); expect(System.isReady).toBe(true); }); test('2. load configuration', () => { // Assumes system is already initialized System.loadConfig('./config.json'); expect(System.config).toBeDefined(); }); test('3. start processing', () => { // Needs both initialization and config System.startProcessing(); expect(System.isProcessing).toBe(true); });
// 😱 BAD: Tests pollute shared resources test('save user to database', async () => { await db.query('INSERT INTO users VALUES ("test@example.com")'); const user = await db.query('SELECT * FROM users WHERE email = "test@example.com"'); expect(user).toBeDefined(); }); test('count users', async () => { // Will fail if previous test didn't run or cleanup! const count = await db.query('SELECT COUNT(*) FROM users'); expect(count).toBe(1); // Assumes previous test's data exists });
// ✅ GOOD: Fresh state for each test describe('User Management', () => { let database; let user; beforeEach(() => { // Fresh instances for each test database = new Database(); user = createUser('alice'); }); afterEach(() => { // Clean up after each test database.close(); }); test('create user', () => { database.save(user); expect(database.find(user.id)).toEqual(user); }); test('update user', () => { database.save(user); // Set up required state user.name = 'Alice Smith'; database.update(user); expect(database.find(user.id).name).toBe('Alice Smith'); }); });
// ✅ GOOD: Each test sets up everything it needs test('process order with discount', () => { // Complete setup within the test const customer = new Customer({ id: 'test-123', loyaltyTier: 'gold' }); const order = new Order({ customer, items: [ { id: 1, price: 100, quantity: 2 }, { id: 2, price: 50, quantity: 1 } ] }); const discount = new DiscountCalculator(); const total = discount.calculate(order); expect(total).toBe(225); // 250 - 10% gold discount });
// ✅ GOOD: Consistent test data creation class UserFactory { static create(overrides = {}) { return { id: Math.random().toString(36), email: `test-${Date.now()}@example.com`, name: 'Test User', createdAt: new Date(), ...overrides }; } } test('user can update profile', () => { const user = UserFactory.create({ name: 'Alice' }); const service = new UserService(); service.updateProfile(user.id, { name: 'Alice Smith' }); expect(service.getUser(user.id).name).toBe('Alice Smith'); }); test('user can delete account', () => { const user = UserFactory.create(); // Independent user const service = new UserService(); service.deleteAccount(user.id); expect(service.getUser(user.id)).toBeNull(); });
If adding .only
to a test makes it fail, you have a dependency problem. This is your canary in the coal mine.
Many test frameworks support --random
or --shuffle
flags. Use them in CI to catch order dependencies early.
If your tests can't run in parallel, they're probably sharing state. Fix the isolation issues and enjoy faster test runs.
Use timestamps, UUIDs, or random strings to ensure test data doesn't collide.test-1755785662885-0.025417722827643097
is your friend.
If any of these fail, you have isolation problems to fix.
When your tests are properly isolated: