icon안동민 개발노트

단위 테스트 설정 (Jest)


 Next.js의 App Router를 사용하는 프로젝트에서 Jest를 활용한 단위 테스트 설정은 애플리케이션의 안정성과 신뢰성을 보장하는 중요한 과정입니다.

 이 절에서는 Jest 설정 방법, 테스트 작성 기본 원칙, 그리고 Next.js App Router에 특화된 테스트 전략에 대해 알아보겠습니다.

Jest 설치 및 구성

  1. 필요한 패키지 설치 :
npm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
  1. jest.config.js 파일 생성 :
const nextJest = require('next/jest')
 
const createJestConfig = nextJest({
  dir: './',
})
 
const customJestConfig = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  moduleNameMapper: {
    '^@/components/(.*)$': '<rootDir>/app/components/$1',
  },
}
 
module.exports = createJestConfig(customJestConfig)
  1. jest.setup.js 파일 생성 :
import '@testing-library/jest-dom'
  1. package.json에 테스트 스크립트 추가 :
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch"
  }
}

기본적인 테스트 케이스 작성법

 App Router를 사용하는 Next.js 프로젝트에서의 기본적인 테스트 케이스 작성하기

 예시

// app/utils/math.js
export function add(a, b) {
  return a + b
}
 
// app/utils/math.test.js
import { add } from './math'
 
describe('Math utilities', () => {
  test('adds two numbers correctly', () => {
    expect(add(1, 2)).toBe(3)
    expect(add(-1, 1)).toBe(0)
    expect(add(0, 0)).toBe(0)
  })
})

모킹과 스파이 사용법

 Next.js App Router 환경에서의 모킹과 스파이 사용

 예시

// app/utils/api.js
import axios from 'axios'
 
export async function fetchUser(id) {
  const response = await axios.get(`/api/users/${id}`)
  return response.data
}
 
// app/utils/api.test.js
import axios from 'axios'
import { fetchUser } from './api'
 
jest.mock('axios')
 
describe('API utilities', () => {
  test('fetchUser returns user data', async () => {
    const mockUser = { id: 1, name: 'John Doe' }
    axios.get.mockResolvedValue({ data: mockUser })
 
    const user = await fetchUser(1)
    expect(user).toEqual(mockUser)
    expect(axios.get).toHaveBeenCalledWith('/api/users/1')
  })
})

Next.js App Router 컴포넌트 테스트 전략

 App Router에서는 서버 컴포넌트와 클라이언트 컴포넌트를 구분하여 테스트해야 합니다.

 서버 컴포넌트 테스트

 서버 컴포넌트는 서버에서 렌더링되므로, 일반적인 React 테스트 방식과는 다르게 접근해야 합니다.

// app/components/ServerComponent.js
import { fetchData } from '../utils/api'
 
export async function ServerComponent({ id }) {
  const data = await fetchData(id)
  return <div>{data.name}</div>
}
 
// app/components/ServerComponent.test.js
import React from 'react'
import { render } from '@testing-library/react'
import { ServerComponent } from './ServerComponent'
 
jest.mock('../utils/api', () => ({
  fetchData: jest.fn().mockResolvedValue({ name: 'John Doe' })
}))
 
describe('ServerComponent', () => {
  it('renders with correct data', async () => {
    const component = await ServerComponent({ id: 1 })
    const { getByText } = render(component)
    expect(getByText('John Doe')).toBeInTheDocument()
  })
})

 클라이언트 컴포넌트 테스트

 클라이언트 컴포넌트는 기존의 React 테스트 방식과 유사하게 테스트할 수 있습니다.

// app/components/ClientComponent.js
'use client'
 
import { useState } from 'react'
 
export function ClientComponent() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}
 
// app/components/ClientComponent.test.js
import { render, fireEvent } from '@testing-library/react'
import { ClientComponent } from './ClientComponent'
 
describe('ClientComponent', () => {
  it('increments count when button is clicked', () => {
    const { getByText } = render(<ClientComponent />)
    const button = getByText('Increment')
    fireEvent.click(button)
    expect(getByText('Count: 1')).toBeInTheDocument()
  })
})

7장 서버 컴포넌트와 클라이언트 컴포넌트 테스팅 연결

 7장에서 다룬 서버 컴포넌트와 클라이언트 컴포넌트의 개념은 테스트 전략에 직접적인 영향을 미칩니다. 서버 컴포넌트는 서버에서 실행되므로 주로 렌더링 결과와 데이터 페칭 로직을 테스트하며, 클라이언트 컴포넌트는 사용자 상호작용과 상태 변화를 중심으로 테스트합니다.

실습 : 서버 컴포넌트와 클라이언트 컴포넌트 단위 테스트 작성

  1. 서버 컴포넌트 테스트 :
// app/components/UserProfile.js
import { fetchUserData } from '../utils/api'
 
export async function UserProfile({ userId }) {
  const userData = await fetchUserData(userId)
  return <div>Welcome, {userData.name}!</div>
}
 
// app/components/UserProfile.test.js
import { render } from '@testing-library/react'
import { UserProfile } from './UserProfile'
import { fetchUserData } from '../utils/api'
 
jest.mock('../utils/api', () => ({
  fetchUserData: jest.fn()
}))
 
describe('UserProfile', () => {
  it('renders user name correctly', async () => {
    fetchUserData.mockResolvedValue({ name: 'John Doe' })
    
    const component = await UserProfile({ userId: 1 })
    const { getByText } = render(component)
    expect(getByText('Welcome, John Doe!')).toBeInTheDocument()
  })
})
  1. 클라이언트 컴포넌트 테스트 :
// app/components/Counter.js
'use client'
 
import { useState } from 'react'
 
export function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}
 
// app/components/Counter.test.js
import { render, fireEvent } from '@testing-library/react'
import { Counter } from './Counter'
 
describe('Counter', () => {
  it('renders initial count and increments correctly', () => {
    const { getByText } = render(<Counter />)
    
    expect(getByText('Count: 0')).toBeInTheDocument()
    
    fireEvent.click(getByText('Increment'))
    expect(getByText('Count: 1')).toBeInTheDocument()
  })
})

 이 실습을 통해 App Router 환경에서 서버 컴포넌트와 클라이언트 컴포넌트의 테스트 접근 방식 차이를 이해할 수 있습니다. 서버 컴포넌트 테스트는 주로 비동기 로직과 렌더링 결과를 검증하는 데 중점을 두며, 클라이언트 컴포넌트 테스트는 사용자 상호작용과 상태 변화를 검증하는 데 초점을 맞춥니다.

 Next.js App Router 프로젝트에서 Jest를 사용한 단위 테스트는 애플리케이션의 안정성을 보장하고 버그를 사전에 방지하는 데 큰 도움이 됩니다. 서버 컴포넌트와 클라이언트 컴포넌트의 특성을 이해하고 각각에 맞는 테스트 전략을 적용하는 것이 중요합니다. 또한, 모킹과 스파이를 적절히 활용하여 외부 의존성을 제어하고, 테스트 커버리지를 높이는 것이 좋습니다. 정기적인 테스트 실행과 지속적인 테스트 케이스 업데이트를 통해 애플리케이션의 품질을 지속적으로 관리할 수 있습니다.