트러블슈팅

Vitest clearAllMocks 뒤에도 남은 mock 구현이 다음 테스트를 깨뜨린 이유

개개발 노하우 노트2026년 11월 11일 · 4분 읽기

Vitest로 테스트를 짜다 보면 각 테스트 앞에서 vi.clearAllMocks()를 호출해 mock을 "초기화"했다고 생각하기 쉽다. 그런데 앞 테스트에서 바꾼 mock 구현이 그대로 남아 뒤 테스트의 결과를 바꿔 버리는 일이 생겼다. 테스트를 하나만 돌리면 통과하고 같이 돌리면 실패하는, 순서 의존 테스트를 겪고 있다면 참고할 만한 사례다.

상황

자체 호스팅한 에이전트 오케스트레이터에 작업을 넘기는 디스패치 모듈을 테스트하고 있었다. 디스패치 전에 "대상 에이전트가 실제로 있는지" 확인하는 함수를 호출하는데, 테스트에서는 이 함수를 모듈 mock으로 대체했다. 기본은 성공(resolve)이고, 에이전트가 없는 상황을 시험하는 테스트에서만 mockRejectedValue로 실패하게 바꿨다.

테스트 파일의 beforeEach에는 vi.clearAllMocks()가 들어 있었다. 매 테스트가 깨끗한 mock에서 시작한다고 믿고 있었다.

증상

새로 추가한 에이전트 존재 확인 테스트 바로 뒤에 있는 펜싱(중복 실행 방지) 테스트가 실패했다. 기대 결과는 작업을 건너뛰었다는 skipped였는데, 실제로는 재시도 가능한 실패를 뜻하는 retryable_failed가 나왔다.

처음에는 새 검증 호출이 끼어들면서 DB 호출 순서가 바뀌어 펜싱 로직이 다르게 동작한다고 의심했다. 그래서 DB mock의 호출 순서부터 살폈지만 원인은 거기 없었다. 펜싱 테스트를 단독으로 돌리면 통과한다는 점이 결정적 단서였다.

원인

Vitest 공식 문서는 세 가지 초기화 함수를 분명히 구분한다.

  • mockClear / vi.clearAllMocks: 호출 기록만 지운다. 문서 표현으로는 mock 구현에 영향을 주지 않는다.
  • mockReset / vi.resetAllMocks: 호출 기록을 지우고 구현도 되돌린다. vi.fn()으로 만든 mock은 undefined를 돌려주는 빈 함수가 되고, vi.fn(impl)로 만든 mock은 처음 넘긴 impl로 돌아간다.
  • mockRestore / vi.restoreAllMocks: mockReset이 하는 일에 더해 vi.spyOn으로 감싼 원래 메서드를 복원한다.

앞 테스트가 존재 확인 mock의 구현을 reject로 바꿨고, beforeEach의 vi.clearAllMocks()는 호출 기록만 지웠다. 그래서 다음 테스트에서도 존재 확인이 계속 실패했고, 디스패치 코드는 이를 "일시적 오류"로 분류해 retryable_failed를 돌려줬다. 펜싱 로직까지 가지도 못한 것이다.

해결

beforeEach에서 이 mock의 성공 기본 구현을 명시적으로 다시 설정했다.

```ts

beforeEach(() => {

vi.clearAllMocks()

// 일부 테스트가 reject로 바꾸므로 기본 성공 구현을 매번 복원

vi.mocked(assertAgentExists).mockResolvedValue(undefined)

})

```

vi.resetAllMocks()로 바꾸는 방법도 있지만 이 파일처럼 모듈 mock의 팩토리에서 vi.fn()으로 만든 mock이 많으면 모든 mock이 빈 함수가 되어 다른 테스트가 줄줄이 깨질 수 있다. 그래서 "테스트가 바꾸는 mock만 기본값을 다시 넣는다"는 좁은 해결을 골랐다. 수정 뒤 표적 테스트와 전체 테스트가 모두 통과했다.

어떤 방식을 고를까

  • 테스트가 구현을 바꾸지 않고 호출 횟수만 본다면 clearAllMocks로 충분하다.
  • mock을 vi.fn(impl) 형태로 기본 구현과 함께 만들었다면 resetAllMocks가 그 impl로 되돌려 주므로 깔끔하다.
  • 기본 구현 없이 vi.fn()으로 만든 mock이 섞여 있다면 resetAllMocks는 과하다. 바꾼 mock의 기본값을 beforeEach에서 다시 설정하는 편이 안전하다.
  • 설정 파일의 clearMocks, mockReset, restoreMocks 옵션으로 매 테스트 뒤 초기화를 자동화할 수 있다. 팀 차원에서 하나로 정해 두면 파일마다 다른 습관이 섞이는 것을 막는다.

다시 겪지 않으려면

  • 단독 실행은 통과하고 함께 실행하면 실패한다면 먼저 테스트 사이에 남는 상태(mock 구현, 전역 변수, 타이머)를 의심한다.
  • clearAllMocks는 호출 기록만 지운다는 점을 기억한다. 이름만 보고 전체 초기화로 오해하지 않는다.
  • mockRejectedValue, mockImplementation처럼 지속 구현을 바꾸는 테스트가 있으면 beforeEach에서 기본값을 다시 설정한다.
  • 한 번만 바꾸면 되는 경우는 mockRejectedValueOnce처럼 Once 계열을 써서 다음 테스트로 새지 않게 한다.
  • 오류 분류 로직(재시도 가능 여부)이 있는 코드는 mock 실패가 엉뚱한 분류로 이어질 수 있으니 기대 결과와 함께 실패 사유도 검증한다.

참고 자료