vi.mock이 안 먹는 이유 — vitest isolate:false와 모듈 캐시
Vitest 실행 시 isolate 옵션이 꺼져 있으면 모듈 캐시가 공유되어 vi.mock이 무력화될 수 있습니다. 테스트 순서에 따라 결과가 달라지는 현상을 해결하려면 모듈 전체를 모킹하는 대신 vi.spyOn을 사용해 객체의 메서드만 교체하는 방식으로 가로채기 지점을 낮추는 것이 효율적입니다

테스트 파일 하나만 돌리면 전부 통과하는데, 전체를 돌리면 같은 테스트가 깨진다. mock 문법을 아무리 들여다봐도 멀쩡하다. isolate: false를 쓰고 있다면 범인은 문법이 아니라 모듈 캐시다.
상황
훅에 알림 분기를 하나 추가하고, 그 훅이 부르는 API 함수를 vi.mock으로 가로채는 테스트를 썼다.
vi.mock("../lib/api", async (importOriginal) => {
const orig = await importOriginal<typeof import("../lib/api")>();
return { ...orig, api: { ...orig.api, toggleStar: vi.fn().mockResolvedValue({}) } };
});단독 실행: 7건 전부 통과. 인접 테스트 파일들과 함께 실행: 7건 중 6건 실패. 기대값은 "추가되었습니다"인데 "실패했습니다"가 나왔다. mock이 안 걸려서 진짜 fetch가 나가고 그게 실패한 것이다.
설정에는 이게 있었다.
// vitest.config.ts
export default defineConfig({
test: {
isolate: false, // 속도를 위해 파일 간 격리를 껐다
},
});문제 — 로드가 끝난 모듈에는 지시할 수 없다
isolate: false는 테스트 파일마다 새 모듈 레지스트리를 만들지 않는다. 실행 시간을 크게 벌어주지만, 모듈 캐시가 파일 사이에 공유된다.
그래서 이런 순서가 만들어진다.
- 다른 테스트 파일이 먼저
useFileMutations.ts를 import 한다. 이때 그 모듈은 진짜api객체를 바인딩한 채 평가되고 캐시에 남는다. - 내 파일이
vi.mock("../lib/api")을 건다. 호이스팅되니 순서 자체는 맞다. - 내 파일이
useFileMutations.ts를 import 한다 — 그런데 이미 평가된 모듈이 캐시에서 그대로 온다. 재평가되지 않으니 mock한 api를 다시 바인딩할 기회가 없다.
vi.mock은 "이 모듈을 앞으로 로드할 때 대신 이걸 줘라"는 지시다. 로드가 이미 끝났으면 지시할 대상이 없다. await import()로 바꿔도 소용없다 — 캐시 히트는 마찬가지다.
고약한 점은 실패가 실행 순서에 의존한다는 것이다. 파일 하나만 돌리면 초록불, CI에서 전체를 돌리면 빨간불. 로컬에서 재현이 안 되니 flaky로 오해하기도 쉽다. 실제로는 완전히 결정적인 실패다 — 순서가 같으면 항상 같은 결과가 나온다.
해결 — 모듈 경계 대신 객체를 갈아끼운다
가로채는 지점을 한 단계 낮춘다. 모듈이 무엇을 반환하느냐를 바꾸는 대신, 이미 존재하는 객체의 메서드를 교체한다.
import { afterEach, beforeEach, vi } from "vitest";
import { api } from "../lib/api";
beforeEach(() => {
vi.spyOn(api, "toggleStar").mockResolvedValue({});
});
afterEach(() => {
vi.restoreAllMocks();
});캐시된 모듈이 참조하는 것도 결국 같은 api 객체다. 그 객체의 속성을 바꾸면 누가 언제 import 했든 보인다. restoreAllMocks로 원본을 되돌리니 다른 파일로 새지도 않는다.
전제가 두 가지 있다.
- 대상 객체가
Object.freeze되어 있지 않을 것. - 호출부가 속성 접근으로 부를 것.
api.toggleStar(...)는 되지만, 모듈 최상단에서const { toggleStar } = api로 미리 떼어놓으면 그 바인딩은 spy 이전 값이라 교체가 보이지 않는다.
판별법
mock이 의심스러우면 이 두 가지를 비교한다.
# 1) 파일 하나만
npx vitest run tests/my-feature.test.ts
# 2) 인접 파일들과 함께
npx vitest run tests/my-feature.test.ts tests/neighbor-a.test.ts tests/neighbor-b.test.ts- 1은 통과, 2는 실패 → 모듈 캐시 문제. mock 문법은 건드릴 필요 없다.
- 둘 다 실패 → 그때 문법을 본다.
isolate: true로 되돌리면 물론 해결되지만, 그 설정은 대개 실행 시간 때문에 의도적으로 꺼둔 것이다. 스위트 전체의 격리를 되살리는 것보다 테스트 하나의 가로채기 지점을 낮추는 편이 비용이 훨씬 싸다.
정리
isolate: false는 모듈 캐시를 파일 간에 공유한다.- 다른 파일이 대상 모듈을 먼저 로드했으면
vi.mock은 무력화된다 — 재평가가 일어나지 않기 때문이다. - 증상은 "혼자 돌리면 통과, 같이 돌리면 실패". flaky가 아니라 순서 의존이다.
vi.spyOn(obj, "method")+afterEach(vi.restoreAllMocks)는 캐시와 무관하게 동작하고 누출도 없다.