vitest에서 사용자 지정 assertion을 추가하여 Zod 스키마와 Response 객체를 비교하기
// vitest 5.0 기준
import { expect } from 'vitest'
import type { MatcherResult } from 'vitest'
import type { ZodTypeAny } from 'zod'
expect.extend({
/**
* @param received 테스트할 Response 객체
* @param schema 검증할 Zod 스키마
*/
async toMatchSchema(received: Response, schema: ZodTypeAny) {
const response = await received.json()
const result = await schema.safeParseAsync(response)
return {
message: () => '',
pass: result.success,
} satisfies MatcherResult
},
})
vitest.d.ts 에서 vitest 모듈의 Matchers 인터페이스를 확장한다. 비동기 매처라 반환 타입은 R 이 아니라 Promise<void>1.
import 'vitest'
import type { ZodTypeAny } from 'zod'
declare module 'vitest' {
interface Matchers<R, T> {
toMatchSchema: (schema: ZodTypeAny) => Promise<void>
}
}
todoResponse의 응답 데이터가 todoSchema에 정의된 Zod 스키마와 일치하는지 확인한다. 매처가 async라 테스트에서 await 한다1.
test('todo', async () => {
expect(todoResponse.ok).toBeTruthy()
await expect(todoResponse).toMatchSchema(todoSchema)
})
Footnotes
-
Extending Matchers | Guide | Vitest: “If a matcher implementation is asynchronous, declare its return type as
Promise<void>instead ofRand don’t forget toawaitit in the test” ↩ ↩2
Vitest/Jest Fake Timers로 시간 제어
describe('time-dependent tests', () => {
beforeEach(() => {
vi.useFakeTimers() // jest.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers() // jest.useRealTimers()
})
it('특정 시간에 동작 확인', () => {
vi.setSystemTime(new Date(2000, 1, 1, 13)) // 13시 설정
expect(purchase()).toEqual({ message: 'Success' })
})
it('영업시간 외 동작', () => {
vi.setSystemTime(new Date(2000, 1, 1, 19)) // 19시 설정
expect(purchase()).toEqual({ message: 'Error' })
})
})
@sinonjs/fake-timers 기반. Date, setTimeout 등 모킹.1
Footnotes
-
vi - Vitest: “The implementation is based internally on
@sinonjs/fake-timers.” / The Jest Object: “Fake timers implementation is backed by@sinonjs/fake-timers.” ↩
ResizeObserver Mock (Vitest, ES Module 환경)
vi.mock() 은 파일 맨 위로 끌어올려져 바깥 변수를 못 본다 — 그래서 vi.hoisted() 로 모킹 함수를 미리 선언해 넘긴다.1
import { vi } from 'vitest'
const { mockResizeObserver, MockResizeObserver } = vi.hoisted(() => {
let observers: { callback: ResizeObserverCallback; observer: any }[] = []
// new 로 불리므로 화살표 함수가 아니라 function — 화살표면 "is not a constructor"
const MockResizeObserver = vi.fn().mockImplementation(function (callback) {
const observer = {
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}
observers.push({ callback, observer })
return observer
})
return {
MockResizeObserver,
mockResizeObserver: {
triggerResize: (entries: ResizeObserverEntry[], index = 0) => {
observers[index]?.callback(entries, observers[index].observer)
},
reset: () => {
observers = []
},
},
}
})
vi.mock('resize-observer-polyfill', () => ({ default: MockResizeObserver }))
// 테스트에서 사용
beforeEach(() => mockResizeObserver.reset())
it('should handle resize', () => {
const mockEntry = {
target: document.createElement('div'),
contentRect: { width: 100 },
}
mockResizeObserver.triggerResize([mockEntry])
})
핵심: ES Module에서는 vi.hoisted() 필수. 여러 observer는 배열로 추적.
Footnotes
-
vi.mock - Vitest: “The call to
vi.mockis hoisted, so it doesn’t matter where you call it. … If you need to reference some variables outside of its scope, you can define them insidevi.hoistedand reference them insidevi.mock.” / vi.spyOn: “If you provide an arrow function, you will get<anonymous> is not a constructorerror when the mock is called.” ↩
Vitest 모킹: vi.mocked() vs vi.hoisted()
vi.mocked()는 타입만 제공, 실제 모킹 구현체는 별도로 필요.1
// ❌ mockImplementation이 undefined
const mockUseSize = vi.mocked(useSize)
// ✅ vi.hoisted() 사용 (추천)
const mockUseSize = vi.hoisted(() => vi.fn())
vi.mock('ahooks', () => ({ useSize: mockUseSize }))
beforeEach(() => {
mockUseSize.mockImplementation(() => ({ width: 100, height: 20 }))
})
DOM 속성 모킹 (scrollWidth/clientWidth):
beforeEach(() => {
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {
configurable: true,
get() {
return 150
},
})
})
afterEach(() => {
// 원래 속성 복원
})
DOM 측정이 복잡하면 Playwright/Cypress로 통합 테스트 고려.
Footnotes
-
vi.mocked - Vitest: “Type helper for TypeScript. Just returns the object that was passed.” ↩