# no-test-isolation Prevent test interdependencies that can cause failures when tests run in different orders. ## Rule Details Test isolation violations occur when tests depend on the execution or state of other tests: - Tests may pass when run individually but fail in suites - Test execution order affects outcomes - Shared state between tests creates dependencies - Setup/teardown issues can leak between tests - Global modifications persist across test boundaries This rule helps prevent test flakiness by detecting patterns that violate test isolation principles. ## Options This rule accepts an options object with the following properties: ```json { "test-flakiness/no-test-isolation": [ "error", { "allowSharedSetup": true, "checkGlobalState": true, "allowedSharedVariables": [] } ] } ``` ### `allowSharedSetup` (default: `true`) When set to `true`, allows shared setup in `beforeAll`/`beforeEach` hooks. ```javascript // With allowSharedSetup: true (default) beforeAll(() => { global.testDatabase = createTestDatabase(); // Allowed }); // With allowSharedSetup: false beforeAll(() => { global.testDatabase = createTestDatabase(); // Not allowed }); ``` ### `checkGlobalState` (default: `true`) When set to `true`, checks for global state modifications that could affect other tests. ```javascript // With checkGlobalState: true (default) test("should do something", () => { global.config = { debug: true }; // Flagged }); // With checkGlobalState: false test("should do something", () => { global.config = { debug: true }; // Ignored }); ``` ### `allowedSharedVariables` (default: `[]`) Array of variable names that are allowed to be shared between tests. ```javascript // With allowedSharedVariables: ["testUtils"] let testUtils = createTestUtils(); // Allowed // With allowedSharedVariables: [] let sharedData = {}; // Not allowed ``` ## Examples ### Incorrect ```javascript // Shared variables between tests let userData = {}; let counter = 0; test("creates user", () => { userData = { id: 1, name: "John" }; counter++; }); test("uses user data", () => { expect(userData.name).toBe("John"); // Depends on previous test expect(counter).toBe(1); // Order dependent }); // Tests that modify global state test("sets global config", () => { process.env.NODE_ENV = "test"; global.apiUrl = "http://localhost"; }); test("uses global config", () => { expect(global.apiUrl).toBe("http://localhost"); // Depends on previous test }); // DOM modifications that persist test("adds elements to DOM", () => { document.body.innerHTML = '
Test
'; }); test("expects DOM elements", () => { expect(document.getElementById("test")).toBeTruthy(); // Depends on previous test }); // File system modifications test("creates test file", () => { fs.writeFileSync("test.txt", "content"); }); test("reads test file", () => { const content = fs.readFileSync("test.txt"); // Depends on file creation expect(content.toString()).toBe("content"); }); // Database state dependencies test("creates user in database", () => { database.users.insert({ id: 1, name: "John" }); }); test("finds user in database", () => { const user = database.users.findById(1); // Depends on previous insert expect(user.name).toBe("John"); }); // Mock state that persists let mockFn = jest.fn(); test("calls mock function", () => { mockFn("test"); expect(mockFn).toHaveBeenCalledWith("test"); }); test("expects mock to have been called", () => { expect(mockFn).toHaveBeenCalled(); // Depends on previous test }); // Class/object state sharing class TestState { constructor() { this.value = 0; } } const testState = new TestState(); test("increments value", () => { testState.value++; }); test("expects incremented value", () => { expect(testState.value).toBe(1); // Depends on previous test }); ``` ### Correct ```javascript // Each test is self-contained test("creates user", () => { const userData = { id: 1, name: "John" }; expect(userData.name).toBe("John"); }); test("processes user data", () => { const userData = { id: 2, name: "Jane" }; // Own test data expect(processUser(userData)).toBeDefined(); }); // Proper setup and teardown describe("User management", () => { let testDatabase; beforeEach(() => { testDatabase = createTestDatabase(); }); afterEach(() => { testDatabase.cleanup(); }); test("creates user", () => { const user = testDatabase.createUser({ name: "John" }); expect(user.name).toBe("John"); }); test("finds user", () => { const user = testDatabase.createUser({ name: "Jane" }); const found = testDatabase.findUser(user.id); expect(found.name).toBe("Jane"); }); }); // DOM cleanup between tests describe("DOM tests", () => { afterEach(() => { document.body.innerHTML = ""; }); test("adds elements", () => { document.body.innerHTML = '
Test
'; expect(document.getElementById("test")).toBeTruthy(); }); test("works with clean DOM", () => { document.body.innerHTML = '
Other
'; expect(document.getElementById("other")).toBeTruthy(); }); }); // File system cleanup describe("File operations", () => { const testFile = "test.txt"; afterEach(() => { if (fs.existsSync(testFile)) { fs.unlinkSync(testFile); } }); test("creates file", () => { fs.writeFileSync(testFile, "content"); expect(fs.existsSync(testFile)).toBeTruthy(); }); test("works independently", () => { // File doesn't exist from previous test expect(fs.existsSync(testFile)).toBeFalsy(); fs.writeFileSync(testFile, "new content"); expect(fs.readFileSync(testFile, "utf8")).toBe("new content"); }); }); // Fresh mocks for each test describe("Mock tests", () => { let mockFn; beforeEach(() => { mockFn = jest.fn(); }); test("calls mock function", () => { mockFn("test"); expect(mockFn).toHaveBeenCalledWith("test"); }); test("has fresh mock", () => { expect(mockFn).not.toHaveBeenCalled(); // Clean mock mockFn("other"); expect(mockFn).toHaveBeenCalledWith("other"); }); }); // Factory functions for test data function createTestUser(overrides = {}) { return { id: Math.random(), // Or use deterministic ID name: "Test User", ...overrides, }; } test("processes user A", () => { const user = createTestUser({ name: "Alice" }); expect(processUser(user)).toBeDefined(); }); test("processes user B", () => { const user = createTestUser({ name: "Bob" }); expect(processUser(user)).toBeDefined(); }); ``` ## Best Practices ### 1. Use beforeEach/afterEach for Setup/Teardown Ensure each test starts with a clean state: ```javascript describe("Component tests", () => { let component; beforeEach(() => { component = createComponent(); }); afterEach(() => { component.destroy(); }); test("renders correctly", () => { expect(component.render()).toMatchSnapshot(); }); test("handles props", () => { component.setProps({ title: "Test" }); expect(component.getTitle()).toBe("Test"); }); }); ``` ### 2. Avoid Shared Variables Keep test data local to each test: ```javascript // Instead of shared state let sharedUser = {}; test("creates user", () => { sharedUser = createUser(); }); test("uses user", () => { expect(sharedUser.name).toBeDefined(); // Risky dependency }); // Use local state test("creates and uses user", () => { const user = createUser(); expect(user.name).toBeDefined(); }); ``` ### 3. Clean Up Global State Reset global modifications after each test: ```javascript describe("Environment tests", () => { const originalEnv = process.env.NODE_ENV; afterEach(() => { process.env.NODE_ENV = originalEnv; }); test("works in development", () => { process.env.NODE_ENV = "development"; expect(getConfig()).toMatchObject({ debug: true }); }); test("works in production", () => { process.env.NODE_ENV = "production"; expect(getConfig()).toMatchObject({ debug: false }); }); }); ``` ### 4. Use Test-Specific Databases Create isolated database instances: ```javascript describe("Database operations", () => { let db; beforeEach(async () => { db = await createTestDatabase(); }); afterEach(async () => { await db.close(); }); test("creates record", async () => { const record = await db.create({ name: "Test" }); expect(record.id).toBeDefined(); }); test("finds record", async () => { const created = await db.create({ name: "Find Me" }); const found = await db.findById(created.id); expect(found.name).toBe("Find Me"); }); }); ``` ### 5. Reset Mocks Between Tests Ensure mocks don't carry state: ```javascript describe("API tests", () => { const mockApi = jest.fn(); beforeEach(() => { mockApi.mockClear(); // or mockReset() }); test("calls API once", () => { callService(mockApi); expect(mockApi).toHaveBeenCalledTimes(1); }); test("calls API with correct data", () => { callService(mockApi, { data: "test" }); expect(mockApi).toHaveBeenCalledWith({ data: "test" }); }); }); ``` ### 6. Use Factory Functions Create fresh test data for each test: ```javascript // Test data factory function createTestOrder(overrides = {}) { return { id: `order-${Date.now()}`, items: [], total: 0, ...overrides, }; } test("calculates total", () => { const order = createTestOrder({ items: [{ price: 10 }, { price: 20 }], }); expect(calculateTotal(order)).toBe(30); }); test("handles empty order", () => { const order = createTestOrder(); expect(calculateTotal(order)).toBe(0); }); ``` ## Framework-Specific Examples ### Jest ```javascript // ✅ Proper test isolation describe("UserService", () => { let userService; beforeEach(() => { userService = new UserService(); }); test("creates user", () => { const user = userService.create({ name: "John" }); expect(user.id).toBeDefined(); }); test("validates user", () => { const result = userService.validate({ name: "" }); expect(result.valid).toBe(false); }); }); ``` ### Cypress ```javascript // ✅ Clean state between tests describe("User workflow", () => { beforeEach(() => { cy.clearLocalStorage(); cy.clearCookies(); cy.visit("/login"); }); it("logs in user", () => { cy.get('[data-cy="username"]').type("testuser"); cy.get('[data-cy="password"]').type("password"); cy.get('[data-cy="login"]').click(); cy.url().should("include", "/dashboard"); }); it("shows error for invalid credentials", () => { cy.get('[data-cy="username"]').type("invalid"); cy.get('[data-cy="password"]').type("invalid"); cy.get('[data-cy="login"]').click(); cy.get('[data-cy="error"]').should("be.visible"); }); }); ``` ### Playwright ```javascript // ✅ Isolated contexts test.describe("Authentication", () => { test("successful login", async ({ page }) => { await page.goto("/login"); await page.fill('[data-testid="username"]', "user"); await page.fill('[data-testid="password"]', "pass"); await page.click('[data-testid="login"]'); await expect(page).toHaveURL("/dashboard"); }); test("failed login", async ({ page }) => { await page.goto("/login"); await page.fill('[data-testid="username"]', "invalid"); await page.fill('[data-testid="password"]', "invalid"); await page.click('[data-testid="login"]'); await expect(page.locator('[data-testid="error"]')).toBeVisible(); }); }); ``` ## When Not To Use It This rule may not be suitable if: - You're testing integration scenarios that require shared state - You're using test frameworks that handle isolation automatically - You have legitimate shared setup that doesn't affect test outcomes In these cases: ```javascript // Disable for legitimate shared state // eslint-disable-next-line test-flakiness/no-test-isolation const sharedTestDatabase = createDatabase(); // Or configure to allow specific patterns { "test-flakiness/no-test-isolation": ["error", { "allowSharedSetup": true, "allowedSharedVariables": ["testDatabase", "mockServer"] }] } ``` ## Related Rules - [no-global-state-mutation](./no-global-state-mutation.md) - Prevents global state modifications - [no-random-data](./no-random-data.md) - Ensures deterministic test data - [no-unconditional-wait](./no-unconditional-wait.md) - Prevents timing-dependent tests ## Further Reading - [Jest - Setup and Teardown](https://jestjs.io/docs/setup-teardown) - [Test Isolation Principles](https://martinfowler.com/bliki/TestIsolation.html) - [xUnit Test Patterns - Fresh Fixture](http://xunitpatterns.com/Fresh%20Fixture.html) - [Cypress - Test Isolation](https://docs.cypress.io/guides/references/best-practices#Having-tests-rely-on-the-state-of-previous-tests) - [Playwright - Test Isolation](https://playwright.dev/docs/browser-contexts)