Picasso: 구성 요소 라이브러리를 테스트하는 방법

게시 됨: 2022-03-11

Toptal 디자인 시스템의 새 버전이 최근에 출시되어 사내 구성 요소 라이브러리인 Picasso의 거의 모든 구성 요소를 변경해야 했습니다. 우리 팀은 다음과 같은 도전에 직면했습니다. 회귀가 발생하지 않도록 하려면 어떻게 해야 할까요?

짧은 대답은 의외로 테스트입니다. 많은 테스트.

우리는 테스트의 이론적 측면을 검토하지 않으며 다양한 유형의 테스트, 그 유용성에 대해 논의하거나 애초에 코드를 테스트해야 하는 이유를 설명하지 않습니다. 우리 블로그와 다른 사람들은 이미 이러한 주제를 다루었습니다. 대신 우리는 테스트의 실제적인 측면에만 집중할 것입니다.

Toptal의 개발자가 테스트를 작성하는 방법을 알아보려면 계속 읽으십시오. 우리의 리포지토리는 공개되어 있으므로 실제 예제를 사용합니다. 추상화나 단순화가 없습니다.

테스트 피라미드

테스트 피라미드 자체는 정의되어 있지 않지만 그렇게 하면 다음과 같이 보일 것입니다.

테스트 피라미드 그림

Toptal의 테스트 피라미드는 우리가 강조하는 테스트를 보여줍니다.

단위 테스트

단위 테스트는 작성하기 쉽고 실행하기 쉽습니다. 테스트를 작성할 시간이 거의 없다면 첫 번째 선택이 되어야 합니다.

그러나 그것들은 완벽하지 않습니다. 어떤 테스트 라이브러리를 선택했는지(저희의 경우 Jest 및 React Testing Library[RTL])에 관계없이 실제 DOM이 없고 다른 브라우저에서 기능을 확인할 수 없지만 제거할 수는 있습니다. 복잡성을 없애고 라이브러리의 간단한 구성 요소를 테스트하십시오.

단위 테스트는 코드의 동작을 테스트하여 가치를 추가할 뿐만 아니라 코드의 전체 테스트 가능성을 확인합니다. 단위 테스트를 쉽게 작성할 수 없다면 코드가 잘못되었을 가능성이 있습니다.

시각적 회귀 테스트

100% 단위 테스트 적용 범위가 있다고 해도 구성 요소가 장치와 브라우저에서 좋아 보인다는 의미는 아닙니다.

시각적 회귀는 수동 테스트에서 특히 발견하기 어렵습니다. 예를 들어 버튼의 레이블이 1px 이동하면 QA 엔지니어가 알아차릴까요? 고맙게도 제한된 가시성 문제에 대한 많은 솔루션이 있습니다. LambdaTest 또는 Mabl과 같은 엔터프라이즈급 올인원 솔루션을 선택할 수 있습니다. Percy와 같은 플러그인을 기존 테스트에 통합할 수 있을 뿐만 아니라 Loki 또는 Storybook과 같은 DIY 솔루션(Picasso 이전에 사용)을 통합할 수 있습니다. 모두 단점이 있습니다. 일부는 너무 비싸고 다른 일부는 학습 곡선이 가파르거나 너무 많은 유지 관리가 필요합니다.

만세! Percy의 직접적인 경쟁자이지만 훨씬 저렴하고 더 많은 브라우저를 지원하며 사용하기 쉽습니다. 또 다른 큰 판매 포인트? 이것은 시각적 테스트를 위해 Storybook을 사용하지 않기를 원했기 때문에 중요한 Cypress 통합을 지원합니다. 사용 사례를 문서화해야 하기 때문이 아니라 시각적 테스트 범위를 보장하기 위해 스토리를 만들어야 하는 상황에 처했습니다. 그것은 우리 문서를 더럽히고 이해하기 어렵게 만들었습니다. 우리는 시각적 문서에서 시각적 테스트를 분리하고 싶었습니다.

통합 테스트

두 구성 요소에 단위 및 시각적 테스트가 있더라도 함께 작동한다는 보장은 없습니다. 예를 들어, 드롭다운 항목에서 사용할 때는 툴팁이 열리지 않지만 단독으로 사용할 때는 잘 작동하는 버그를 발견했습니다.

구성 요소가 잘 통합되도록 하기 위해 Cypress의 실험적 구성 요소 테스트 기능을 사용했습니다. 처음에는 저조한 성능에 불만이 있었지만 커스텀 웹팩 구성으로 개선할 수 있었습니다. 결과? 우리는 Cypress의 우수한 API를 사용하여 구성 요소가 함께 잘 작동하는지 확인하는 성능 테스트를 작성할 수 있었습니다.

