Every test must be independent — no shared mutable state, no execution order dependencies. When tests share state, they pass in isolation but fail together (or worse, fail randomly), creating flaky CI that wastes hours of debugging time.
Shared mutable state between tests creates order-dependent results: test A modifies a shared variable, test B reads it and passes, but when test B runs first, it fails. These flaky tests erode team trust in the test suite. Developers start ignoring failures ("it's probably just flaky") and stop running tests locally. The test suite becomes a liability that slows development instead of catching bugs.
BeforeMerge scans your pull requests against this rule and 3+ others. Get actionable feedback before code ships.
Test isolation means every test creates its own state, runs independently, and cleans up after itself. When tests share mutable state, the results depend on execution order:
These failures are non-deterministic. They pass locally but fail in CI (or vice versa). They pass alone but fail in a suite. Developers spend hours debugging a test that "should work," only to discover it depends on side effects from another test.
Flaky tests are worse than no tests. They train developers to ignore failures.
Every test must:
// BAD: shared mutable state between tests
let testUser: User;
describe("UserService", () => {
it("creates a user", async () => {
// This test creates state that other tests depend on
testUser = await userService.createUser({ name: "Alice", email: "a@b.com" });
expect(testUser.id).toBeDefined();
});
it("finds the user by email", async () => {
// This test DEPENDS on the previous test running first
const found = await userService.findByEmail("a@b.com");
expect(found?.id).toBe(testUser.id); // fails if run independently
});
it("updates the user", async () => {
// Also depends on test 1 — and modifies shared state
await userService.updateUser(testUser.id, { name: "Bob" });
testUser.name = "Bob"; // mutates shared state!
});
});// GOOD: every test creates its own state
describe("UserService", () => {
it("creates a user", async () => {
const user = await userService.createUser({
name: "Alice",
email: `alice-${randomId()}@test.com`,
});
expect(user.id).toBeDefined();
});
it("finds a user by email", async () => {
// Creates its own user — doesn't depend on other tests
const email = `find-test-${randomId()}@test.com`;
await userService.createUser({ name: "Alice", email });
const found = await userService.findByEmail(email);
expect(found?.email).toBe(email);
});
it("updates a user", async () => {
// Creates its own user
const user = await userService.createUser({
name: "Alice",
email: `update-test-${randomId()}@test.com`,
});
await userService.updateUser(user.id, { name: "Bob" });
const updated = await userService.findById(user.id);
expect(updated?.name).toBe("Bob");
});
});Look for shared mutable variables declared outside test blocks:
grep -n "^let \|^var " --include="*.test.ts" --include="*.spec.ts" -r src/Also look for tests that don't have their own setup and rely on beforeAll to create shared state.
let variables with per-test setupcreateTestUser(), createTestOrder()beforeEach for setup and afterEach for cleanup instead of beforeAll/afterAllvitest --sequence.shuffle