본문 바로가기

프론트엔드

Storybook에서 user interactions 테스트하기

  • 프로젝트: loplat UI
  • 키워드: storybook addon interactions, Storybook test runner, mdx story, csf story, jest
  • 상황
    1. loplat UI에 있던 기존 jest test file들은 cli를 통해서 동작하며 테스트의 진행 과정을 눈으로 확인하기 힘들다는 단점이 있다.
    2. loplat UI는 비지니스 로직보다는 ui 중심이므로, 스토리북과 연동된 테스트를 실행하면서 테스트 진행 상황을 스토리북에서도 확인할 수 있으면 좋을 것이다.
    3. 새로 알게된 'Storybook Addon Interactions'와 'Storybook Test Runner'를 도입하는 과정을 소개하고자한다.
  • 해결 과정 1 (storybook addon interactions)

    1. interactions addon(https://storybook.js.org/docs/essentials/interactions) 공식 문서에 나와있는대로 패키지 설치와 기본 configuration 설정을 완료한다.

      package 설치와 storybook config 설정


    2. 이제 interactions 테스트를 위한 코드를 작성해야하는데, 여기서 유의할 점은 loplat UI에는 이미 mdx와 csf 포맷으로 된 두 종류의 스토리가 존재한다는 것이다.
      따라서 storybook test code를 담고 있는 wrapper function을 'storybookTest.ts' 파일에 작성하고, 해당 함수를 mdx/csf storybook file에 import하여 기존의 스토리를 wrapper function으로 감싸는 방식을 취하기로 한다.(다음 단계에서 자세히 설명)


    3. 먼저 일반적으로 많이 쓰이는 csf(Component Story Format) story를 예로 설명한다. loplat UI의 RadioButton story가 csf 방식으로 작성되어 있다. (https://loplat-ui.web.app/?path=/story/components-radiobutton-group--default-story)

      RadioButton story

      테스트 코드인 'storybookTest.ts'를 작성한다.
      기존의 story를 인자로 받아서 play 속성을 추가해주고 story를 다시 return하는 wrapper function이다.
        // RadioButton/storybookTest.ts
      import { RadioButton } from './';
      import { ComponentStory } from '@storybook/react';
      import { expect } from '@storybook/jest';
      import { userEvent, within } from '@storybook/testing-library';
      import { radioValues } from './index.stories';
      
      export function runTest(story: ComponentStory<typeof RadioButton>): ComponentStory<typeof RadioButton> {
        story.play = async ({ canvasElement }) => {
          const canvas = within(canvasElement);
          const form = canvas.getByTestId('loplat-ui__form');
          const availableRadioButton = canvas.getByLabelText(radioValues[0]);
          const disabledRadioButton = canvas.getByLabelText(radioValues[4]);
      
          expect(form).toHaveLength(radioValues.length);
      
          // click available radio button
          userEvent.click(availableRadioButton);
          expect(availableRadioButton).toBeChecked();
      
          // click disabled radio button
          userEvent.click(disabledRadioButton);
          expect(disabledRadioButton).toBeDisabled();
        };
      
        return story;
      }

      그리고 csf story file에서 Default story를 runTest 함수로 감싸주었다.
        // RadioButton/index.stories.tsx
      const Template: ComponentStory<typeof RadioButton> = (args: RadioButtonProps) => <RadioButtonGroup {...args} />;
      
      export const Default = Template.bind({});
      Default.args = { name: 'test' };
      runTest(Default);

      이제 storybook을 실행하면 Interactions addon tab이 생기고, 자동으로 테스트 코드가 실행된다!
      RadioButton csf storybook


      각 step을 클릭하여 원하는 테스트를 다시 실행할 수도 있어 test debugging이 쉬워졌다.
      debugging 가능한 test


    4. 다음은 mdx 방식의 story에 interaction test를 도입할 차례이다. loplat UI의 대부분의 컴포넌트는 문서화를 위해 mdx 방식의 story로 작성되어있다. 그 중, Button Component에 먼저 도입해보자.(https://loplat-ui.web.app/?path=/story/components-button-button--default-story)
      똑같은 방식으로 Button의 storybookTest 코드를 작성한다.
        // Button/storybookTest.ts
      import { Button } from './';
      import { ComponentStory } from '@storybook/react';
      import { expect } from '@storybook/jest';
      import { userEvent, within } from '@storybook/testing-library';
      
      export function runTest(story: ComponentStory<typeof Button>): ComponentStory<typeof Button> {
        story.play = async ({ args, canvasElement }) => {
          const canvas = within(canvasElement);
          const button = canvas.getByRole('button');
      
          // render children
          if (typeof args.children === 'string') {
            expect(button).toHaveTextContent(args.children);
          }
      
          // click button
          userEvent.click(button);
          expect(args.onClick).toHaveBeenCalled();
      
          // click outside
          userEvent.click(canvasElement);
          expect(button).not.toHaveFocus();
        };
      
        return story;
      }​

      csf와 비슷한 방식으로 mdx story에 runTest 함수를 씌운다.

        // Button/index.stories.mdx
      import { runTest } from './storybookTest.ts'
      ...
      export const Template = (args) => <Button {...args} />;
      ...
      <Canvas>
        <Story name="default" args={{ children: 'default', role: 'button' }}>
          {runTest(Template.bind({}))}
        </Story>
      </Canvas>


      mdx 방식의 story에도 interactions addon이 적용된다!
      Button mdx storybook
    5. 예시) Input story: https://ui.loplat.com/?path=/story/components-input--default-story

 

  • 해결과정 2 (storybook test runner)

    1. 앞서 interactions addon을 추가해보았다.
      하지만 테스트 결과를 storybook에서만 확인 가능하기 때문에, CI에서 (bitbucket pipeline) 테스트를 모두 통과하는지 검사하는 과정을 추가하고자 한다.

    2. Storybook Test Runner Addon 공식문서(https://storybook.js.org/addons/@storybook/test-runner) 가이드대로 설치를 진행한다.
      package 설치와 storybook config, package.json scripts 설정


    3. 스토리북을 실행하고, test-storybook을 실행하면 기대한 것처럼 동작하지 않는다.
        yarn storybook // localhost:6006
      yarn test-storybook

      기본적으로 mdx 파일의 story를 인식하지 못하고, export default가 없는 문서화용 스토리까지 테스트를 해버리는 것이다.

      하지만 공식문서에서 이 문제에 대한 해결법을 찾을 수 있다. 바로 stories.json mode를 사용하는 방법인데, loplat UI의 story(mdx 포함)들을 빌드하여 stories.json 파일에 indexing 해두는 것이다. (결과: https://ui.loplat.com/stories.json)
      그럼 mdx story들도 모두 인덱싱 되기 때문에 test-storybook에서 인식하고 테스트를 진행시킨다.

      먼저 .storybook/main.js 에서 stories json mode로 빌드함을 명시하고,
        // .storybook/main.js
      features: {
        buildStoriesJson: true,
        interactionsDebugger: true,
      },

      test runner scripts에서 stories json mode로 테스트함을 명시한다.
        // package.json
      "scripts": {
        "storybook": "start-storybook -p 6006",
        "test-storybook": "test-storybook --stories-json"
      },

      이제 test-runner를 실행하면 모든 story에 대해 테스트가 잘 작동한다!
      yarn test-runner
       
    4. 마지막으로 bitbucket pipeline에 yarn test-runner를 실행하는 step을 추가하고자 한다.
        # bitbucket-pipelines.yml
      - step:
        name: Test Storybook
        caches:
          - node
        script:
          - yarn test-storybook --url https://loplat-ui.web.app


      예상과 달리 이 step에서 에러가 발생했다.
      bitbucket pipeline error

      에러 문구대로 'npx playwright install', 'npx playwright install-deps' 와 같은 script를 추가해봤지만 효과가 없었다.
      그러다 playwright 공식문서에서 해답을 찾을 수 있었는데, pipeline step에 docker image를 추가하면 되는 것이었다.
      (https://playwright.dev/docs/ci#bitbucket-pipelines)

        - step:
        name: Test Storybook on browser using Docker
        image: mcr.microsoft.com/playwright:v1.19.2-focal #v.1.20에서 docker issue가 있는 것으로 파악되어 v1.19.2를 사용하도록 명시
        caches:
          - node
        script: 
          - yarn test-storybook --url https://loplat-ui.web.app


      이제 CI에서 storybook test 로직이 잘 작동한다!
      bitbucket pipeline success
  • 의의

    1. storybook과 jest test를 연동하여 UI library와 어울리는 테스트 코드를 작성할 수 있게 되었다.
    2. 개발자와 스토리북 데모사이트 사용자들이 test step을 하나씩 실행하며 디버깅할 수 있게 되었다.
    3. storybook, bitbucket, playwright 공식문서만으로 깔끔하게 트러블슈팅을 하였다.