테스트 피라미드 적용하기

이 모든 것이 실생활에서 어떻게 생겼습니까? Accordion 컴포넌트를 테스트해봅시다!

첫 번째 본능은 편집기를 열고 코드 작성을 시작하는 것일 수 있습니다. 나의 충고? 시간을 할애하여 구성 요소의 모든 기능을 이해하고 다루고자 하는 테스트 사례를 기록하십시오.

Picasso 구성 요소 라이브러리 데모 GIF

무엇을 테스트할 것인가?

테스트에서 다루어야 하는 사례에 대한 분석은 다음과 같습니다.

  • 상태 – 아코디언을 확장 및 축소할 수 있으며 기본 상태를 구성할 수 있으며 이 기능을 비활성화할 수 있습니다.
  • 스타일 – 아코디언에는 테두리 변형이 있을 수 있습니다.
  • 콘텐츠 – 라이브러리의 다른 단위와 통합할 수 있습니다.
  • 사용자 지정 – 구성 요소는 해당 스타일을 재정의하고 사용자 지정 확장 아이콘을 가질 수 있습니다.
  • 콜백 – 상태가 변경될 때마다 콜백을 호출할 수 있습니다.

Picasso 구성 요소 라이브러리 데모 GIF - 아코디언 구성 요소

테스트 방법?

이제 우리가 무엇을 테스트해야 하는지 알았으니 어떻게 해야 하는지 생각해 봅시다. 테스트 피라미드에는 세 가지 옵션이 있습니다. 우리는 피라미드의 섹션 사이에 최소한의 겹침으로 최대 범위를 달성하기를 원합니다. 각 테스트 케이스를 테스트하는 가장 좋은 방법은 무엇입니까?

  • 상태 – 단위 테스트는 상태가 그에 따라 변경되는지 평가하는 데 도움이 될 수 있지만 구성 요소가 각 상태에서 올바르게 렌더링되는지 확인하기 위한 시각적 테스트도 필요합니다.
  • 스타일 – 시각적 테스트는 다양한 변형의 회귀를 감지하기에 충분해야 합니다.
  • 내용 – 아코디언은 다른 많은 구성 요소와 함께 사용할 수 있으므로 시각적 및 통합 테스트의 조합이 최선의 선택입니다.
  • 사용자 지정 – 단위 테스트를 사용하여 클래스 이름이 올바르게 적용되었는지 확인할 수 있지만 구성 요소와 사용자 지정 스타일이 함께 작동하는지 확인하려면 시각적 테스트가 필요합니다.
  • 콜백 – 단위 테스트는 올바른 콜백이 호출되도록 하는 데 이상적입니다.

아코디언 테스트 피라미드

단위 테스트

전체 단위 테스트 모음은 여기에서 찾을 수 있습니다. 모든 상태 변경, 사용자 정의 및 콜백을 다루었습니다.

 it('toggles', async () => { const handleChange = jest.fn() const { getByText, getByTestId } = renderAccordion({ onChange: handleChange, expandIcon: <span data-test /> }) fireEvent.click(getByTestId('accordion-summary')) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) fireEvent.click(getByTestId('trigger')) await waitFor(() => expect(getByText(DETAILS_TEXT)).not.toBeVisible()) fireEvent.click(getByText(SUMMARY_TEXT)) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) expect(handleChange).toHaveBeenCalledTimes(3) })

시각적 회귀 테스트

시각적 테스트는 이 Cypress 설명 블록에 있습니다. 스크린샷은 Happo의 대시보드에서 찾을 수 있습니다.

