React 테스트 자동화 - React teseuteu jadonghwa

가장 비용이 적게 든다는 단위 테스트를 열심히 작성한다. 하나의 컴포넌트에 필요한 테스트 케이스가 10개를 넘어갈 때도 있지만 옆에서 테스트는 필요하다고 한다. 아직 하나의 화면도 제대로 렌더링되고 있지 않지만 괜찮다. 테스트 커버리지가 높으니 나중에 큰 도움이 될 것이다.

작업이 남은 컴포넌트가 수십개는 되는 상황인데 갑자기 디자인이 변경되었다. 컴포넌트의 계층 구조가 변경되면서 내 컴포넌트 테스트 코드는 하나 둘씩 빨간 피를 토하기 시작했다.

아 잠깐만…

UI 검증을 위한 테스트

컴포넌트는 브라우저에 렌더링이 될 때, 부모 컴포넌트의 영향을 많이 받는다. 그리고 다른 컴포넌트에 영향을 준다. 애초에 그럴 수 밖에 없다. children에 또다른 컴포넌트가 들어온다면 CSS Style 속성에 의해 애써 만든 버튼 컴포넌트의 UI가 깨질 수 있다.

여기에서 핵심은 UI가 언제든 깨질 수 있다는 것이다. 위에서 언급한 1~4번에 모두 제대로 테스트를 통과함에도 불구하고 UI는 언제든지 브라우저에서 깨질 수 있다.

즉 HTML Tree 상 제대로 된 위치에 렌더링이 되었더라도 부모 컴포넌트에 의해 UI가 망가질 수 있다. className이 제대로 변경되었다고 하더라도 스타일이 제대로 안 들어갔을 수도 있다.

그렇다면 우리는 이 버튼 컴포넌트의 테스트 코드를 작성함에 있어 변경에 자유로울 수 있을까? 버그가 없다고 확신할 수 있을까?

왜 테스트해야 하는지 명심하기

사용자가 우리가 만든 애플리케이션을 사용할 때, 의도한대로 동작하는 것에 대해 확신을 갖기 위해 테스트를 작성한다. 우리가 의도한 것이 사용자의 동작에 의해 제대로 작동하는지를 테스트해야 한다. 다시 버튼 컴포넌트의 예시로 돌아가자. 1~4번 중, 사용자의 동작은 무엇일까?

사용자가 버튼을 클릭하는 행위. 4번 뿐이다. 우린 4번에 집중하면 된다.

‘동작’에 집중하자

정적인 UI는 잠시 미뤄두고 동적인 동작에 집중하자. 하지만 4번 그대로를 테스트 하지 않는다.

여기서 ‘그대로’ 테스트 한다는 것은 이벤트에 클릭 이벤트가 제대로 트리거(trigger or dispatchEvent)되는지 테스트 하는 것을 의미하며 이것은 플랫폼을 믿지 않는 것이라고 생각한다.

document.addEventListener 를 통해 click 이벤트를 등록했다면 브라우저를 믿지 못하는 것이고 JSX의 onClick을 통해 이벤트를 등록했다면 React 라이브러리를 믿지 못하는 것이다.

클릭 이벤트는 발생하겠지.

사용자가 버튼을 클릭했을 때, 이벤트가 발생하여 어떠한 부수 효과(side effect)를 가져오는가를 테스트 한다. 버튼을 클릭하여 어떤 컴포넌트의 상태가 변경될 수 있고 다른 페이지로 이동할 수도 있고 상태에 따라 모달 컴포넌트가 보이게 될 수도 있다.

방금 언급한 것들이 애플리케이션의 흐름(Flow)이 되며 프론트엔드 환경에서 비즈니스 로직에 해당된다. 그리고 이것은 개발자가 애플리케이션 코드를 작성할 때 의도한 동작에 해당된다.

화면이 잘 그려질지, 내가 onClick props로 등록한 이벤트가 제대로 발생할지는 사용하고 있는 플랫폼을 믿자.(예를 들면 React) 믿고 이 부분에 대한 단위테스트는 작성하지 않는다.

그래서 무엇을 테스트하는가?

동작을 테스트할 것이고 동작이란 사용자로부터 발생한 이벤트의 부수효과를 의미한다.

그리고 이 부수 효과들이 발생하게 되는 조건들과 결과를 테스트 한다. 대부분의 테스트 코드가 커버(cover)하게 되는 영역은 뒤에서 설명할 Store의 selector, reducer, 그리고 middleware 함수가 될 것이다.

무엇을 테스트하지 않을 것인가?

무엇을 테스트 할 지 결정했다면 무엇을 테스트하지 않을지도 자연스럽게 결정이 될 것이다. 사실 프론트엔드 테스트 전략을 세울 때는 무엇을 테스트하지 않을 것인가를 고민하는 것이 더 중요하다고 생각한다. 테스트에 들어가는 비용이 많이 들어가기 때문이다.

