- 프로젝트: loplat UI
- 키워드: storybook addon interactions, Storybook test runner, mdx story, csf story, jest
- 상황
- loplat UI에 있던 기존 jest test file들은 cli를 통해서 동작하며 테스트의 진행 과정을 눈으로 확인하기 힘들다는 단점이 있다.
- loplat UI는 비지니스 로직보다는 ui 중심이므로, 스토리북과 연동된 테스트를 실행하면서 테스트 진행 상황을 스토리북에서도 확인할 수 있으면 좋을 것이다.
- 새로 알게된 'Storybook Addon Interactions'와 'Storybook Test Runner'를 도입하는 과정을 소개하고자한다.
- 해결 과정 1 (storybook addon interactions)
- interactions addon(https://storybook.js.org/docs/essentials/interactions) 공식 문서에 나와있는대로 패키지 설치와 기본 configuration 설정을 완료한다.
- 이제 interactions 테스트를 위한 코드를 작성해야하는데, 여기서 유의할 점은 loplat UI에는 이미 mdx와 csf 포맷으로 된 두 종류의 스토리가 존재한다는 것이다.
따라서 storybook test code를 담고 있는 wrapper function을 'storybookTest.ts' 파일에 작성하고, 해당 함수를 mdx/csf storybook file에 import하여 기존의 스토리를 wrapper function으로 감싸는 방식을 취하기로 한다.(다음 단계에서 자세히 설명) - 먼저 일반적으로 많이 쓰이는 csf(Component Story Format) story를 예로 설명한다. loplat UI의 RadioButton story가 csf 방식으로 작성되어 있다. (https://loplat-ui.web.app/?path=/story/components-radiobutton-group--default-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이 생기고, 자동으로 테스트 코드가 실행된다!
각 step을 클릭하여 원하는 테스트를 다시 실행할 수도 있어 test debugging이 쉬워졌다. - 다음은 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이 적용된다! - 예시) Input story: https://ui.loplat.com/?path=/story/components-input--default-story
- interactions addon(https://storybook.js.org/docs/essentials/interactions) 공식 문서에 나와있는대로 패키지 설치와 기본 configuration 설정을 완료한다.
- 해결과정 2 (storybook test runner)
- 앞서 interactions addon을 추가해보았다.
하지만 테스트 결과를 storybook에서만 확인 가능하기 때문에, CI에서 (bitbucket pipeline) 테스트를 모두 통과하는지 검사하는 과정을 추가하고자 한다. - Storybook Test Runner Addon 공식문서(https://storybook.js.org/addons/@storybook/test-runner) 가이드대로 설치를 진행한다.
- 스토리북을 실행하고, 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에 대해 테스트가 잘 작동한다!
- 마지막으로 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에서 에러가 발생했다.
에러 문구대로 '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 로직이 잘 작동한다!
- 앞서 interactions addon을 추가해보았다.
- 의의
- storybook과 jest test를 연동하여 UI library와 어울리는 테스트 코드를 작성할 수 있게 되었다.
- 개발자와 스토리북 데모사이트 사용자들이 test step을 하나씩 실행하며 디버깅할 수 있게 되었다.
- storybook, bitbucket, playwright 공식문서만으로 깔끔하게 트러블슈팅을 하였다.
'프론트엔드' 카테고리의 다른 글
Hammer.js로 swipe 이벤트 감지하기 (0) | 2022.04.02 |
---|---|
모바일 기기에서 scroll 관련 버그 대응하기 (0) | 2022.04.02 |
JS library에서 cjs, esm format 모두 지원하기 (0) | 2022.03.09 |
이미지 용량 최적화하기(feat. picture tag, sprite image) (0) | 2022.02.24 |
chrome extension New Tab customizing을 위한 개발 환경 만들기 (0) | 2022.02.17 |