다양한 구성 요소 상태, 변형 및 사용자 지정이 모두 기록된 것을 볼 수 있습니다. PR이 열릴 때마다 CI는 Happo가 저장한 스크린샷을 지점에서 찍은 스크린샷과 비교합니다.

 it('renders', () => { mount( <TestingPicasso> <TestAccordion /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders disabled', () => { mount( <TestingPicasso> <TestAccordion disabled /> <TestAccordion expandIcon={<Check16 />} /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders border variants', () => { mount( <TestingPicasso> <TestAccordion borders='none' /> <TestAccordion borders='middle' /> <TestAccordion borders='all' /> </TestingPicasso> ) cy.get('body').happoScreenshot() })

통합 테스트

이 Cypress 설명 블록에서 Accordion이 여전히 올바르게 작동하고 사용자가 사용자 지정 구성 요소와 상호 작용할 수 있다고 주장하는 "잘못된 경로" 테스트를 작성했습니다. 또한 추가 확신을 위해 시각적 어설션을 추가했습니다.

 describe('Accordion with custom summary', () => { it('closes and opens', () => { mount(<AccordionCustomSummary />) toggleAccordion() getAccordionContent().should('not.be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() toggleAccordion() getAccordionContent().should('be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() }) // … })

지속적인 통합

Picasso는 거의 전적으로 QA용 GitHub Actions에 의존합니다. 또한 준비된 파일의 코드 품질 검사를 위한 Git 후크를 추가했습니다. 최근에 Jenkins에서 GHA로 마이그레이션했으므로 설정은 아직 MVP 단계에 있습니다.

워크플로는 원격 분기의 모든 변경 사항에 대해 순차적으로 실행되며 통합 및 시각적 테스트는 실행 비용이 가장 많이 들기 때문에(성능 및 금전적 비용 모두) 마지막 단계입니다. 모든 테스트가 성공적으로 완료되지 않으면 pull 요청을 병합할 수 없습니다.

다음은 GitHub Actions가 매번 거치는 단계입니다.

  1. 종속성 설치
  2. 버전 제어 – 커밋 형식 및 PR 제목이 기존 커밋과 일치하는지 확인
  3. Lint – ESlint는 좋은 품질의 코드를 보장합니다.
  4. TypeScript 컴파일 – 유형 오류가 없는지 확인
  5. 패키지 컴파일 – 패키지를 빌드할 수 없으면 성공적으로 릴리스되지 않습니다. Cypress 테스트는 컴파일된 코드도 예상합니다.
  6. 단위 테스트
  7. 통합 및 시각적 테스트

전체 워크플로는 여기에서 찾을 수 있습니다. 현재 모든 단계를 완료하는 데 12분이 채 걸리지 않습니다.

테스트 가능성

대부분의 구성 요소 라이브러리와 마찬가지로 Picasso에는 다른 모든 구성 요소를 래핑해야 하고 전역 규칙을 설정하는 데 사용할 수 있는 루트 구성 요소가 있습니다. 이것은 두 가지 이유로 테스트를 작성하기 어렵게 만듭니다. 그리고 추가 상용구:

 import { render } from '@testing-library/react' describe('Form', () => { it('renders', () => { const { container } = render( <Picasso loadFavicon={false} environment='test'> <Form /> </Picasso> ) expect(container).toMatchSnapshot() }) })

테스트를 위한 전역 규칙을 사전 설정하는 TestingPicasso를 만들어 첫 번째 문제를 해결했습니다. 그러나 모든 테스트 케이스에 대해 선언해야 하는 것은 성가신 일입니다. 이것이 우리가 TestingPicasso에서 전달된 구성 요소를 래핑하고 RTL의 렌더링 함수에서 사용 가능한 모든 것을 반환하는 사용자 지정 렌더링 함수를 만든 이유입니다.

우리의 테스트는 이제 더 읽기 쉽고 쓰기 쉽습니다.

 import { render } from '@toptal/picasso/test-utils' describe('Form', () => { it('renders', () => { const { container } = render(<Form />) expect(container).toMatchSnapshot() }) })

결론

여기에 설명된 설정은 완벽하지 않지만 구성 요소 라이브러리를 만들 만큼 모험적인 사용자에게는 좋은 시작점입니다. 피라미드 테스트에 대해 많이 읽었지만 실제로 적용하는 것이 항상 쉬운 것은 아닙니다. 따라서 우리의 코드베이스를 탐색하고 실수와 성공에서 배우도록 초대합니다.

구성 요소 라이브러리는 UI와 상호 작용하는 최종 사용자와 고유한 응용 프로그램을 빌드하기 위해 코드를 사용하는 개발자라는 두 가지 유형의 대상에게 서비스를 제공하기 때문에 고유합니다. 강력한 테스트 프레임워크에 시간을 투자하면 모두에게 도움이 됩니다. 테스트 가능성 개선에 시간을 투자하면 유지 관리자와 라이브러리를 사용(및 테스트)하는 엔지니어에게 도움이 됩니다.

우리는 코드 커버리지, 종단 간 테스트, 버전 및 릴리스 정책과 같은 것에 대해 논의하지 않았습니다. 이러한 주제에 대한 간단한 조언은 다음과 같습니다. 자주 릴리스하고, 적절한 의미론적 버전 관리를 실행하고, 프로세스의 투명성을 유지하고, 라이브러리에 의존하는 엔지니어에 대한 기대치를 설정하십시오. 다음 게시물에서 이 주제에 대해 더 자세히 알아볼 수 있습니다.