우아한테크코스 5기 프론트엔드

jest + react-testing-library 테스트 코드 작성 및 github actions로 자동화 하기

yoxxin 2023. 8. 27. 06:36

이 글은 아래 글에서 소개했던 직접 만든 리액트 전역상태관리 라이브러리인 @yogjin/react-global-state-hook 의 테스트 코드 작성 정리 글입니다.

 

React18의 useSyncExternalStore 훅으로 전역상태 관리하기

이 글은 useSyncExternalStore를 이미 알고 있다는 가정 하에 작성되었습니다. 처음 들어보셨다면 공식문서를 읽고 오시는 것을 추천합니다. 1. useGlobalState 를 만들었다. 공식문서에서는 useSyncExternalStor

yogjin.tistory.com

 

 

@yogjin/react-global-state

Simple global state management library for react18. Latest version: 0.0.3, last published: 13 minutes ago. Start using @yogjin/react-global-state in your project by running `npm i @yogjin/react-global-state`. There are no other projects in the npm registry

www.npmjs.com

1. 테스트 코드를 작성하면 좋은 점

테스트 코드를 추가하면

  1. 마음놓고 코드 변경을 할 수 있다. (잘못된 동작을 하면 테스트코드가 깨지므로 바로 알아챌 수 있다)
  2. github actions로 PR을 날렸을 때 테스트가 돌아가도록 자동화하여 테스트가 깨진다면 merge되지 못하도록 막을 수 있다.

이 장점들을 누리기위해 @yogjin/react-global-state-hook 에 테스트 코드를 추가하려고한다.

2. 어떤 테스트 툴을 이용해야할까?

테스트 하려는 코드는 리액트 전역상태관리 라이브러리이다.

즉 우리가 검증해야할 부분은

  1. 상태관리 로직(함수)에 문제가 없는지
  2. 리액트 환경에서 잘 돌아가는지

를 검증해야한다.

1번은 단위테스트를 해야하고 2번은 유저의 입장에서 테스트해도 괜찮을 것 같았다.

그래서 jest + react-testing-library 조합으로 테스트하기로 했다.

3. 테스트 환경설정

jest는 es6, typescript, react 코드를 이해하지 못하므로 테스트코드를 babel을 이용해서 트랜스파일링 해야한다.

package.json, babel.config.json, jest.config.json 코드와 설명을 주석으로 달아놓았다.

package.json

{
  ...
  "scripts": {
    "prepack": "yarn build",
    "build": "yarn tsc",
    "test": "yarn jest", // jest 실행 스크립트
    "test:watch": "yarn test --watch" 
  },
  "devDependencies": {
    "@babel/core": "^7.22.11", // babel 필수
    "@babel/preset-env": "^7.22.10", // 설정한 환경에 따라 필요한 plugin 제공 preset
    "@babel/preset-react": "^7.22.5", // react preset
    "@babel/preset-typescript": "^7.22.11", // typescript preset
    "@testing-library/jest-dom": "^6.1.2", // Jest에 대한 커스텀 DOM 요소 매칭 함수(toBeInTheDocument()등) 제공
    "@testing-library/react": "^14.0.0",  // render, screen 등 함수제공: react dom을 render하고, screen으로 render된 dom에 접근할 수 있음
    "@testing-library/user-event": "^14.4.3", // "@testing-library/react" 이 제공하는 fireEvent 보다 더 유저 친화적으로 동작하는 함수 제공
    "@types/jest": "^29.5.4",
    "@types/react": "^18.2.21", 
    "jest": "^29.6.4", 
    "jest-environment-jsdom": "^29.6.4", // jest가 jsdom환경에서 돌아가도록 하려면 필수로 설치
    "typescript": "^5.1.6" 
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0" // 테스트 환경에서만 쓰이므로 devDependecies로 옮겨도 무방
  }
}

babel.config.json

{
  "presets": [
    ["@babel/preset-env", { "targets": { "node": "current" } }], // jest테스트는 node환경에서 돌아가므로 
    ["@babel/preset-react", { "runtime": "automatic" }], // automatic: jsx를 transpile하는 함수들을 자동으로 import.
    "@babel/preset-typescript"
  ]
}

