- Published on
실무에 바로 적용하는 프런트엔드 테스트 1부
- Authors
- Name
- Hyo814
테스트가 당신의 코드에 미치는 영향
테스트란?
- 애플리케이션의 품질과 안정성을 높이기 위해 사전에 결함을 찾아내고 수정하는 행위.
- 주로 특정 모듈(특히 컴포넌트)이 사양에 잘 동작하는지 자동화된 테스트로 검증.
테스트의 영향
- 개발 비용 증가 가능성.
테스트 코드의 효과
- 좋은 설계에 대한 사고를 돕는다.
- 결합도: 어떤 모듈이 다른 모듈에 의존하는 정도.
- 결합도가 높으면:
- 한 모듈을 수정할 때 다른 모듈에도 영향을 줄 가능성.
- 특정 기능만 따로 검증하기 어려움.
- 복잡한 구조로 인해 테스트가 누락될 가능성.
- 여러 테스트 코드를 계속 수정해야 하는 문제가 발생.
- 빠르고 안정적인 리팩토링 가능.
- 리팩토링: 결과의 변경 없이 코드의 구조를 재조정.
- 애플리케이션 이해를 돕는 문서 역할.
- 잘 작성된 테스트 코드는 코드의 동작 원리와 의도를 명확히 설명해주는 문서로 활용 가능.
올바른 테스트 작성을 위한 규칙
인터페이스를 기준으로 테스트 작성하기
이유
- 서로 다른 클래스 또는 모듈 간의 상호작용을 검증하기 위해 인터페이스 중심으로 테스트를 작성해야 함.
- 내부 구현을 기준으로 테스트를 작성하면 캡슐화를 위반하고 유지보수성이 낮아짐.
내부 구현에 대한 테스트의 문제점
- 변경되는 상태가 많을 경우:
- 테스트 코드에서 일일이 상태를 직접 변경해야 하며, 변경의 이유와 상황이 명확히 드러나지 않음.
- 테스트 코드의 가독성 저하:
- 내부 상태나 변수 값을 기준으로 검증하므로, 테스트 코드만 보고 어떤 것을 검증하는지 한눈에 파악하기 어려움.
- 구현에 종속적인 테스트 코드:
- 내부 구현을 검증하려다 보니 변수명이나 상태 변경에 따라 테스트 코드를 전면 수정해야 하는 상황 발생.
- 이는 캡슐화를 위반하는 결과를 초래.
단일 책임 원칙
- 원칙: 모든 클래스는 하나의 책임만 가져야 하며, 그와 관련된 내용을 캡슐화하여 유지보수와 변경에 강한 코드를 작성해야 함.
- 이를 통해 코드와 테스트 모두 변경에 견고해짐.
단위 테스트란 무엇일까?
단위 테스트(Unit Test)
정의
- 앱에서 테스트 가능한 가장 작은 소프트웨어(단일 함수, 단일 클래스, 단일 컴포넌트 등)를 실행하여 예상대로 동작하는지 확인하는 테스트.
검증 방식
- 결과값: 함수나 클래스의 출력값 검증.
- 상태: 내부 상태의 변경 확인.
- 행위: 호출된 동작의 실행 여부 검증.
Arrange-Act-Assert 테스트 작성 패턴
- Arrange: 테스트를 위한 환경을 설정.
- 필요한 데이터, 의존성, 초기 상태 준비.
- Act: 테스트 대상 동작을 실행.
- 함수 호출, 이벤트 발생 등.
- Assert: 예상한 결과와 실제 결과를 비교하여 검증.
- 동작이 의도한 대로 실행되었는지 확인.
테스트 환경과 매처
Vitest 주요 기능 및 특징
주요 특징
- Vite의 설정, 트랜스포머, 리졸버, 플러그인을 테스트 환경에서 동일하게 사용 가능.
- 스마트하고 즉각적인 워치 모드(HMR과 유사).
- Vue, React, Svelte 등 다양한 프레임워크의 컴포넌트 테스트 지원.
- TypeScript/JSX 기본 지원.
- ESM(모듈 시스템) 지원 및 최상위
await
사용 가능. - Tinypool을 활용한 멀티스레딩 테스트.
- Tinybench로 벤치마킹 지원.
- 필터링, 타임아웃, 병렬 실행 등 다양한 테스트 옵션 제공.
- Jest 호환 스냅샷 및 Chai 내장(Jest 호환 API 포함).
- DOM 목킹(happy-dom, jsdom) 및 브라우저 모드 지원.
- 코드 커버리지(v8, istanbul) 및 Rust와 유사한 소스 내 테스트 제공.
- 타입 테스트 및 샤딩 지원.
테스트 실행
- 기본적으로 개발 환경에서는 워치 모드, CI 환경에서는 실행 모드로 동작.
vitest watch
: 변경된 테스트만 다시 실행.vitest run
: 전체 테스트 실행.
멀티스레딩 테스트
- 기본적으로 Tinypool(경량 스레드 풀)을 사용하여 테스트 파일을 병렬로 실행.
-pool=threads
옵션으로 Node의 worker_threads 사용 가능.
테스트 작성 패턴
- Arrange (준비):
- 테스트를 실행하기 위한 초기 환경 구성.
- 예: 객체 생성, 의존성 설정.
- Act (실행):
- 테스트 대상 동작 실행.
- 예: 함수 호출, 이벤트 발생.
- Assert (검증):
- 예상한 결과와 실제 결과를 비교하여 검증.
주요 기능
병렬 테스트 실행
.concurrent
키워드를 사용하여 테스트를 병렬 실행 가능.
import { describe, it } from 'vitest' describe.concurrent('병렬 테스트', () => { it('테스트 1', async () => { /* ... */ }) it('테스트 2', async () => { /* ... */ }) })
스냅샷 테스트
- Jest 호환 스냅샷 지원.
import { expect, it } from 'vitest' it('렌더링 확인', () => { const result = render() expect(result).toMatchSnapshot() })
목킹(Mock)
Tinyspy
를 내장하여 Jest와 유사한 목킹 API 지원.
import { vi, expect } from 'vitest' const mockFn = vi.fn() mockFn('테스트', 1) expect(vi.isMockFunction(mockFn)).toBe(true) expect(mockFn.mock.calls[0]).toEqual(['테스트', 1])
브라우저 테스트
- 브라우저 환경에서 컴포넌트 테스트 실행 가능(
happy-dom
,jsdom
설치 필요).
// vitest.config.ts export default defineConfig({ test: { environment: 'happy-dom', }, })
- 브라우저 환경에서 컴포넌트 테스트 실행 가능(
코드 커버리지
v8
및istanbul
기반의 코드 커버리지 지원.
{ "scripts": { "test": "vitest", "coverage": "vitest run --coverage" } }
타입 테스트
expect-type
패키지를 통해 타입 관련 회귀 테스트 지원.
import { assertType, expectTypeOf, test } from 'vitest' test('타입 테스트', () => { expectTypeOf(mount).toBeFunction() })
유용한 옵션
워치 모드:
vitest --watch
(변경된 테스트만 실행).테스트 병렬 처리:
-pool=threads
또는-pool=forks
로 설정 가능.커버리지 옵션:
import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { enabled: true, provider: 'istanbul', }, }, })
setup과 teardown
모든 테스트는 독립적으로 실행 되어야 한다.
setup : 테스트를 실행하기 전 수행해야하는 작업
teardown : 테스트를 실행 한 뒤 수행해야하는 작업
Setup과 Teardown
테스트의 생명주기에 따라 특정 코드를 실행할 수 있도록 돕는 함수들로, 반복적인 설정 및 정리 코드를 피할 수 있습니다.
이 함수들은 현재 컨텍스트에 적용되며, 컨텍스트는 파일 전체 또는 describe
블록 내부로 제한됩니다.
beforeEach
1. - 역할: 각 테스트 실행 전에 실행할 코드를 등록.
- 타입:
beforeEach(fn: () => Awaitable<void>, timeout?: number)
- 테스트가 실행되기 전에 실행되며,
Promise
를 반환하면 테스트는 해당Promise
가 완료될 때까지 대기합니다. - 기본 타임아웃: 5초.
import { beforeEach } from 'vitest'
beforeEach(async () => {
await stopMocking() // 모의 동작 초기화
await addUser({ name: 'John' }) // 테스트 데이터 추가
})
Cleanup 기능 지원: 테스트 후 정리 작업을 위해 정리 함수 반환 가능.
beforeEach(async () => {
await prepareSomething()
return async () => {
await resetSomething() // 각 테스트 후 정리 작업
}
})
afterEach
2. - 역할: 각 테스트 실행 후 실행할 코드를 등록.
- 타입:
afterEach(fn: () => Awaitable<void>, timeout?: number)
- 테스트가 끝난 후 실행되며,
Promise
를 반환하면 해당Promise
가 완료될 때까지 대기합니다. - 기본 타임아웃: 5초.
import { afterEach } from 'vitest'
afterEach(async () => {
await clearTestingData() // 테스트 데이터 초기화
})
beforeAll
3. - 역할: 컨텍스트 내 모든 테스트 실행 전에 한 번 실행.
- 타입:
beforeAll(fn: () => Awaitable<void>, timeout?: number)
- 테스트 실행 전에 한 번만 실행되며,
Promise
를 반환하면 해당Promise
가 완료될 때까지 대기합니다. - 기본 타임아웃: 5초.
import { beforeAll } from 'vitest'
beforeAll(async () => {
await startMocking() // 모든 테스트 실행 전 모의 동작 설정
})
Cleanup 기능 지원: 테스트가 끝난 후 정리 작업을 위해 정리 함수 반환 가능.
beforeAll(async () => {
await startMocking()
return async () => {
await stopMocking() // 모든 테스트 후 정리 작업
}
})
afterAll
4. - 역할: 컨텍스트 내 모든 테스트 실행 후 한 번 실행.
- 타입:
afterAll(fn: () => Awaitable<void>, timeout?: number)
- 모든 테스트 실행 후 한 번만 실행되며,
Promise
를 반환하면 해당Promise
가 완료될 때까지 대기합니다. - 기본 타임아웃: 5초.
import { afterAll } from 'vitest'
afterAll(async () => {
await stopMocking() // 모든 테스트 실행 후 모의 동작 초기화
})
Testing Library
Testing Library의 Queries
개요
Queries는 Testing Library에서 제공하는 DOM 요소를 찾는 방법입니다.
다양한 Query 유형은 다음과 같은 동작 차이를 가집니다:
- get: 요소를 찾지 못하면 오류를 던집니다.
- query: 요소를 찾지 못하면
null
을 반환합니다. - find: Promise를 반환하며, 요소를 찾기 위해 재시도 후 결과를 반환합니다.
Query의 활용
- 사용자 상호작용 시뮬레이션
user-event
또는Events API
를 사용해 상호작용 테스트.
- DOM 변경 감지
- DOM 변화는
waitFor
또는findBy
를 통해 비동기적으로 처리 가능.
- DOM 변화는
- 특정 컨테이너에서만 Query 수행
within
을 사용해 특정 컨테이너 내부의 요소만 검색 가능.
Query의 유형
단일 요소 관련
getBy...
- 요소를 찾지 못하면 오류를 던짐.
- 다수의 요소가 매칭될 경우 오류를 던짐.
queryBy...
- 요소를 찾지 못하면
null
을 반환. - 다수의 요소가 매칭되면 오류를 던짐.
- 요소를 찾지 못하면
findBy...
- Promise를 반환하며, 요소를 찾기 위해 비동기적으로 재시도.
- 요소를 찾지 못하거나 다수 매칭 시 오류를 던짐.
다수 요소 관련
getAllBy...
- 매칭되는 모든 요소 배열 반환.
- 요소를 찾지 못하면 오류를 던짐.
queryAllBy...
- 매칭되는 모든 요소 배열 반환.
- 요소를 찾지 못하면 빈 배열(
[]
) 반환.
findAllBy...
- Promise를 반환하며, 매칭되는 모든 요소 배열을 비동기로 반환.
Query 우선순위
사용자 경험을 반영한 우선순위
- 접근성 중심 Queries
getByRole
: 버튼, 링크 등 접근성 트리에 노출된 요소 검색.getByLabelText
: 라벨과 연결된 입력 필드 검색. (폼 테스트에 적합)getByPlaceholderText
: 플레이스홀더 기반 검색. (라벨이 없는 경우 사용)getByText
: 텍스트 콘텐츠 기반 검색.getByDisplayValue
: 입력 필드에 표시된 현재 값 검색.
- HTML5 및 ARIA 준수 Queries
getByAltText
: 이미지와 같은 요소의alt
속성 검색.getByTitle
: 요소의title
속성 검색.
- Test ID 기반 Queries
getByTestId
: 테스트를 위한 고유 식별자 기반 검색.
Query 사용 예시
기본 Query 사용:
import { render, screen } from '@testing-library/react'
test('로그인 폼 테스트', () => {
render(<Login />)
const input = screen.getByLabelText('Username')
// 이벤트 및 검증...
})
TextMatch
옵션 활용:
// 텍스트 검색
screen.getByText('Hello World') // 정확히 일치
screen.getByText(/World/i) // 정규식 기반 검색 (대소문자 무시)
// 커스텀 함수 사용
screen.getByText((content) => content.startsWith('Hello'))
DOM 변경 감지:
import { render, screen, waitFor } from '@testing-library/react'
test('비동기 테스트', async () => {
render(<AsyncComponent />)
await waitFor(() => expect(screen.getByText('완료')).toBeInTheDocument())
})
추가 도구
- Browser Extension
- Chrome 확장 프로그램인 Testing Playground를 사용하면 테스트 요소 선택에 적합한 Query를 추천받을 수 있습니다.
- Playground
- Testing Playground를 통해 Query 실습 가능.
Mocking 함수와 객체
Mocking은 테스트 중 함수의 동작을 감시(spy)하거나, 글로벌 및 환경 변수를 대체해 원하는 동작을 재현할 수 있는 강력한 도구입니다.
Mocking 주요 기능
vi.fn
1. - 타입:
(fn?: Function) => Mock
- 함수에 spy를 생성하며, 호출 기록(인자, 반환값, 인스턴스)을 저장.
- Mock 함수의 동작을 조작 가능.
const getApples = vi.fn(() => 0)
getApples()
expect(getApples).toHaveBeenCalled()
expect(getApples).toHaveReturnedWith(0)
getApples.mockReturnValueOnce(5)
expect(getApples()).toBe(5)
expect(getApples).toHaveNthReturnedWith(2, 5)
2. Mock 관리 메서드
vi.clearAllMocks
: 모든 spy의 호출 기록 초기화 (구현은 유지).vi.resetAllMocks
: 모든 spy의 호출 기록 초기화 및 구현을 빈 함수로 리셋.vi.restoreAllMocks
: 모든 spy의 호출 기록 초기화 및 원래 구현 복원.
vi.spyOn
3. - 타입:
<T, K extends keyof T>(object: T, method: K, accessType?: 'get' | 'set') => MockInstance
- 객체의 메서드나 getter/setter에 spy를 생성.
- Mock 구현 변경 가능.
let apples = 0
const cart = {
getApples: () => 42,
}
const spy = vi.spyOn(cart, 'getApples').mockImplementation(() => apples)
apples = 1
expect(cart.getApples()).toBe(1)
expect(spy).toHaveBeenCalled()
expect(spy).toHaveReturnedWith(1)
vi.stubEnv
)
4. 환경 변수 대체 (- 타입:
<T extends string>(name: T, value: string | boolean | undefined) => Vitest
process.env
와import.meta.env
의 값을 변경.vi.unstubAllEnvs
로 원래 값 복원 가능.
vi.stubEnv('NODE_ENV', 'production')
expect(process.env.NODE_ENV).toBe('production')
vi.unstubAllEnvs()
expect(process.env.NODE_ENV).toBe('development')
vi.stubGlobal
)
5. 글로벌 변수 대체 (- 타입:
(name: string | number | symbol, value: unknown) => Vitest
- 글로벌 변수(
globalThis
,window
)의 값을 변경. vi.unstubAllGlobals
로 원래 값 복원 가능.
vi.stubGlobal('innerWidth', 100)
expect(innerWidth).toBe(100)
vi.unstubAllGlobals()
expect(globalThis.innerWidth).toBeUndefined()
Mocking 유용한 팁
vi.restoreAllMocks
활용- 테스트 후 원래 메서드 구현 복원.
Browser Mode에서의 제한
Browser Mode에서는
vi.spyOn
으로 직접 메서드 스파이 생성이 불가능.대신
vi.mock
으로 파일 내 모든 메서드를 스파이 가능:vi.mock('./src/file.js', { spy: true })
Mock 예제 요약
함수 Mock
const mockFn = vi.fn(() => 'Hello')
expect(mockFn()).toBe('Hello')
expect(mockFn).toHaveBeenCalled()
객체 메서드 Mock
const obj = { greet: () => 'Hi' }
const spy = vi.spyOn(obj, 'greet').mockReturnValue('Hello')
expect(obj.greet()).toBe('Hello')
vi.restoreAllMocks()
환경 변수 Mock
vi.stubEnv('MODE', 'test')
expect(import.meta.env.MODE).toBe('test')
vi.unstubAllEnvs()
글로벌 변수 Mock
vi.stubGlobal('customVar', 123)
expect(globalThis.customVar).toBe(123)
vi.unstubAllGlobals()
단위 테스트 대상 선정하기
- state나 로직처리 없이 UI만 그리는 컴포넌트는 검증하지 않는다.
- 해당 검증은 스토리북과 같은 도구를 통해 검증
- 간단한 로직 처리만 하는 컴포넌트는 상위 컴포넌트의 통합 테스트에서 검증한다.
- 공통 유틸 함수는 단위 테스트로 검증한다.
- 다른 모듈과의 의존성의 없다.
- 여러 곳에서 사용되기 때문에 검증을 통해 안정성을 높인다.
모듈 모킹
모듈 모킹
1. 모킹이란?
모킹(Mocking)은 실제 모듈이나 객체와 동일한 동작을 하는 **모의 객체(Mock)**를 만들어 테스트에서 이를 대체하는 것을 말합니다.
주요 목적은 테스트를 독립적으로 수행하고, 외부 의존성이나 복잡한 연산을 제거하여 테스트의 신뢰성과 효율성을 높이는 데 있습니다.
2. 모킹의 특징
외부 모듈 모킹
vi.mock()
을 사용하여 특정 모듈의 동작을 가짜로 구현.- 외부 모듈의 검증은 배제하고, 테스트 대상 컴포넌트에만 집중 가능.
- 단, 외부 모듈은 별도로 검증되어야 함.
// 외부 모듈 모킹 예시 import { fetchData } from './api' vi.mock('./api', () => ({ fetchData: vi.fn(() => Promise.resolve('mocked data')), })) test('fetchData를 호출해야 한다.', async () => { const data = await fetchData() expect(fetchData).toHaveBeenCalled() expect(data).toBe('mocked data') })
함수와 의존성 대체
- 복잡한 연산이나 호출 횟수 제한이 있는 API 대신 가벼운 Mock 구현으로 대체 가능.
- 예: 데이터베이스 연결, API 호출, 파일 I/O 등.
3. 모킹 초기화
테스트의 독립성과 안정성을 유지하기 위해 모킹을 초기화하는 것이 중요합니다.
Vitest는 이를 위해 여러 초기화 메서드를 제공합니다.
vi.clearAllMocks
- 모든 Mock 함수의 호출 기록 초기화.
- Mock 함수의 구현은 유지.
const mockFn = vi.fn() mockFn() expect(mockFn).toHaveBeenCalled() vi.clearAllMocks() expect(mockFn).not.toHaveBeenCalled()
vi.resetAllMocks
- 모든 Mock 함수의 호출 기록 초기화 및 구현을 빈 함수로 리셋.
const mockFn = vi.fn(() => 'data') mockFn() vi.resetAllMocks() expect(mockFn()).toBeUndefined() // 구현 초기화됨
vi.restoreAllMocks
- 모든 Mock 함수의 호출 기록 초기화 및 원래 구현 복원.
- 원본 동작으로 돌아가야 하는 경우 사용.
const obj = { greet: () => 'Hello' } const spy = vi.spyOn(obj, 'greet').mockReturnValue('Hi') expect(obj.greet()).toBe('Hi') vi.restoreAllMocks() expect(obj.greet()).toBe('Hello')
4. 모킹의 장점
- 테스트 독립성 확보
- 외부 모듈의 동작에 의존하지 않고 테스트 대상에만 집중 가능.
- 효율적인 테스트 환경 구성
- 복잡한 연산이나 시간 소모적인 작업을 Mock으로 대체.
- 의존성 분리
- 컴포넌트나 함수의 동작을 외부 요인으로부터 분리하여 검증 가능.
5. 주의 사항
- 외부 모듈 검증
- 모킹된 모듈 자체의 동작은 별도로 검증되어야 함.
- 모킹 초기화 누락
- 초기화를 누락하면 테스트 간 데이터가 공유되어 예기치 않은 결과 발생 가능.
- Mock 동작 과도 의존
- Mock으로 인해 실제 동작과 차이가 발생할 수 있으므로 실제 환경 테스트도 필요.
6. 모킹 활용 예시
1) API 호출 모킹
import { fetchData } from './api'
vi.mock('./api', () => ({
fetchData: vi.fn(() => Promise.resolve({ data: 'mocked data' })),
}))
test('API 호출 테스트', async () => {
const response = await fetchData()
expect(fetchData).toHaveBeenCalled()
expect(response).toEqual({ data: 'mocked data' })
})
2) 모킹 초기화
import { fetchData } from './api'
vi.mock('./api', () => ({
fetchData: vi.fn(() => Promise.resolve({ data: 'mocked data' })),
}))
afterEach(() => {
vi.clearAllMocks() // 각 테스트 후 호출 기록 초기화
})
test('테스트 1', async () => {
const response = await fetchData()
expect(fetchData).toHaveBeenCalledTimes(1)
})
test('테스트 2', async () => {
expect(fetchData).not.toHaveBeenCalled() // 호출 기록 초기화됨
})
리액트 훅 테스트
React Test Utilities
React Test Utilities는 React 컴포넌트를 테스트 프레임워크에서 쉽게 테스트할 수 있도록 도와주는 도구입니다. 이 도구를 사용하여 컴포넌트의 렌더링 및 동작을 검증할 수 있습니다.
1. 기본 설정
Import 방법
// ES6
import ReactTestUtils from 'react-dom/test-utils'
// ES5
const ReactTestUtils = require('react-dom/test-utils')
추천 도구
- React Testing Library를 사용하는 것이 권장됩니다. 이는 사용자 관점에서 컴포넌트를 테스트하도록 설계되었습니다.
- React 16 이하 버전에서는 Enzyme을 사용해 컴포넌트 출력과 DOM을 쉽게 탐색 및 조작할 수 있습니다.
2. 주요 메서드 및 사용법
act()
1) - 컴포넌트의 렌더링 및 상태 업데이트를 테스트 전에 준비.
act()
내부에서 렌더링 및 업데이트가 실행되어 React의 브라우저 동작과 비슷한 환경을 제공합니다.
사용 예시
import { act } from 'react-dom/test-utils'
class Counter extends React.Component {
constructor() {
super()
this.state = { count: 0 }
}
handleClick = () => this.setState({ count: this.state.count + 1 })
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.handleClick}>Click me</button>
</div>
)
}
}
let container
beforeEach(() => {
container = document.createElement('div')
document.body.appendChild(container)
})
afterEach(() => {
document.body.removeChild(container)
container = null
})
it('renders and updates the counter', () => {
act(() => {
ReactDOM.createRoot(container).render(<Counter />)
})
const button = container.querySelector('button')
const label = container.querySelector('p')
expect(label.textContent).toBe('You clicked 0 times')
act(() => {
button.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
expect(label.textContent).toBe('You clicked 1 times')
})
2) Mocking
mockComponent(componentClass, [mockTagName])
모의 컴포넌트를 생성.jest.mock()
사용을 권장.
3) 요소 확인
isElement(element)
: 주어진 객체가 React 엘리먼트인지 확인.isElementOfType(element, componentClass)
: 특정 React 컴포넌트 유형인지 확인.isDOMComponent(instance)
: DOM 컴포넌트인지 확인(<div>
등).isCompositeComponent(instance)
: 사용자 정의 컴포넌트인지 확인.
3. DOM 검색 및 조작 메서드
1) DOM 검색
findAllInRenderedTree(tree, test)
: 렌더링된 트리를 순회하며 조건에 맞는 모든 컴포넌트를 반환.scryRenderedDOMComponentsWithClass(tree, className)
: 특정 클래스명을 가진 모든 DOM 요소를 반환.findRenderedDOMComponentWithClass(tree, className)
: 특정 클래스명을 가진 DOM 요소를 하나만 반환 (없거나 여러 개일 경우 예외 발생).scryRenderedDOMComponentsWithTag(tree, tagName)
: 특정 태그명을 가진 모든 DOM 요소를 반환.findRenderedDOMComponentWithTag(tree, tagName)
: 특정 태그명을 가진 DOM 요소를 하나만 반환 (없거나 여러 개일 경우 예외 발생).
2) React 컴포넌트 검색
scryRenderedComponentsWithType(tree, componentClass)
: 특정 타입의 모든 컴포넌트를 반환.findRenderedComponentWithType(tree, componentClass)
: 특정 타입의 컴포넌트를 하나만 반환 (없거나 여러 개일 경우 예외 발생).
4. 테스트를 위한 렌더링
renderIntoDocument(element)
- React 요소를 문서에 렌더링.
- DOM이 필요하며,
document.createElement
로 생성된 DOM 컨테이너에 요소를 렌더링.
const domContainer = document.createElement('div')
ReactDOM.createRoot(domContainer).render(<MyComponent />)
5. 이벤트 시뮬레이션
Simulate
- DOM 노드에서 이벤트를 시뮬레이션.
- React에서 이해하는 모든 이벤트에 대해 메서드 제공.
사용 예시
클릭 이벤트
const button = document.querySelector('button') ReactTestUtils.Simulate.click(button)
입력 필드 변경 후 Enter 키 입력
const input = document.querySelector('input') input.value = 'Test Value' ReactTestUtils.Simulate.change(input) ReactTestUtils.Simulate.keyDown(input, { key: 'Enter', keyCode: 13 })
6. 추가 참고 사항
- 테스트 환경
- React는 DOM을 접근할 수 있어야 합니다.
- Jest에서는
testEnvironment
를jsdom
으로 설정.
- Testing Library 활용
- React Testing Library를 사용하면
act()
와 같은 보일러플레이트 코드를 줄이고 사용자 관점에서 테스트 작성 가능.
- React Testing Library를 사용하면
타이머 테스트
타이머 테스트
타이머 테스트는 테스트 중에 시간을 조작하거나 제어할 수 있는 방법으로, **타이머(mocked timers)**를 사용해 시간 관련 로직을 효과적으로 검증할 수 있습니다. 이는 setTimeout, setInterval, 그리고 Date와 같은 타이머 기반 함수들이 포함된 코드의 테스트를 쉽게 만들어 줍니다.
1. 타이머 모킹(Mock Timers)
1) 타이머 모킹의 필요성
- 타이머가 포함된 코드의 동작을 원하는 대로 제어할 수 있음.
- 실제 시간이 흐르지 않아도 테스트 환경에서 즉각적인 시간의 경과를 시뮬레이션 가능.
- 시간 관련 로직(setTimeout, setInterval 등)의 결과를 빠르고 정확하게 검증할 수 있음.
useFakeTimers()
2) - *
vi.useFakeTimers()
*는 타이머를 모킹하기 위해 사용하는 함수. - 타이머를 모킹하면 setTimeout, setInterval, clearTimeout, clearInterval, 그리고 Date 객체와 같은 시간 기반 함수들을 제어 가능.
사용 예시
import { vi } from 'vitest'
test('타이머 모킹으로 setTimeout 테스트', () => {
vi.useFakeTimers() // 타이머 모킹 활성화
const callback = vi.fn()
setTimeout(callback, 1000)
// 아직 시간이 흐르지 않았으므로 callback은 호출되지 않음
expect(callback).not.toHaveBeenCalled()
// 1초(1000ms)가 흐른 것으로 시뮬레이션
vi.advanceTimersByTime(1000)
// callback이 호출되었는지 확인
expect(callback).toHaveBeenCalled()
})
2. 타이머 제어
advanceTimersByTime()
1) - 특정 시간(밀리초 단위)을 경과한 것처럼 시뮬레이션.
- 이를 통해 타이머 기반 함수들이 실행되는 시점을 제어 가능.
사용 예시
vi.useFakeTimers()
const callback = vi.fn()
setTimeout(callback, 2000)
// 1초만 경과 시킴
vi.advanceTimersByTime(1000)
expect(callback).not.toHaveBeenCalled() // 아직 실행되지 않음
// 추가로 1초 더 경과 시킴
vi.advanceTimersByTime(1000)
expect(callback).toHaveBeenCalled() // 2초가 지나 callback 실행
setSystemTime()
2) - 현재 시스템 시간을 특정 값으로 설정.
- Date 객체를 사용하는 코드의 동작을 테스트할 때 유용.
사용 예시
vi.useFakeTimers()
// 현재 시스템 시간을 2025년 1월 1일로 설정
const mockDate = new Date('2025-01-01T00:00:00Z')
vi.setSystemTime(mockDate)
const currentDate = new Date()
expect(currentDate.toISOString()).toBe('2025-01-01T00:00:00.000Z')
3. 타이머 복원
1) 왜 복원이 필요한가?
- 타이머가 모킹된 상태를 유지하면 다른 테스트에 영향을 줄 수 있음.
- 테스트의 독립성과 안정성을 보장하려면 모킹된 타이머를 복원해야 함.
useRealTimers()
2) - 타이머 모킹을 해제하고 원래의 타이머 동작으로 되돌림.
- 보통
afterEach()
훅에서 호출하여 각 테스트가 끝난 후 타이머를 복원.
사용 예시
import { vi } from 'vitest'
beforeEach(() => {
vi.useFakeTimers() // 각 테스트 시작 전에 타이머 모킹
})
afterEach(() => {
vi.useRealTimers() // 각 테스트 후 타이머 복원
})
test('setTimeout 테스트', () => {
const callback = vi.fn()
setTimeout(callback, 500)
vi.advanceTimersByTime(500)
expect(callback).toHaveBeenCalled()
})
4. 타이머 테스트의 장점
- 시간 제어
- 실제로 기다리지 않고 시간 경과를 제어할 수 있어 테스트 속도 향상.
- 정확한 검증
- 시간에 의존적인 코드(예: 애니메이션, API 호출 지연 등)를 정확하게 검증.
- 독립성 보장
- 시스템 시간과 무관하게 테스트를 실행하여 환경에 따른 테스트 실패 방지.
useEvent 를 사용한 사용자 상호작용 테스트
fireEvent
와 userEvent
를 활용한 테스트
React Testing Library에서 제공하는 fireEvent
와 userEvent
는 DOM 상호작용을 시뮬레이션하기 위한 도구입니다. 두 도구의 차이점과 사용 사례를 학습하면 보다 실용적이고 신뢰성 높은 테스트 코드를 작성할 수 있습니다.
fireEvent
1. fireEvent
란?
1) - React Testing Library에 내장된 도구로, 특정 DOM 요소에서 이벤트를 간단히 발생시킬 수 있음.
- DOM 이벤트를 직접 호출하는 방식으로,
click
,change
,keydown
등 이벤트를 트리거 가능.
2) 주요 특징
- DOM 이벤트만 발생.
- 실제 사용자 상호작용 시 발생하는 연쇄적인 이벤트 호출은 없음.
- 비활성화된 버튼 클릭, 입력 불가능한 필드에도 강제로 이벤트 발생 가능.
3) 사용법
예제 1: 버튼 클릭
import { fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import React from 'react'
function ButtonComponent() {
const [clicked, setClicked] = React.useState(false)
return <button onClick={() => setClicked(true)}>{clicked ? 'Clicked!' : 'Click Me'}</button>
}
test('버튼 클릭 시 상태 변경', () => {
render(<ButtonComponent />)
const button = screen.getByText('Click Me')
fireEvent.click(button) // 클릭 이벤트 트리거
expect(button).toHaveTextContent('Clicked!') // 상태 변화 확인
})
예제 2: 입력 필드 값 변경
test('입력 필드 값 변경', () => {
render(<input placeholder="Type here" />)
const input = screen.getByPlaceholderText('Type here')
fireEvent.change(input, { target: { value: 'Hello' } }) // change 이벤트 발생
expect(input.value).toBe('Hello') // 값이 변경되었는지 확인
})
userEvent
2. userEvent
란?
1) - 사용자 상호작용을 더 현실적으로 시뮬레이션하기 위해 설계된 도구.
- 키보드 입력, 마우스 클릭, 드래그 등 복잡한 사용자 동작도 처리 가능.
2) 주요 특징
- DOM 이벤트 외에도 사용자 동작을 모방해 연쇄적인 이벤트 발생.
- 버튼이 비활성화된 경우 클릭 이벤트를 발생시키지 않음.
- 입력 불가능한 필드에서는 입력 이벤트를 차단.
- 실제 사용자의 동작을 최대한 모방하여 테스트 신뢰성 향상.
3) 사용법
예제 1: 버튼 클릭 (연쇄 이벤트 발생)
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
test('버튼 클릭 시 연쇄 이벤트 확인', async () => {
render(<button onClick={() => console.log('clicked')}>Click Me</button>)
const button = screen.getByText('Click Me')
await userEvent.click(button) // 연쇄적인 클릭 이벤트 발생
// 클릭 이벤트 발생 시 예상된 콘솔 출력 확인 (테스트 환경에서 확인 가능)
})
예제 2: 키보드 입력
test('입력 필드에 텍스트 입력', async () => {
render(<input placeholder="Type something" />)
const input = screen.getByPlaceholderText('Type something')
await userEvent.type(input, 'Hello World!') // 사용자가 입력하는 것처럼 동작
expect(input).toHaveValue('Hello World!') // 입력된 값 확인
})
fireEvent
와 userEvent
의 비교
3. 기능 | fireEvent | userEvent |
---|---|---|
DOM 이벤트 발생 | 단일 DOM 이벤트 발생 | 연쇄적인 DOM 이벤트 발생 |
실제 사용자 상호작용 | 모방하지 않음 | 실제 사용자 동작에 가까운 테스트 가능 |
비활성화된 버튼 처리 | 강제 이벤트 발생 가능 | 클릭 이벤트 발생하지 않음 |
입력 필드 제어 | 입력 불가능한 필드에도 강제 입력 가능 | 입력 불가능한 필드에서는 입력 차단 |
테스트 신뢰성 | 낮음 | 높음 |
주요 사용 사례 | 간단한 DOM 이벤트 검증 | 사용자 상호작용 테스트 |
fireEvent
와 userEvent
의 사용 시점
4. fireEvent
를 사용하는 경우
- 간단한 DOM 이벤트 테스트가 필요한 경우.
- 연쇄 이벤트 호출이 필요하지 않은 상황.
- 사용자의 동작과 무관하게 DOM의 상태를 변경해야 하는 경우.
userEvent
를 사용하는 경우
- 실제 사용자 상호작용과 유사한 동작을 테스트하고 싶은 경우.
- 버튼 클릭, 키보드 입력, 드래그 앤 드롭 등 복잡한 이벤트가 필요한 경우.
- 비활성화된 버튼이나 입력 필드와 같은 엣지 케이스를 포함한 테스트.
5. 권장사항
- 테스트 신뢰성을 높이기 위해
userEvent
를 우선 사용. userEvent
에서 지원하지 않는 기능(예: 비활성화된 버튼 클릭)이 필요할 때만fireEvent
사용을 고려.
단위 테스트의 한계
단위 테스트(Unit Test)의 한계
단위 테스트는 소프트웨어 개발에서 가장 기본적인 테스트 방법 중 하나로, 개별 모듈(또는 함수)이 올바르게 작동하는지를 검증합니다. 그러나 단위 테스트만으로는 모든 시나리오를 검증할 수 없으며, 몇 가지 한계가 있습니다.
1. 단위 테스트의 한계
1) 통합된 환경에서 발생하는 문제를 검출하기 어려움
- 단위 테스트는 개별 함수나 모듈의 동작을 검증하기 때문에 여러 모듈이 상호작용하면서 발생하는 문제를 발견할 수 없습니다.
- 예를 들어, API 요청과 데이터베이스 처리 간의 상호작용 문제는 단위 테스트로는 확인하기 어렵습니다.
2) 외부 종속성에 대한 제한
- 단위 테스트는 일반적으로 모듈의 외부 의존성을 Mocking(모킹)하여 테스트합니다.
- 모킹은 실제 환경과 다를 수 있어, 실제 의존성과의 상호작용에서 발생하는 오류를 놓칠 수 있습니다.
3) 사용자 경험(UX) 검증 불가
- 단위 테스트는 코드 레벨에서의 동작만 검증하므로, 사용자가 시스템을 어떻게 경험하는지는 확인할 수 없습니다.
- UI/UX와 관련된 문제는 단위 테스트의 범위를 벗어납니다.
4) 복잡한 시나리오 처리의 어려움
- 단위 테스트는 주로 단순한 입력과 출력을 기반으로 동작을 검증하기 때문에, 복잡한 비즈니스 로직이나 시나리오를 검증하기 어렵습니다.
- 복잡한 상태 변화가 필요한 테스트는 단위 테스트보다 상위 수준의 테스트가 필요합니다.
2. 통합 테스트(Integration Test)
통합 테스트는 단위 테스트에서 검증한 개별 모듈을 연결하여 상호작용이 올바르게 작동하는지 확인하는 테스트 방법입니다. 단위 테스트가 개별 동작을 검증한다면, 통합 테스트는 모듈 간의 협력을 검증합니다.
1) 통합 테스트의 목적
- 모듈 간 상호작용에서 발생할 수 있는 오류 검출.
- 실제 의존성과의 상호작용 확인(API, 데이터베이스 등).
- 비즈니스 로직이 통합된 상태에서 올바르게 작동하는지 검증.
2) 통합 테스트의 특징
- 실제 환경과 유사한 설정으로 테스트를 수행.
- 단위 테스트보다 실행 속도가 느리고 설정이 복잡함.
- 의존성(데이터베이스, API 등)을 실제로 사용하거나 테스트용 데이터를 이용.
3. 단위 테스트와 통합 테스트의 비교
특징 | 단위 테스트 | 통합 테스트 |
---|---|---|
테스트 범위 | 개별 함수나 모듈 | 모듈 간 상호작용 및 통합된 기능 검증 |
의존성 처리 | Mocking을 주로 사용 | 실제 의존성을 포함하거나 테스트용 데이터 사용 |
테스트 속도 | 빠름 | 상대적으로 느림 |
복잡성 | 낮음 | 높음 |
문제 발견 | 개별 모듈의 문제 검출 | 모듈 간 통합 문제 및 의존성 관련 오류 발견 |
사용자 관점의 검증 | 없음 | 일부 사용자 시나리오 검증 가능 |
4. 통합 테스트의 장점
- 상호작용 문제 검출
- 모듈 간의 의존성이나 데이터 흐름이 올바르게 작동하는지 확인할 수 있습니다.
- 테스트의 현실성
- 실제 환경과 비슷한 조건에서 테스트를 수행하므로, 실환경에서 발생할 수 있는 문제를 미리 발견할 수 있습니다.
- 비즈니스 로직 검증
- 모듈이 통합되어 동작하는 전체 비즈니스 로직을 검증할 수 있습니다.
- 시스템 안정성 향상
- 단위 테스트로는 놓칠 수 있는 문제를 통합 테스트에서 보완하여 시스템의 신뢰성과 안정성을 높일 수 있습니다.
5. 통합 테스트의 단점
- 설정과 유지보수의 복잡성
- 통합 테스트는 실제 데이터베이스, API, 파일 시스템 등 외부 의존성을 필요로 하므로, 설정과 유지보수가 복잡할 수 있습니다.
- 긴 실행 시간
- 실제 환경에서 동작하는 테스트는 단위 테스트보다 실행 속도가 느립니다.
- 디버깅의 어려움
- 문제가 발생하면 여러 모듈이 관여하기 때문에 원인을 특정하기 어렵습니다.
6. 단위 테스트와 통합 테스트의 조화
단위 테스트와 통합 테스트는 상호 보완적인 역할을 합니다. 두 테스트를 적절히 활용하여 테스트 피라미드를 구성하는 것이 중요합니다.
- 테스트 피라미드:
- 단위 테스트: 가장 많은 양을 작성하여 기본적인 동작을 검증.
- 통합 테스트: 중간 규모로 작성하여 모듈 간의 상호작용을 검증.
- 엔드투엔드(E2E) 테스트: 사용자 관점에서 최종 동작을 검증하는 소수의 테스트.
통합 테스트란 무엇일까?
상태관리 모킹하기
테스트 작성 및 환경 설정
테스트 환경 구성
테스트를 작성하기 전에 테스트 환경을 설정해야 합니다. 특히 JavaScript/TypeScript를 사용하거나 React 컴포넌트를 테스트하려면 JSDOM과 같은 가상 DOM 환경을 사용해야 할 수 있습니다.
주요 테스트 러너
- Jest: 인기 있는 테스트 러너. CommonJS 기반.
- Vitest: 빠른 테스트 러너. ES 모듈 기반.
UI 및 네트워크 테스트 도구
React와 관련된 UI 컴포넌트를 테스트하거나 네트워크 요청을 모킹하려면 아래 도구를 추천합니다.
- React Testing Library (RTL):
- React DOM을 테스트하는 데 사용되는 간단하고 완전한 도구.
ReactDOM
의render
및react-dom/test-utils
의act
를 기반으로 동작.- React Testing Library 설정
- Mock Service Worker (MSW):
- 네트워크 요청을 모킹하기 위한 도구.
- 애플리케이션 로직을 수정하지 않고도 테스트가 가능.
- MSW 설치
Zustand 테스트 환경 설정
Zustand는 React 상태 관리 라이브러리입니다. 테스트를 위해 store를 리셋하는 기능이 필요하며, 이를 위해 아래 설정을 사용할 수 있습니다.
Jest에서 Zustand 모킹
Zustand 모킹 파일 생성:
__mocks__/zustand.ts
import { act } from '@testing-library/react'; import type * as ZustandExportedTypes from 'zustand'; export * from 'zustand'; const { create: actualCreate } = jest.requireActual<typeof ZustandExportedTypes>('zustand'); const storeResetFns = new Set<() => void>(); export const create = <T>(stateCreator: ZustandExportedTypes.StateCreator<T>) => { const store = actualCreate(stateCreator); const initialState = store.getState(); storeResetFns.add(() => store.setState(initialState, true)); return store; }; afterEach(() => { act(() => { storeResetFns.forEach((resetFn) => resetFn()); }); });
Jest 설정 파일 생성:
jest.config.ts
import type { JestConfigWithTsJest } from 'ts-jest' const config: JestConfigWithTsJest = { preset: 'ts-jest', testEnvironment: 'jsdom', setupFilesAfterEnv: ['./setup-jest.ts'], } export default config
Vitest에서 Zustand 모킹
Zustand 모킹 파일 생성:
__mocks__/zustand.ts
import { act } from '@testing-library/react'; import type * as ZustandExportedTypes from 'zustand'; export * from 'zustand'; const { create: actualCreate } = await vi.importActual<typeof ZustandExportedTypes>('zustand'); const storeResetFns = new Set<() => void>(); export const create = <T>(stateCreator: ZustandExportedTypes.StateCreator<T>) => { const store = actualCreate(stateCreator); const initialState = store.getState(); storeResetFns.add(() => store.setState(initialState, true)); return store; }; afterEach(() => { act(() => { storeResetFns.forEach((resetFn) => resetFn()); }); });
Vitest 설정 파일 생성:
vitest.config.ts
import { defineConfig } from 'vitest/config' export default defineConfig({ test: { globals: true, environment: 'jsdom', setupFiles: ['./setup-vitest.ts'], }, })
React 컴포넌트 테스트 예제
Zustand Store
Zustand Store 생성:
use-counter-store.ts
import { create } from 'zustand' type CounterStore = { count: number inc: () => void } export const useCounterStore = create<CounterStore>((set) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), }))
Counter 컴포넌트:
import { useCounterStore } from '../stores/use-counter-store' export function Counter() { const { count, inc } = useCounterStore() return ( <div> <h4>{count}</h4> <button onClick={inc}>Increment</button> </div> ) }
Counter 컴포넌트 테스트:
import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Counter } from './counter' test('Counter increases value on button click', async () => { render(<Counter />) expect(screen.getByText('1')).toBeInTheDocument() await userEvent.click(screen.getByText('Increment')) expect(screen.getByText('2')).toBeInTheDocument() })
msw로 API 모킹하기
통합 테스트에서 API 호출 컴포넌트 시뮬레이션
통합 테스트를 진행할 때 API를 호출하는 컴포넌트를 정확히 시뮬레이션하기 위해서는 탠스택 쿼리와 **Mock Service Worker (MSW)**의 적절한 설정과 활용이 필요합니다.
1. 탠스택 쿼리 설정
사용 목적
탠스택 쿼리는 다음과 같은 이유로 API 호출 관리를 간소화하기 위해 사용됩니다:
- 로딩 상태 처리: API 호출 중 로딩 상태를 손쉽게 관리.
- 에러 상태 처리: 호출 실패 시 에러를 일관되게 처리.
- 페이지네이션 및 캐싱: 데이터 캐싱 및 페이지네이션 지원.
테스트 환경에 맞춘 설정
테스트 중에는 기본 설정(예: retry
)이 테스트 결과에 영향을 미칠 수 있으므로, 이를 조정해야 합니다.
예를 들어, 테스트 전용 설정을 포함한 render.jsx
수정:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false, // 테스트 중 재시도 비활성화
staleTime: Infinity, // 캐시된 데이터를 항상 최신으로 간주
},
},
})
export const renderWithQueryClient = (ui) => {
const queryClient = createTestQueryClient()
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>)
}
2. Mock Service Worker (MSW) 설정
사용 목적
MSW는 API 요청을 모킹하여 테스트 환경에서 실제 서버 호출 없이도 API 동작을 시뮬레이션할 수 있게 해줍니다.
MSW의 주요 기능:
- 브라우저 환경: 서비스 워커를 사용해 API 요청을 가로채고 응답을 반환.
- Node.js 환경: XHR 및 fetch 요청을 가로채 테스트에서 처리.
설정 방법
핸들러 작성
API 요청을 모킹하기 위해 핸들러를 작성합니다.
// mock/handlers.js import { rest } from 'msw' export const handlers = [ rest.get('/api/data', (req, res, ctx) => { return res(ctx.status(200), ctx.json({ data: ['item1', 'item2', 'item3'] })) }), ]
MSW 초기화
서비스 워커를 설정합니다.
// mock/server.js import { setupServer } from 'msw/node' import { handlers } from './handlers' export const server = setupServer(...handlers) // 테스트 환경 setup beforeAll(() => server.listen()) // 테스트 시작 전 서비스 워커 활성화 afterEach(() => server.resetHandlers()) // 각 테스트 후 핸들러 리셋 afterAll(() => server.close()) // 테스트 종료 후 서비스 워커 비활성화
테스트 파일에서 MSW 설정 포함
MSW 서버를 포함하여 테스트를 실행합니다.
import { render, screen } from '@testing-library/react' import { renderWithQueryClient } from './test-utils' import App from './App' // 테스트할 컴포넌트 import { server } from './mock/server' test('API 데이터를 렌더링해야 합니다', async () => { renderWithQueryClient(<App />) expect(await screen.findByText(/item1/i)).toBeInTheDocument() expect(await screen.findByText(/item2/i)).toBeInTheDocument() })
3. 테스트 시 유의 사항
setup과 teardown 사용
- MSW 설정은 테스트 실행 전에 활성화(
setup
)되고, 테스트 종료 후 비활성화(teardown
)되어야 합니다.
- MSW 설정은 테스트 실행 전에 활성화(
테스트 데이터 분리
- 각 테스트는 독립적으로 실행되므로, 필요하면
server.resetHandlers()
를 호출해 핸들러를 초기화합니다.
- 각 테스트는 독립적으로 실행되므로, 필요하면
로깅 활용
- 테스트 중 발생하는 API 요청 및 응답을 추적하기 위해 MSW의 로깅 기능을 활성화할 수 있습니다.
server.events.on('request:start', (req) => { console.log('Starting request:', req.url.href) })
React Testing Library (RTL) 비동기 유틸 함수 활용 테스트 작성
React Testing Library (RTL) 비동기 유틸 함수 활용 테스트 작성
React Testing Library(RTL)는 비동기 작업(예: API 호출, 시간 지연 처리 등)을 포함한 테스트를 간소화하는 여러 유틸리티 함수를 제공합니다. 이를 활용하면 비동기 동작을 테스트할 때 명확하고 안정적인 코드를 작성할 수 있습니다.
1. 주요 비동기 유틸 함수
findBy*
1.1 - 비동기 요소 탐색: 특정 조건을 만족하는 요소를 DOM에서 찾을 때 사용.
- RTL의
findBy
메서드는 요소가 나타날 때까지 기다렸다가 반환합니다. - 기본적으로
waitFor
를 내부적으로 호출하며, 지정된 시간(기본 1000ms)까지 반복적으로 확인.
// 예: 비동기 데이터가 렌더링될 때까지 기다림
const heading = await screen.findByText(/비동기 제목/i)
expect(heading).toBeInTheDocument()
waitFor
1.2 - 조건 만족 대기: 특정 조건이 충족될 때까지 기다리며, 폴링(polling)을 통해 확인합니다.
- 일반적으로 상태 변경(예: API 응답 처리) 후 렌더링을 기다릴 때 사용.
await waitFor(() => {
expect(screen.getByText(/완료/i)).toBeInTheDocument()
})
waitForElementToBeRemoved
1.3 - 요소가 사라질 때까지 대기: DOM에서 특정 요소가 제거될 때까지 기다림.
- 로딩 상태나 알림이 사라지는 상황에서 유용.
// 예: 로딩 스피너가 사라질 때까지 기다림
await waitForElementToBeRemoved(() => screen.getByText(/로딩 중/i))
2. 비동기 테스트 작성 예제
아래는 API 호출과 로딩 상태를 포함한 비동기 컴포넌트를 테스트하는 예제입니다.
2.1 컴포넌트 코드
// components/AsyncComponent.jsx
import React, { useEffect, useState } from 'react'
const fetchData = () =>
new Promise((resolve) => {
setTimeout(() => resolve(['아이템 1', '아이템 2', '아이템 3']), 1000)
})
export function AsyncComponent() {
const [items, setItems] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchData().then((data) => {
setItems(data)
setLoading(false)
})
}, [])
if (loading) {
return <div>로딩 중...</div>
}
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
)
}
2.2 테스트 코드
// components/AsyncComponent.test.jsx
import { render, screen } from '@testing-library/react'
import { AsyncComponent } from './AsyncComponent'
test('API 데이터를 비동기적으로 렌더링', async () => {
render(<AsyncComponent />)
// 로딩 상태 확인
expect(screen.getByText(/로딩 중.../i)).toBeInTheDocument()
// 데이터가 렌더링될 때까지 기다림
const item1 = await screen.findByText(/아이템 1/i)
const item2 = await screen.findByText(/아이템 2/i)
expect(item1).toBeInTheDocument()
expect(item2).toBeInTheDocument()
// 로딩 메시지가 사라지는지 확인
expect(screen.queryByText(/로딩 중.../i)).not.toBeInTheDocument()
})
3. 유용한 팁과 주의사항
findBy
와waitFor
의 차이findBy
: 요소가 나타날 때까지 기다린 후 반환.waitFor
: 특정 조건이 충족되는지 반복적으로 확인.
- 타이밍 관련 문제 방지
- 테스트는 항상 비동기 작업이 완료된 이후 상태를 확인해야 합니다.
findBy
또는waitFor
를 사용해 비동기 흐름을 정확히 테스트합니다.
- 불필요한 대기 제거
waitFor
내부에서 테스트 대상의 상태를 검증하며, 불필요한setTimeout
을 피하세요.
- 네트워크 요청 모킹
- Mock Service Worker(MSW)를 사용해 API 요청을 모킹하면 테스트 결과를 더 신뢰할 수 있습니다.