개발자는 평균적으로 버그를 고치는데 매주 4-8 시간 정도를 사용한다고 합니다. 버그가 프로덕션(production)에 몰래 끼어들기 시작하면, 상황은 나빠지기만 합니다. 이 경우, 버그를 고치는데에는 5-10배 더 많은 시간이 듭니다. 이러한 이유로 UI 테스팅이 질 높은 사용자 경험을 전달하는데 필수적이지만, 동시에 엄청난 시간 낭비가 될 수 있습니다. 코드를 변경할 때마다 모든 테스트를 수작업으로 하나하나 하려하면 일이 지나치게 커집니다.

이런 작업 흐름(workflow)를 자동화해서 개발자가 코드를 push할 때마다 테스트가 작동하게 만들 수 있습니다. 테스트들은 백그라운드에서 실행되고 완료되면 결과를 보고합니다. 이는 회귀(regressions) 에러를 자동으로 감지할 수 있게 해줍니다.

이 챕터에서는 어떻게 이런 작업 흐름을 Github Actions을 이용해 구축할 수 있는지 보여줍니다. 더불어, 테스트 실행을 최적화하는 방법도 짚어보겠습니다.

지속적인 UI 테스트

코드 리뷰는 개발자가 되는데 중요한 부분입니다. 여러분이 버그를 조기에 발견하고 높은 코드 품질을 유지할 수 있게 돕습니다.

풀 리퀘스트(Pull request)가 프로덕션 코드를 망가트리지 않는다는 걸 보장하기 위해서, 여러분은 보통 코드를 pull 해와서 테스트 스위트를 로컬에서 실행해볼 겁니다. 이런 작업은 작업 흐름을 끊고 시간도 오래 걸립니다. 지속적 통합(CI)을 통해서, 손으로 하나하나 개입하지 않고도 테스트의 이득은 모두 챙길 수 있습니다.

여러분은 UI를 바꿀 수도, 새로운 기능을 만들 수도, 의존성을 최신으로 업데이트할 수도 있습니다. 풀 리퀘스트를 열었을 때, CI 서버는 자동적으로 포괄적인 UI테스트를 -시각적 요소, 구성, 접근성, 상호작용, 사용자 흐름- 실행해줍니다.

테스트 결과로 PR 뱃지를 받게 되고, 이를 통해 모든 검사 결과를 개략적으로 확인할 수 있습니다.

React 테스트 자동화 - React teseuteu jadonghwa

풀 리퀘스트가 모든 품질 검사를 통과했는지 한눈에 알 수 있습니다. 그 답이 "네(yes)"라면 실제 코드를 리뷰하는 단계로 넘어가면 됩니다. 그렇지 못한 경우에는, 로그를 살펴보면서 무엇이 잘못됐는지 찾습니다.

"테스트는 의존성 최신화를 자동화해도 저에게 확신을 줍니다. 테스트가 통과하면, 최신화된 사항을 merge 시키죠."

— Simon Taggart, Twilio 수석 엔지니어(Principal Engineer)

튜토리얼

지난 다섯 챕터들에서는 Taskbox UI의 다양한 면을 어떻게 테스트할 수 있는지 보여주었습니다. 여기에 더해서 우리는 이제 Github Actions를 이용해 지속적 통합(CI)을 구축해볼 것입니다.

CI 구축하기

먼저 저장소에 .github/workflows/ui-tests.yml파일을 만들면서 시작해봅시다. 한 작업 흐름(workflow)은 자동화하고 싶은 여러 작업(jobs)으로 이루어집니다. 작업 흐름은 commit을 push한다던가 풀 리퀘스트를 만드는 것 같은 이벤트(events)에 의해 트리거됩니다.

우리가 만들 작업 흐름은 코드를 저장소의 브랜치에 push하는 순간 실행되고, 3가지 작업(job)을 가지고 있습니다.

  • 상호작용 테스트와 접근성 검사를 Jest로 수행합니다.
  • 시각적 요소 그리고 구성 테스트를 크로마틱(Chromatic)으로 수행합니다.
  • 사용자 흐름(user flow) 테스트를 사이프레스(Cypress)로 수행합니다.

.github/workflows/ui-tests.yml

name: 'UI Tests'

on: push

jobs:
  # Run interaction and accessibility tests
  interaction-and-accessibility:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '14.x'
      - name: Install dependencies
        run: yarn
      - name: Install Playwright
        run: npx playwright install --with-deps
      - name: Build Storybook
        run: yarn build-storybook --quiet
      - name: Serve Storybook and run tests
        run: |
          npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
            "npx http-server storybook-static --port 6006 --silent" \
            "npx wait-on tcp:6006 && yarn test-storybook"
  # Run visual and composition tests with Chromatic
  visual-and-composition:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0 # Required to retrieve git history
      - name: Install dependencies
        run: yarn
      - name: Publish to Chromatic
        uses: chromaui/action@v1
        with:
          # Grab this from the Chromatic manage page
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
  # Run user flow tests with Cypress
  user-flow:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install dependencies
        run: yarn
      - name: Cypress run
        uses: cypress-io/github-action@v2
        with:
          start: npm start

