폼 제출 시 FormData 테스트

test('폼 제출 시 formData 확인', () => {
  const handleSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.target as HTMLFormElement)
    const data = Object.fromEntries(formData.entries())
    expect(data).toEqual({ username: 'testuser', password: 'password' })
  })

  render(<LoginForm onSubmit={handleSubmit} />)

  fireEvent.change(screen.getByPlaceholderText('아이디'), {
    target: { value: 'testuser' },
  })
  fireEvent.change(screen.getByPlaceholderText('비밀번호'), {
    target: { value: 'password' },
  })
  fireEvent.click(screen.getByRole('button', { name: '로그인' }))

  expect(handleSubmit).toHaveBeenCalled()
})
#326

AuthContext 테스트 패턴

// localStorage 함수 테스트
vi.spyOn(Storage.prototype, 'getItem').mockImplementation((key) => {
  if (key === 'auth_token') return 'test_token'
  return null
})

// AuthProvider 상태 테스트
const TestComponent = () => {
  const { token, login, logout } = useAuth()

  return (
    <div>
      <p data-testid="token">{token || 'null'}</p>
      <button onClick={() => login('test_token')}>Login</button>
      <button onClick={logout}>Logout</button>
    </div>
  )
}

// useAuth 훅 테스트
it('AuthProvider 외부에서 호출하면 에러', () => {
  expect(() => renderHook(() => useAuth())).toThrowError()
})

it('AuthProvider 내부에서 정상 동작', () => {
  const wrapper = ({ children }) => <AuthProvider>{children}</AuthProvider>
  const { result } = renderHook(() => useAuth(), { wrapper })
  expect(result.current).toHaveProperty('token', null)
})
#328

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

Jest setupFiles vs setupFilesAfterEnv - 실행 시점이 다르다.

  • setupFiles: 테스트 프레임워크 설치 . Jest 전역 객체 없음. 환경 변수, 폴리필용.
  • setupFilesAfterEnv: 테스트 프레임워크 설치 . jest.setTimeout(), 커스텀 matcher, 전역 beforeEach용.

process.env는 setupFiles에서. 모듈이 import 시점에 환경 변수를 읽기 때문에 setupFilesAfterEnv에서 설정하면 이미 늦다.

// jest.config.js
{ setupFiles: ['./jest.env.js'], setupFilesAfterEnv: ['./jest.setup.js'] }

// jest.env.js - 환경 변수
process.env.API_URL = 'http://test-api.example.com'

// jest.setup.js - Jest API 활용
import '@testing-library/jest-dom'
beforeEach(() => { jest.clearAllMocks() })
#505

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

RTL에서 overflow scroll 상태 테스트

// scrollWidth > clientWidth면 가로 스크롤 필요
const isOverflowScrollable = (el) => ({
  horizontal: el.scrollWidth > el.clientWidth,
  vertical: el.scrollHeight > el.clientHeight,
})

test('overflow 확인', () => {
  render(<ScrollableComponent parentWidth={300} childWidth={500} />)

  const parent = screen.getByTestId('parent-container')

  expect(isOverflowScrollable(parent).horizontal).toBe(true)
})

test('동적 크기 변경', () => {
  const { rerender } = render(<Comp parentWidth={400} childWidth={300} />)

  expect(isOverflowScrollable(screen.getByTestId('parent')).horizontal).toBe(
    false
  )

  rerender(<Comp parentWidth={400} childWidth={600} />)

  expect(isOverflowScrollable(screen.getByTestId('parent')).horizontal).toBe(
    true
  )
})

CAUTION

DOM 완전 렌더링 후 측정해야 정확. getBoundingClientRect()는 실제 크기 반환.

#521