icon안동민 개발노트

통합 테스트 작성


 Next.js App Router를 사용하는 애플리케이션에서 통합 테스트는 여러 컴포넌트나 기능이 함께 올바르게 작동하는지 확인하는 중요한 과정입니다.

 이 절에서는 통합 테스트의 개념, React Testing Library를 사용한 테스트 작성법, 그리고 효과적인 테스트 전략에 대해 알아보겠습니다.

통합 테스트의 정의

 통합 테스트는 여러 단위(컴포넌트, 함수 등)가 함께 올바르게 동작하는지 확인하는 테스트입니다.

 단위 테스트가 개별 함수나 컴포넌트를 독립적으로 테스트한다면, 통합 테스트는 이들이 결합되었을 때의 동작을 검증합니다.

 주요 차이점

  • 범위 : 단위 테스트는 작은 단위를, 통합 테스트는 더 큰 단위를 대상으로 합니다.
  • 복잡성 : 통합 테스트는 일반적으로 더 복잡하고 시간이 오래 걸립니다.
  • 의존성 : 통합 테스트는 실제 의존성을 더 많이 포함하며, 때로는 외부 서비스와의 상호작용도 테스트합니다.

React Testing Library

 Next.js App Router 환경에서 React Testing Library를 사용한 통합 테스트 예제

app/components/UserProfile.js
'use client'
 
import { useState, useEffect } from 'react'
import { fetchUserData } from '../utils/api'
 
export function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
 
  useEffect(() => {
    async function loadUser() {
      const data = await fetchUserData(userId)
      setUser(data)
    }
    loadUser()
  }, [userId])
 
  if (!user) return <div>Loading...</div>
 
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  )
}
app/components/UserProfile.test.js
import { render, screen, waitFor } from '@testing-library/react'
import { UserProfile } from './UserProfile'
import { fetchUserData } from '../utils/api'
 
jest.mock('../utils/api')
 
describe('UserProfile', () => {
  it('loads and displays user data', async () => {
    fetchUserData.mockResolvedValue({ name: 'John Doe', email: '[email protected]' })
 
    render(<UserProfile userId={1} />)
 
    expect(screen.getByText('Loading...')).toBeInTheDocument()
 
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument()
      expect(screen.getByText('Email: [email protected]')).toBeInTheDocument()
    })
 
    expect(fetchUserData).toHaveBeenCalledWith(1)
  })
})

API 호출 모킹

 Next.js App Router 환경에서 API 호출을 모킹하는 방법

import { rest } from 'msw'
import { setupServer } from 'msw/node'
 
const server = setupServer(
  rest.get('/api/user/:id', (req, res, ctx) => {
    return res(ctx.json({ name: 'John Doe', email: '[email protected]' }))
  })
)
 
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
 
// 이제 테스트에서 실제 API 대신 이 모의 서버가 응답합니다.

라우팅 및 상태 관리 관련 통합 테스트

 Next.js App Router의 라우팅과 상태 관리를 포함한 통합 테스트 예제

app/users/[id]/page.js
'use client'
 
import { useParams } from 'next/navigation'
import { UserProfile } from '../../components/UserProfile'
 
export default function UserPage() {
  const params = useParams()
  return <UserProfile userId={params.id} />
}
app/users/[id]/page.test.js
import { render, screen } from '@testing-library/react'
import UserPage from './page'
import { fetchUserData } from '../../utils/api'
 
jest.mock('next/navigation', () => ({
  useParams: () => ({ id: '1' }),
}))
 
jest.mock('../../utils/api', () => ({
  fetchUserData: jest.fn().mockResolvedValue({ name: 'John Doe', email: '[email protected]' }),
}))
 
describe('UserPage', () => {
  it('renders user profile with correct data', async () => {
    render(<UserPage />)
 
    await screen.findByText('John Doe')
    expect(screen.getByText('Email: [email protected]')).toBeInTheDocument()
  })
})

11장 API 라우트 테스팅과의 연관성

 통합 테스트는 11장에서 다룬 API 라우트 테스팅과 밀접하게 연관됩니다.

 API 라우트를 테스트할 때는 주로 서버 측 로직을 검증하지만, 통합 테스트에서는 이러한 API 호출이 클라이언트 컴포넌트와 어떻게 상호작용하는지 테스트합니다.

 예를 들어, API 라우트가 반환하는 데이터가 UI에 올바르게 표시되는지 확인할 수 있습니다.