"@babel/preset-react" 에서 { "runtime": "automatic" } 설정을 해줘도

typescript 이용하는 테스트 코드에서는 타임검사때문에 어쩔수없이 import React from 'react' 를 해줘야한다.

(컴파일타임에는 나중에 자동으로 함수가 import 될 거라는 사실을 모르기 때문에)

jest.config.json

{
  "testEnvironment": "jsdom", // 테스트를 실행할 환경, 가상 dom 환경을 사용
  "verbose": true, // 개별 테스트 결과 출력
  "collectCoverage": true // 테스트 코드 커버리지 수집, 결과파일 생성
}

4. 작성한 테스트 코드

import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';

import { globalState, useSetGlobalState } from '../src/index';
import Component1 from './src/components/Component1';
import Component2 from './src/components/Component2';

test('non-react 코드에서 global state 저장이 잘 된다.', () => {
  const state = globalState(1);
  const setState = useSetGlobalState(state);

  setState(2);

  expect(state.getState()).toEqual(2);
});

test('컴포넌트들끼리 glabalState 공유가 잘 된다.', async () => {
  render(
    <div>
      <Component1 />
      <Component2 />
    </div>
  );

  userEvent.click(screen.getByText('Add Count'));
  userEvent.click(screen.getByText('Add Count'));

  const countsOf2 = await screen.findAllByText('2');
  expect(countsOf2).toHaveLength(2);
  const countsOf3 = await screen.findAllByText('3');
  expect(countsOf3).toHaveLength(2);
});

첫번째 test에서는 실제 set 로직이 잘 돌아가는지 테스트했다.

또한 리액트 환경이 아닐때도 잘 돌아가는것을 추가로 확인할 수 있다.

 

두번째 test는 리액트 환경에서도 잘 돌아가는지 테스트하기 위해 

내부적으로 라이브러리를 이용하는 Component1, Component2 컴포넌트를 만들어서 렌더링했다.

가상 돔에서 이렇게 돌아가고 있을 것이다

Add Count를 두번해서 생긴 2와 3이 2개씩 존재하는지 확인함으로써 

라이브러리가 잘 동작하는지 검증했다.

5. github actions를 이용해 테스트자동화

PR을 날릴 때마다 해당 PR으로 인해 변경된 라이브러리 코드가 문제없는지 확인하기 위해 테스트 자동화가 필요했다.

.github/test.yml 에 간단히 작성해보았다.

name: jest test
on: [pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Install dependencies
        run: yarn
      - name: Run tests
        run: yarn test

이제보니 라이브러리 캐싱을 통해 CI를 조금 더 빠르게 돌리려면

Install dependencies의 yarn을 yarn install --frozen-lockfile 로 바꾸면 더 좋을 것 같다

잘 동작한다

6. 마치며

단순히 리액트 전역상태관리를 하려고만 했는데

욕심이 많아져서 생각보다 더 많은 일을 했다.. ㅋㅋㅋ

npm 배포, 테스트코드와 자동화까지..

또 포스팅하진 않았지만 이왕 라이브러리 소개와 사용법을 적은 README를 추가하고 MIT LICENSE 파일도 추가해줬다.

 

npm 배포가 처음은 아니고, 우테코 레벨2 때 modal을 배포해본 경험이 있긴하다.

하지만 시간에 쫓겨 급하게 한 감이 있어서 다시한번 해보고 싶었다.

테스트코드도 동글에서는 cypress를 이용했기 때문에, jest + rtl 조합이 어떤 느낌인지 경험해보고 싶었다.

actions 자동화도 해두면 언제나 좋기 때문에 진행했다.

 

github package와 npm을 동기화해서 npm 배포도 자동화하고 싶었는데,,

며칠동안 삽질만 하고 어떻게 해야할 지 아직 잘 모르겠다😭

하나에 매몰되어있어 조금 지쳤기도하고, 다른 공부도 해보고 싶은 게 많기 때문에

나중에 또 기회가 되면 다시 도전할 생각이다.