여기서 몇 가지 주목할 것이 있습니다. 테스트 러너를 위해서, concurrently, http-server 와 wait-on 라이브러리들의 조합을 이용해 테스트를 수행할 스토리북을 빌드하고 서빙합니다.

크로마틱을 실행하려면, CHROMATIC_PROJECT_TOKEN이 필요합니다. 이는 크로마틱의 관리 페이지에서 가져올 수 있고 우리의 저장소 secrets에 넣어둬야 합니다. 반면에 GITHUB_TOKEN은 기본적으로 이용할 수 있게 되어 있습니다.

React 테스트 자동화 - React teseuteu jadonghwa
React 테스트 자동화 - React teseuteu jadonghwa

마지막으로, 새 commit을 만들고, 변경사항을 깃허브에 push하면, 작업 흐름이 실행되는 걸 볼 수 있어야 합니다!

React 테스트 자동화 - React teseuteu jadonghwa

의존성 캐시하기

각 작업은 독립적으로 실행되고, 이는 CI 서버가 의존성을 세 작업 모두에서 설치해야 한다는 뜻입니다. 때문에 테스트 실행은 느려집니다. 이를 피하기 위해서 의존성(dependencies)을 캐시해놓고, 오직 lock file이 변경되었을 때에만 yarn install을 실행하도록 할 수 있습니다. 그러면 작업 흐름이 install-cache 작업을 포함해서 수정합니다.

.github/workflows/ui-tests.yml

name: 'UI Tests'

on: push

jobs:
  # Install and cache npm dependencies
  install-cache:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Commit
        uses: actions/checkout@v2
      - name: Cache yarn dependencies and cypress
        uses: actions/cache@v2
        id: yarn-cache
        with:
          path: |
            ~/.cache/Cypress
            node_modules
          key: ${{ runner.os }}-yarn-v1-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-v1
      - name: Install dependencies if cache invalid
        if: steps.yarn-cache.outputs.cache-hit != 'true'
        run: yarn
  # Run interaction and accessibility tests
  interaction-and-accessibility:
    runs-on: ubuntu-latest
    needs: install-cache
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '14.x'
      - name: Restore yarn dependencies
        uses: actions/cache@v2
        id: yarn-cache
        with:
          path: |
            ~/.cache/Cypress
            node_modules
          key: ${{ runner.os }}-yarn-v1-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-v1
     - name: Install Playwright
       run: npx playwright install --with-deps
     - name: Build Storybook
       run: yarn build-storybook --quiet
     - name: Serve Storybook and run tests
       run: |
         npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
           "npx http-server storybook-static --port 6006 --silent" \
           "npx wait-on tcp:6006 && yarn test-storybook"
  # Run visual and composition tests with Chromatic
  visual-and-composition:
    runs-on: ubuntu-latest
    needs: install-cache
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0 # Required to retrieve git history
      - name: Restore yarn dependencies
        uses: actions/cache@v2
        id: yarn-cache
        with:
          path: |
            ~/.cache/Cypress
            node_modules
          key: ${{ runner.os }}-yarn-v1-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-v1
      - name: Publish to Chromatic
        uses: chromaui/action@v1
        with:
          # Grab this from the Chromatic manage page
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
  # Run user flow tests with Cypress
  user-flow:
    runs-on: ubuntu-latest
    needs: install-cache
    steps:
      - uses: actions/checkout@v2
      - name: Restore yarn dependencies
        uses: actions/cache@v2
        id: yarn-cache
        with:
          path: |
            ~/.cache/Cypress
            node_modules
          key: ${{ runner.os }}-yarn-v1-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-v1
      - name: Cypress run
        uses: cypress-io/github-action@v2
        with:
          start: npm start

나머지 세 작업도 install-cache작업이 끝나고 캐시된 의존성(dependencies)을 사용할 수 있을 때까지 기다리도록 약간 고쳤습니다. 이 작업 흐름을 다시 실행해보기 위해 다른 commit을 push해봅시다.

성공! 여러분은 테스팅 작업 흐름을 자동화했습니다. 이제 PR을 열면 알아서 제스트(Jest), 크로마틱 그리고 사이프레스를 병렬로 실행하고 PR 페이지에 그 결과를 보여줄 것입니다.

React 테스트 자동화 - React teseuteu jadonghwa

UI 테스팅 작업 흐름을 정복하기

테스트 작업 흐름은 스토리북을 이용해 컴포넌트를 분리시키는 것에서부터 시작합니다. 코드를 짜는 동안 검사를 실행하면서 더 빠른 피드백 고리(feedback loop)를 만들 수 있습니다. 마지막으로 여러분의 모든 테스트 스위트를 지속적 통합(Continuous Integration)을 이용해서 실행해보도록 합시다.