vitest에서 사용자 지정 assertion을 추가하여 Zod 스키마와 Response 객체를 비교하기

import { expect } 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 ExpectationResult
  },
})

vitest.d.ts 파일에서 CustomMatchers 인터페이스를 확장하여 TypeScript와의 통합성을 유지.

import type { ZodTypeAny } from 'zod'

interface CustomMatchers<R = unknown> {
  toMatchSchema(schema: ZodTypeAny): Promise<R>
}

todoResponse의 응답 데이터가 todoSchema에 정의된 Zod 스키마와 일치하는지 확인한다.

test('todo', async () => {
  expect(todoResponse.ok).toBeTruthy()
  expect(todoResponse).toMatchSchema(todoSchema)
})

#317

Vitest/Jest Fake Timers로 시간 제어

describe('time-dependent tests', () => {
  beforeEach(() => {
    vi.useFakeTimers() // jest.useFakeTimers('modern')
  })

  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 등 모킹.

#347

ResizeObserver Mock (Vitest, ES Module 환경)

vi.hoisted()로 모킹 함수 미리 선언 후 vi.mock()에서 사용:

import { vi } from 'vitest'

const { mockResizeObserver, MockResizeObserver } = vi.hoisted(() => {
  let observers: { callback: ResizeObserverCallback; observer: any }[] = []

  const MockResizeObserver = vi.fn().mockImplementation((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는 배열로 추적.

#514

Vitest 모킹: vi.mocked() vs vi.hoisted()

vi.mocked()는 타입만 제공, 실제 모킹 구현체는 별도로 필요.

// ❌ 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로 통합 테스트 고려.

#515