ABOUT ME

-

Today
-
Total
-
  • Storybook과 Vitest에서 MSW 모킹 핸들러 재사용하기
    Study/Frontend 2023. 8. 18. 12:03
    반응형

     

    프론트엔드 개발자의 작업 능률은 Storybook과 MSW를 만나기 전과 후로 나뉜다고 할 수 있다. 프론트엔드 개발을 하면서 가장 불편한 점 중 하나가 백엔드 개발에 대한 의존성이 높다는 점이다. 백엔드 API가 완성되어야 데이터 처리를 진행할 수 있는데, 보통 UI 작업을 먼저 진행해도 백엔드 작업이 끝나기 전까지 대기하는 상황이 종종 있었다. 물론 Mocking 데이터를 직접 만들어서 UI가 제대로 렌더링 되는지 확인할 수는 있었지만 꽤나 번거로운 일이었다.

    하지만 Storybook을 만나면서 백엔드 API 없이도 데이터에 대한 다양한 케이스를 만들어서 미리 정확한 UI를 만들어 보며 시각적 테스트를 진행할 수 있게 됐다. 그리고 MSW를 도입하면서 API가 완성되지 않아도 백엔드팀에서 Schema만 받을 수 있다면 데이터 처리 작업도 수월하게 진행할 수 있었다. API 리스폰스를 직접 변경하면서 다양한 케이스의 UI를 바로 확인하면서 효율적으로 개발할 수 있었다. 

     

    그리고 훌륭한 두 가지 도구에 Vitest 라는 테스트 러너를 기반으로 테스트 코드를 작성하게 됐는데, 여기서 문제점이 발생했다. Storybook과 테스트 파일에서 MSW로 모킹하는 코드가 중복으로 작성되고 있었다. 이를 해결하기 위해 Storybook에서 작성한 Story 컴포넌트를 Vitest 테스트에서 재활용하고, 중복 작성된 MSW 코드를 제외하고 테스트 코드를 깔끔하게 정리한 방법을 아래에서 소개하도록 하겠다.

     

    📍프로젝트 환경

    electron-vite + React + TypeScript

     

    📍테스트 코드 작성 흐름

    1. 시각적 테스트
      • Storybook에 Story 작성
      • Story에 MSW 모킹 코드 추가
    2. 컴포넌트 단위 통합 테스트
      • Vitest 기반 test 파일에서 작성한 Story Import 해서 재사용
      • 스토리에 추가된 MSW 모킹 재사용

     

    📍테스트 도구 설치

    yarn add -D vitest @storybook/testing-react @testing-library/jest-dom @testing-library/react jsdom msw-storybook-addon
    • Vitest : Vite 환경에서 테스트를 진행할수 있도록 도와주는 프레임워크 (Jest와 호환)
    • @testing-library/jest-dom : Dom 요소 matcher를 제공하는 라이브러리
    • @testing-library/react : React 컴포넌트를 테스팅할 수 있도록 도와주는 라이브러리
    • msw-storybook-addon : Storybook에서 MSW 모킹을 도와주는 라이브러리
    • @storybook/testing-react : 단위 테스트에서 스토리를 재사용 할 수 있도록 도와주는 라이브러리
    • jsdom : node 환경에서도 브라우저 환경을 테스트할 수 있도록 Dom 구현체를 제공해 주는 라이브러리

     

    📍Vitest Configuration

    import { defineConfig } from 'vitest/config'
    import { resolve } from 'path'
    
    export default defineConfig({
      test: {
        globals: true,
        environment: 'jsdom',
        setupFiles: 'vitest-setup.ts',
        alias: {
          '@renderer': resolve('src/renderer/src')
        }
      },
      build: {
        rollupOptions: {
          input: {
            index: resolve(__dirname, 'src/renderer/index.html')
          }
        },
        sourcemap: true
      }
    })

    .storybook/preview.tsx

     

    Vitest의 주요 장점 중 하나는 Vite와의 통합 구성이 가능하다는 것이다. Vitest를 사용하기 전에 Vite 환경에 Jest를 사용해보려고 했는데 Jest를 실행하기 위해 여러 디펜던시들이 필요했고, 설정도 Vitest보다 복잡했다. 그래서 차라리 Vite 환경에는 Vitest를 사용하는 것이 더 적합하다는 생각이 들었다.

    간단하게 vite.config.ts 파일에서 test 프로퍼티에 테스트 관련 옵션을 추가해 주면 테스트 가능한 환경이 된다. 또한, resolve.alias가 설정되어 있다면 그 설정 그대로 테스트 코드에서도 활용 가능하다.

     

    하지만 현재 프로젝트에서 사용하는 electron-vite에서 vitest 설정을 지원하지 않고 있다. (관련 Github Issue)

    따라서 기존 vite configuration 파일이었던 electron.vite.config.ts 에서 테스팅 관련 설정이 불가하므로 vite.config.ts 파일을 생성해서 테스팅 및 빌드 관련 옵션을 추가해줘야 한다.

     

    1. build 프로퍼티의 rollupOptions에서 entryPoint를 지정해 준다.
    2. test 프로퍼티에 Vitest 관련 옵션을 추가한다.
      • globals: vitest는 글로벌 API를 기본적으로 제공하지 않는다. 하지만 Jest와 같이 describe, it, expect 함수를 글로벌로 사용하고 싶다면 true로 설정해 준다. 그리고 타입스크립트가 작동하려면 tsconfig.json에 types 필드에 vitest/globals를 추가해 준다.
      • environment: vitest는 기본적으로 node 환경에서 실행되기 때문에 브라우저 환경을 테스트하기 위해서는 jsdom으로 설정해줘야 한다.
      • setupFiles: 테스트 전에 실행되는 파일의 경로이다.
      • alias: 테스트 파일에서 사용할 경로 별칭을 설정해 줄 수 있다.

     

    📍Storybook에서 MSW 사용하기

    1. MSW 초기화 함수 실행 및 MSW addon loader를 글로벌하게 제공하도록 설정한다.

    import React from 'react'
    import type { Preview } from '@storybook/react'
    import '../src/renderer/src/config/index.css'
    import { initialize, mswLoader } from 'msw-storybook-addon'
    import { handlers } from '../src/renderer/src/mocks/handler'
    import SampleProvider from '../SampleProvider'
    
    initialize()
    
    const preview: Preview = {
      parameters: {
        msw: {
          handlers //optional
        }
      },
      loaders: [mswLoader],
      decorators: [
        (Story) => (
          <SampleProvider>
            <Story />
          </SampleProvider>
        )
      ]
    }
    
    export default preview

    .storybook/preview.tsx

     

    msw-storybook-addon의 initialize 함수 호출해서 스토리북에서 MSW를 사용할 수 있도록 MSW를 초기화해 준다.

    만약 전체 스토리에서 전역적으로 필요한 리퀘스트 핸들러가 있다면, parameters 속성에서 MSW로 모킹한 리퀘스트 핸들러들을 넣어준다. 그렇지 않다면 이 속성은 생략해 준다.

    그리고 MSW addon loader를 글로벌하게 제공하도록 loaders에 addon을 추가한다.

     

    번외로 Storybook에서 Context Provider를 사용하는 법은 decorators를 활용하면 된다. decorators는 스토리에 필요한 요소들로 감싸서 렌더링 하는 데 사용된다. 이를 활용해서 글로벌로 Context Provider로 감싸서 스토리를 렌더링 하면 스토리에서 context에 접근할 수 있다.

     

    2. Storybook에서 기존에 사용하던 service worker 파일을 읽을 수 있도록 static 파일 경로 설정을 해준다.

    import type { StorybookConfig } from '@storybook/react-vite'
    
    const config: StorybookConfig = {
      staticDirs: ['../src/renderer'], 
    }
    export default config

    .storybook/main.ts

     

    electron-vite에서 제공하는 템플릿에서 정적 에셋을 담는 폴더는 src/renderer 폴더에 해당하므로 service worker 등록에 필요한 파일은 여기에 위치해 있다. 따라서 Storybook 역시 정적 에셋 폴더 위치를 동일한 위치로 지정해야 MSW가 작동할 수 있다.

    보통 React 애플리케이션은 npx msw init public/ 명령어를 통해 public 폴더에 생성한다.

     

    3. MSW를 브라우저와 노드 환경에 따라 실행되도록 구성한다.

    동일한 리퀘스트 핸들러를 공유할 수 있지만 실행되는 환경마다 다르게 프로세스를 구성한다.

    import { setupServer } from 'msw/node'
    import { handlers } from '../../mocks/handlers'
    
    export const server = setupServer(...handlers)

    server.ts

    import { setupWorker } from 'msw'
    import { handlers } from '../../mocks/handlers'
    
    export const worker = setupWorker(...handlers)

    browser.ts

    위와 같이 노드와 브라우저 각각 서비스 워커를 별도로 등록해 준다.

     

    import '@testing-library/jest-dom'
    import { server } from './src/renderer/src/test/env/server'
    
    beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
    
    afterAll(() => server.close())
    
    afterEach(() => server.resetHandlers())

    vitest-setup.ts

    테스트 별로 모킹에 의한 사이드 이펙트를 없애기 위해 테스트 수행 후 모킹한 핸들러를 초기화하는 코드를 작성한다.

    그리고 이 파일을 각 테스트마다 실행할 수 있도록 vite.config.ts 파일에서 setupFiles에 위 파일 경로를 작성해 준다.

     

    import { worker } from './test/env/browser'
    
    if (process.env.NODE_ENV === 'development') {
      worker.start()
    }
    

    src/renderer/src/main.tsx

    브라우저에서 실행할 서비스 워커는 렌더러 폴더의 main 파일에서 등록해 준다.

     

    4. 스토리에서 MSW 모킹 하는 코드를 추가한다.

    import Sample from './Sample'
    import { sampleHandlers } from '@renderer/mocks/handlers/sample-handler'
    
    export const Default: Story = {
      render: (args) => <Sample />
      parameters: {
        msw: {
          handlers: sampleHandlers
        }
      }
    }
    

    sample.stories

    parameters에 스토리에서 필요한 모킹 핸들러를 추가해 준다. 위와 같이 작성하면 컴포넌트 내부에서 API를 호출했을 때 MSW가 가로채서 모킹 한 리스폰스로 응답한다.

     

    📍테스트에 사용할 customRender 함수 생성

    전역으로 공급되는 요소들을 매번 테스트 코드에 작성하기 불편하므로 render 함수를 커스텀해서 사용하는 것이 좋다. 아래에서는 @testing-library/react의 render 함수를 래핑하는 customRender 함수를 생성해서 export 해서 사용한다.

    import { RenderOptions, render } from '@testing-library/react'
    import { server } from '../env/server'
    import SampleProvider from '../SampleProvider'
    
    function mockStoryHandlers(story) {
      server.use(...(story.type.parameters?.msw?.handlers || []))
    }
    
    interface IProvidersProps {
      children: React.ReactElement
    }
    
    const AllTheProviders = ({ children }: IProvidersProps) => {
      return (
        <SampleProvider>
    		{children}
        </SampleProvider>
      )
    }
    
    const customRender = (ui: React.ReactElement, options?: Omit<RenderOptions, 'queries'>) => {
      mockStoryHandlers(ui)
      return render(ui, { wrapper: AllTheProviders, ...options })
    }
    
    export * from '@testing-library/react'
    
    export { customRender as render }
    1. mockStoryHandlers 함수를 통해 테스트할 스토리에 parameters?.msw?.handlers 가 있는지 확인하고 스토리에서 사용한 API를 모킹하도록 한다.
    2. Context Provider로 렌더링 할 요소를 감싸준다.

     

    📍VITEST에서 MSW 사용 및 Story 재사용

    import { render, waitFor } from '@renderer/test/utils/test-utils'
    import { composeStories } from '@storybook/testing-react'
    import * as SampleStories from './Sample.stories'
    
    const { SampleAmpty } = composeStories(SampleStories)
    
    describe('Sample', () => {
      test('Sample 리스트가 없는 경우 빈 화면을 표시한다', async () => {
        const { getByText } = render(<SampleAmpty />)
        await waitFor(() => expect(getByText('Empty')).toBeInTheDocument())
      })
    })
    1. composeStories 함수를 통해 테스트에서 스토리 컴포넌트를 재사용할 수 있도록 변환해 준다.
    2. 위에서 만들었던 customRender 함수로 렌더링을 해서 MSW 모킹 코드를 재사용하고 context를 전역적으로 사용할 수 있도록 한다.

     


    좋은 도구들을 최대한 활용해서 개발 능률을 최대치로 끌어올려보자!

     

     

    참고 자료

    https://fe-developers.kakaoent.com/2022/220317-integrate-msw-storybook-jest/

    https://testing-library.com/docs/react-testing-library/setup/#custom-render

    https://mswjs.io/docs/getting-started/integrate

    https://storybook.js.org/addons/msw-storybook-addon

    https://blog.mathpresso.com/msw로-api-모킹하기-2d8a803c3d5c

    https://vitest.dev/guide/

     

    반응형

    댓글