본문 바로가기

React/NextJS

Nextjs Boilerplate 만들기 - 5 [Jest - 컴포넌트 단위 테스트]

애플리케이션 개발에서 테스트는 코드의 품질을 높이고, 코드의 변경에 예상치 못한 버그를 방지할 수 있습니다.

 

 

테스트 환경 구축

 

yarn add -D jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom

 

프로젝트 루트에 jest.setup.js 파일 생성

 

import '@testing-library/jest-dom/extend-expect'

 

프로젝트 루트에 jest.config.ts 파일 생성

 

테스트 파일을 Typescript 로 작성하기 위해 ts-node를 설치합니다.

 yarn add -D ts-node typescript jest
import type { Config } from 'jest'
import nextJest from 'next/jest.js'

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
})

// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config: Config = {
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config)

 

작성 후 package.json 에 test 실행을 위한 스크립트를 추가합니다.

 

 ...
 "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint --dir src",
    "format": "next lint --fix --dir src",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "test": "jest"
  },
  ...

 

근데 오류가 난다...SyntaxError: Cannot use import statement outside a module

 

 

좀 찾아보다가 아래처럼 수정하였습니다.

 

import type { Config } from 'jest'

const nextJest = require('next/jest')
const createJestConfig = nextJest({
  dir: './',
})
const customJestConfig: Config = {
  testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jsdom',
  testEnvironmentOptions: {
    html: '<html lang="zh-cmn-Hant"></html>',
    url: 'https://jestjs.io/',
    userAgent: 'Agent/007',
  },
}
module.exports = createJestConfig(customJestConfig)

 

다시 실행한 결과 잘 실행됩니다.

 

 

테스트 예시

컴포넌트를 생성합니다.

 

import { ChangeEvent, useState } from 'react'

type InputProps = JSX.IntrinsicElements['input'] & {
  label: string
}
export const Input = ({ label, ...rest }: InputProps) => {
  const [text, setText] = useState<string>('')
  const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value)
  }
  const resetInputField = () => {
    setText('')
  }
  return (
    <div>
      <label htmlFor={rest.id}>{label}</label>
      <input {...rest} type='text' value={text} onChange={onInputChange} />
      <button onClick={resetInputField}>Reset</button>
    </div>
  )
}

 

test 파일을 작성합니다.

첫 화면의 input 요소는 비워져있어야하는 test를 진행합니다.

 

import { Input } from '@/components/Input'
import { RenderResult, fireEvent, render, screen } from '@testing-library/react'

/**
 * describe 함수를 사용하여 함수를 모은다. 여기서는 Input Component 의 테스트를 진행한다.
 */
describe('Input', () => {
  let renderResult: RenderResult

  /**
   * beforeEach , afterEach 함수는 각 테스트 실행 전, 후의 처리를 기술합니다.
   */

  // 각 테스트 케이스 전에 컴포넌트를 화면에 그리고, renderResult에 설정한다.
  beforeEach(() => {
    renderResult = render(<Input id='testInput' label='Test Input' />)
  })

  // 테스트 케이스 실행 후 화면에 그려진 컴포넌트를 unmount를 호출해 릴리스 합니다.
  afterEach(() => {
    renderResult.unmount()
  })

  it('첫 화면에서 input 요소는 비워져있어야 합니다.', () => {
    // label이 Test Input 인 input 요소를 가져옵니다.
    const inputNode = screen.getByLabelText('Test Input') as HTMLInputElement

    // Input 요소의 표시가 비어있는지 확인한다.
    expect(inputNode).toHaveValue('')
  })

  it('문자 입력 후 내용이 표시되는지 테스트', () => {
    const inputText = 'Test Input Text'
    const inputNode = screen.getByLabelText('Test Input') as HTMLInputElement

    //fireEvent 를 사용해 input 요소의 onChange 이벤트를 트리거합니다.
    fireEvent.change(inputNode, { target: { value: inputText } })

    // input 요소에 입력한 텍스트가 표시되는지 확인한다.
    expect(inputNode).toHaveValue(inputText)
  })

  it('reset 버튼이 클릭되면 입력 텍스트가 초기화되는지 테스트', () => {
    const inputText = 'Test Input Text'
    const inputNode = screen.getByLabelText('Test Input') as HTMLInputElement
    fireEvent.change(inputNode, { target: { value: inputText } })

    // 버튼을 받아온다
    const buttonNode = screen.getByRole('button', {
      name: 'Reset',
    }) as HTMLButtonElement

    // 버튼 Click
    fireEvent.click(buttonNode)

    //input요소가 Reset 됫는지 확인한다.
    expect(inputNode).toHaveValue('')
  })
})

 

테스트를 실행합니다.

 

yarn test