실습 : 통합 테스트

 복잡한 페이지 컴포넌트에 대한 통합 테스트를 작성해보겠습니다.

 이 컴포넌트는 로그인 프로세스와 데이터 페칭을 포함합니다.

app/components/Dashboard.js
'use client'
 
import { useState } from 'react'
import { login, fetchUserData } from '../utils/api'
 
export function Dashboard() {
  const [user, setUser] = useState(null)
  const [error, setError] = useState(null)
 
  async function handleLogin(event) {
    event.preventDefault()
    const username = event.target.username.value
    const password = event.target.password.value
 
    try {
      const loginResult = await login(username, password)
      const userData = await fetchUserData(loginResult.userId)
      setUser(userData)
      setError(null)
    } catch (err) {
      setError('Login failed. Please try again.')
    }
  }
 
  if (user) {
    return (
      <div>
        <h1>Welcome, {user.name}!</h1>
        <p>Email: {user.email}</p>
      </div>
    )
  }
 
  return (
    <div>
      <h1>Login</h1>
      {error && <p>{error}</p>}
      <form onSubmit={handleLogin}>
        <input name="username" placeholder="Username" required />
        <input name="password" type="password" placeholder="Password" required />
        <button type="submit">Login</button>
      </form>
    </div>
  )
}
app/components/Dashboard.test.js
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { Dashboard } from './Dashboard'
import { login, fetchUserData } from '../utils/api'
 
jest.mock('../utils/api')
 
describe('Dashboard', () => {
  it('allows user to login and displays user data', async () => {
    login.mockResolvedValue({ userId: '1' })
    fetchUserData.mockResolvedValue({ name: 'John Doe', email: '[email protected]' })
 
    render(<Dashboard />)
 
    fireEvent.change(screen.getByPlaceholderText('Username'), { target: { value: 'johndoe' } })
    fireEvent.change(screen.getByPlaceholderText('Password'), { target: { value: 'password123' } })
    fireEvent.click(screen.getByText('Login'))
 
    await waitFor(() => {
      expect(screen.getByText('Welcome, John Doe!')).toBeInTheDocument()
      expect(screen.getByText('Email: [email protected]')).toBeInTheDocument()
    })
 
    expect(login).toHaveBeenCalledWith('johndoe', 'password123')
    expect(fetchUserData).toHaveBeenCalledWith('1')
  })
 
  it('displays error message on login failure', async () => {
    login.mockRejectedValue(new Error('Invalid credentials'))
 
    render(<Dashboard />)
 
    fireEvent.change(screen.getByPlaceholderText('Username'), { target: { value: 'wronguser' } })
    fireEvent.change(screen.getByPlaceholderText('Password'), { target: { value: 'wrongpass' } })
    fireEvent.click(screen.getByText('Login'))
 
    await waitFor(() => {
      expect(screen.getByText('Login failed. Please try again.')).toBeInTheDocument()
    })
  })
})

 이 실습에서는 로그인 프로세스와 사용자 데이터 페칭을 포함하는 Dashboard 컴포넌트에 대한 통합 테스트를 작성했습니다.

 테스트는 성공적인 로그인 시나리오와 실패 시나리오를 모두 다루며, API 호출을 모킹하여 다양한 상황을 시뮬레이션합니다.

 Next.js App Router 환경에서의 통합 테스트는 애플리케이션의 여러 부분이 함께 잘 작동하는지 확인하는 중요한 도구입니다.

 React Testing Library를 사용하면 사용자 관점에서 애플리케이션을 테스트할 수 있으며, 이는 실제 사용 시나리오를 더 잘 반영합니다.

 API 호출 모킹, 라우팅 테스트, 상태 관리 테스트 등을 통해 애플리케이션의 다양한 측면을 포괄적으로 검증할 수 있습니다.

 효과적인 통합 테스트 전략은 애플리케이션의 안정성을 높이고 버그를 사전에 방지하는 데 큰 도움이 됩니다.