[Next.js] 테스팅 도입기(1)- Next.js 에 Jest, React Testing Library 세팅

aeong98·2022년 3월 7일
4
post-thumbnail
post-custom-banner

회사에서 프론트엔드 리뉴얼을 진행하며 테스트를 도입하게 되었다. 아무래도 이전까지는 테스트 코드를 작성하지 않았기 때문에, 리팩토링이나 기능 수정이 있을 때마다 일일이 console.log 를 찍어 값을 확인하거나 직접 화면을 구동하면 해당 코드가 의도한 동작대로 수행하는지 확인하는 수밖에 없었다.
따라서 테스트 자동화 도입에 대한 필요성을 느끼게 되었고, 프로젝트 규모가 커질경우, 더욱 안정적인 서비스를 제공하기 위해서는 테스트 코드 작성이 필수적이라는 판단을 내리게 되었다.

도입을 결정한 테스팅 툴은 jest+react testing library 이다. 프론트엔드 테스트를 할 수 있는 방법은 매우 여러가지가 있지만, 여건 상 모든 부분에 대한 테스트를 진행할 수 없기 때문에 꼭 필요한 유닛테스트, 통합테스트부터 우선적으로 도입하게 되었다.

아래는 Next.js 환경에 맞춰 테스팅 환경을 구축한 방법이다.

1. 필요한 라이브러리 설치

yarn add jest               // jest 유닛테스트 라이브러리
@testing-library/react      // 리액트 테스팅 라이브러리 
@testing-library/jest-dom   // 가상 돔 렌더링
@types/jest                 // jest 타입스크립트
@testing-library/user-event // 유저 이벤트 테스트
@swc/core                   // swc 트랜스파일러 
@swc/jest                   // swc jest 트랜스파일러
jest-canvas-mock            // m1 칩 canvas 에러 해결 
-D

2. jest.config.js 세팅

Next.js 는 jest 트랜스파일러로 swc 를 간편하게 지원한다. 바벨에서 swc로 바꾸니 테스트 실행 속도가 약 4-5배 이상 증가하였다.

// jest.config.js
const nextJest = require('next/jest')

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
const customJestConfig = {
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
  moduleDirectories: ['node_modules', '<rootDir>/'],
  testEnvironment: 'jest-environment-jsdom',
  transform: {
    "^.+\\.(t|j)sx?$": ["@swc/jest"],
  },
  transformIgnorePatterns: [
    '<rootDir>/node_modules/', 
    "<rootDir>/.next/",
    '^.+\\.module\\.(css|sass|scss)$',
  ],
  testMatch: [
    '<rootDir>/**/*.test.(js|jsx|ts|tsx)',
    '<rootDir>/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))',
  ],
  moduleNameMapper: {
    // Handle CSS imports (without CSS modules)
    "^.+\\.(css|sass|scss)$": "<rootDir>/__mocks__/styleMock.js",

    // Handle CSS imports (with CSS modules)
    // https://jestjs.io/docs/webpack#mocking-css-modules
    "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",

    // Handle image imports
    // https://jestjs.io/docs/webpack#handling-static-assets
    '^.+\\.(jpg|jpeg|png|gif|webp|avif|svg)$': `<rootDir>/__mocks__/fileMock.js`,

    // Handle module aliases
    '^@/pages/(.*)$': '<rootDir>/pages/$1',
  },
  globals:{
    "ts-jest":{
      "tsconfig":"tsconfig.jest.json"
    }
  }
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)

**mocks 폴더

추후 mocking 데이터 생성을 위한 폴더

css, image import 를 위해서

  • fileMock.js

  • styleMock.js

  • next/image.tsx

**fileMock.js

jest에 이미지 파일 주입을 위해서

(module.exports = "/test-file-stub")

**styleMock.js

jest 에 css 파일 주입을 위해서

module.exports={};

**next/image.tsx

next/image 객체 모킹을 위해 (해당 파일 설정안해주면, 태그 사용시 옵션 설정안해주면 에러남.

import * as React from 'react'

const mock = (props: { children: React.ReactElement }): React.ReactElement => {
  return <>{props.children}</>
}

export default mock

**jest.setup.js 세팅

jest 에 testing-library 주입

jest 에서 canvas 테스트를 위한 jest-canvas-mock 주입

import '@testing-library/jest-dom'
import 'jest-canvas-mock';

**tsconfig.jest.json 세팅

jest 타입스크립트에서 JSX 문법을 사용할 수 있도록 하기 위해서 따로 세팅 필요함.

{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "jsx": "react-jsx" // html문법을 사용하기 위해 compilerOptions 추가해줘야 함.
    }
}

3. Redux 추가

  • utils.tsx 함수 추가
    - 테스트 대상에 포함되는 리듀서를 주입한다.
    • next/router API 를 사용하기 위해, 해당 객체를 모킹한다.

tests/utils.tsx

import React from "react";
import { render as rtlRender } from "@testing-library/react";
import { configureStore } from "@reduxjs/toolkit";
import { Provider } from "react-redux";


// 테스트 대상에 포함되는 리듀서 import 
import counter from '../store/modules/counter';
import admin from "../store/modules/admin";
import depthInfo from "../store/modules/depthInfo";
import selected from "../store/modules/selected";
import worst10 from "../store/modules/worst10";
import login from "../store/modules/login";

// 테스트할 대상을 감쌀 wrapper 에 store 주입
function render(
  ui:any,
  {
    preloadedState,
    store = configureStore({ reducer: {
      // 테스팅할 리듀서 주입 
      counter:counter,
      admin:admin,
      depthInfo:depthInfo,
      selected:selected,
      worst10:worst10,
      login: login
    
  }, preloadedState }),
    ...renderOptions
  }:any= {}
) {
  function Wrapper({ children } :any) {
    return <Provider store={store}>{children}</Provider>;
  }
  return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
}

// re-export everything
export * from "@testing-library/react";
// override render method
export { render };



// 테스트에 mock router 주입
export function createMockRouter(router: Partial<NextRouter>): NextRouter {
  return {
    basePath: "",
    pathname: "/",
    route: "/",
    query: {},
    asPath: "/",
    back: jest.fn(),
    beforePopState: jest.fn(),
    prefetch: jest.fn(),
    push: jest.fn(),
    reload: jest.fn(),
    replace: jest.fn(),
    events: {
      on: jest.fn(),
      off: jest.fn(),
      emit: jest.fn(),
    },
    isFallback: false,
    isLocaleDomain: false,
    isReady: true,
    defaultLocale: "en",
    domainLocales: [],
    isPreview: false,
    ...router,
  };
}
profile
프린이탈출하자
post-custom-banner

1개의 댓글

comment-user-thumbnail
2022년 3월 24일

잘 봤습니당 ~!

답글 달기