자바스크립트/러닝 리엑트

리액트 테스트

막이86 2023. 11. 13. 13:05
728x90

러닝 리엑트을 요약한 내용입니다.

  • 코드의 품질을 유지하기 위한 방법중 하나는 단위 테스팅이다.
  • 단위 테스팅을 통해 애플리케이션이 의도대로 작동하는지 검증할 수 있다.

10.1 ESLint

  • 자바스크립트를 분석하는 과정을 힌팅(hinting) 또는 **린팅(Linting)**이라고 부른다.
  • JSHint와 JSLint는 원래 자바스크립트를 분석하고 형식에 대한 제안을 제공하는 도구였다.
  • ESLint는 최신 자바스크립트 문법을 지원하는 코드 린터다.
  • ESLint는 플러그인을 추가할 수 있다.
    • 기능을 확장하기 위해 플러그인을 만들어서 공유할수 있다.
    • 다른 사람이 만든 플러그인을 활용할 수 있다.
  • create-react-app을 사용하면 ESLint를 즉시 사용할 수 있다.
  • eslint-plugin-react라는 플러그인을 사용한다.
    • 자바스크립트 외에 JSX와 리액트 문법을 분석해준다.
  • eslint를 개발 의존 관계로 설치
  • npm install eslint --save-dev # 또는 yarn add eslint --dev
  • ESLint를 사용하기 전에 먼저 함께 따라야 할 몇 가지 문법 규칙을 설정해야 한다.
    • 프로젝트의 최상위 디렉터리에 설정 파일을 만들고 규칙을 지정할 수 있다.
    • 설정 파일은 JSON 이나 YAML로 작성할 수 있다.
    • YANML은 JSON과 비슷한 데이터 직렬화 형식이지만 문법이 덜 복잡하고 사람이 읽기 조금 더 쉽다.
  • ESLint 설정을 만들려면 eslint —init을 실행하고 코딩 스타일에 대한 몇가지 질문에 답변해야 한다.
  • npx eslint --init
  • npx eslint —init을 실행하면 3가지 일이 벌어진다.
    1. eslint-plugin-react가 ./node_modules 아래에 로컬 설치된다.
    2. package.json 파일에 자동으로 의존 관계가 추가된다.
    3. 프로젝트 최상위 디렉터리에 .eslintrc.json 설정 파일을 만든다.
  • npx eslint --init ? How would you like to use ESLint? … (ESLint를 어떻게 설정하겠습니까?) To check syntax only To check syntax and find problems To check syntax, find problems, and enforce code style ❯ To check syntax and find problems ? What type of modules does your project use? … (프로젝트에서 어떤 모듈을 사용합니까?) JavaScript modules (import/export) CommonJS (require/exports) None of these ❯ JavaScript modules (import/export) ? Which framework does your project use? … (프로젝트에서 어떤 프레임워크를 사용합니까? React Vue.js None of these ❯ React ? Does your project use TypeScript? › No / Yes (프로젝트에서 TypeScript를 사용합니까?) ❯ No ? Where does your code run? … (Press <space> to select, <a> to toggle all, <i> to invert selection) (코드를 어디서 실행합니까? 스페이스 바를 눌러서 선택하거나, a를 눌러서 전체를 토글하거나, i를 눌러서 선택을 반전시킬 수 있음) Browser Node ❯ Browser ? What format do you want your config file to be in? … (설정 파일을 어떤 형식으로 저장하겠습니까?) JavaScript YAML JSON ❯ JSON ? Would you like to install them now with npm? › No / Yes (npm으로 eslint를 설치하겠습니까?) ❯ Yes
  • .eslintrc.json 파일의 내용
    • extends에 eslint와 react를 초기화 해둔 것을 볼 수 있다.
    {
        "env": {
            "browser": true,
            "es2021": true
        },
        "extends": [
            "eslint:recommended",
            "plugin:react/recommended"
        ],
        "parserOptions": {
            "ecmaFeatures": {
                "jsx": true
            },
            "ecmaVersion": 13,
            "sourceType": "module"
        },
        "plugins": [
            "react"
        ],
        "rules": {
        }
    }
    
  • sample.js 파일을 만들어서 ESLint 설정과 규칙을 테스트해보자.
  • const gnar = "gnarly" const info = ({ file = __filename, dir = __dirname }) => ( <p> {dir}: {file} </p> ) switch (gnar) { default: console.log("gnarly") break }
  • ESLint를 실행해서 앞에서 정한 규칙에 따라 어떤 피드백을 바등ㄹ 수 있는지 살펴보자.
    • 프로퍼티 검증과 관련 오류가 있다
    • ESLint가 자동으로 Node.js 전역 변수를 포함시키지 않기 때문에 __filename과 __dirname에 대한 오류가 표시된다.</aside>
    • <aside> 💡 내꺼에서는 발생 안함....
    • JSX를 사용하기 위해서는 React가 범위 안에 있어야 한다는 사실을 알려준다.
    npx eslint src/sample.js
    
    2:7   error  'info' is assigned a value but never used  no-unused-vars
    'info'에 값을 지정했지만 사용하지 않음
    3:12  error  '__filename' is not defined                no-undef
    'filename'이 정의되지 않음 
    4:11  error  '__dirname' is not defined                 no-undef
    'dirname'이 정의되지 않음 
    6:5   error  'React' must be in scope when using JSX    react/react-in-jsx-scope
    JSX를 사용하려면 'React'가 영역에 들어 있어야 함
    
  • eslint . 이라는 명령은 전체 디렉터리를 검사한다.
  • .eslintignore 파일에 ESLint가 무시할 파일과 디렉터리를 적을 수 있다.
    • assets 폴더를 무시하지 않으면 ESLint가 클라이언트의 bundle.js 파일을 분석해서 엄청나게 많은 지적사항을 볼수 있을 것이다.
    dist/assets/
    sample.js
    

10.1.1 ESLint 플러그인

  • ESLint에 추가해서 코드 작성시 도움을 받을 수 있는 프러그인이 많이 있다.
  • 리액트 프로젝트의 경우 eslint-plugin-react-hooks를 꼭 설치해야 한다.
    • 훅스와 관련된 규칙을 추가해준다.
    • 리액트 팀이 훅스 사용과 관련한 버그를 수정할 떄 도움이 될수 있도록 배포했다.
    npm install eslint-plugin-react-hooks --save-dev
    
    # 또는
    yarn add eslint-plugin-react-hooks --dev
    
  • .eslintrc.json을 열어서 다음을 추가하자.
    • use라는 단어로 시작하는 함수가 훅스 관련 규칙을 만족하는지 검사한다.
    {
        "env": {
            "browser": true,
            "es2021": true
        },
        "extends": [
            "eslint:recommended",
            "plugin:react/recommended"
        ],
        "parserOptions": {
            "ecmaFeatures": {
                "jsx": true
            },
            "ecmaVersion": 13,
            "sourceType": "module"
        },
        "plugins": [
            "react",
            **"react-hooks"**
        ],
        "rules": {
            **"react-hooks/rules-of-hooks": "error",
            "react-hooks/exhaustive-deps": "warn"**
        }
    }
    
  • 추가한 플러그인이 잘 동작하는지 테스트하는 코드
  • function gnar() { const [nickname, setNickname] = useState( "dude" ) return <h1>gnarly</h1> }
  • 오류 확인
    • 컴포넌트나 훅이 아닌 함수 내부에서 useState를 사용하려고 시도한다는 오류 메시지
    4:37  error  React Hook "useState" is called in function "gnar" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter  react-hooks/rules-of-hooks
    
  • npx eslint src/sample.js 3:10 error 'gnar' is defined but never used no-unused-vars 4:12 error 'nickname' is assigned a value but never used no-unused-vars 4:22 error 'setNickname' is assigned a value but never used no-unused-vars 4:37 error React Hook "useState" is called in function "gnar" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter react-hooks/rules-of-hooks
  • 유용한 ESLint 플러그인으로는 eslint-plugin-jsx-a11y가 있다
    • a11y는 숫자를 사용한 줄임말으로 a와 y 사이에 11글자가 들어 있는 표현이다
    • accessibility(접근성)의 줄임말
    • 웹사이트, 도구, 기술 등이 얼마나 쉽게 장애인이 사용할 수 있냐를 뜻한다.
    • 설치를 위해서는 npm이나 yarn을 사용한다.
    • npm install eslint-plugin-jsx-a11y # 또는 yarn add eslint-plugin-jsx-a11y
    • 설정 파일 .eslintrc.json을 추가하자
    • { "env": { "browser": true, "es2021": true }, "extends": [ "eslint:recommended", "plugin:react/recommended", **"plugin:jsx-a11y/recommended"** ], "parserOptions": { "ecmaFeatures": { "jsx": true }, "ecmaVersion": 13, "sourceType": "module" }, "plugins": [ "react", "react-hooks", **"jsx-a11y"** ], "rules": { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" } }
  • a11y 플러그인을 테스트 해보자
    • jsx/a11y 플러그인이 알려주는 오류를 볼 수 있다.
    • npx eslint src/sample.js 1:10 error 'Image' is defined but never used no-unused-vars 2:12 error img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text 2:12 error 'React' must be in scope when using JSX
    • image 태그가 린트 검사를 통과하려면 alt 프로퍼티가 있거나, 이미지가 없으도 내용을 이애하는 데 문제가 없으면 alt 프로퍼티의 내용이 빈 문자열이여야만 한다.
    • 2:12 error img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text
  • function Image() { return <img src="/img.png" /> }

10.2 프리티어

  • 프리티어(Prettier)는 다양한 프로젝트에서 사용할 수 있는 옵션 선택이 가능한 코드 형식화기 이다.
  • 프리티어가 ESLint와 함께 동작하게 하려면 프로젝트 설정을 좀 더 다듬어야 한다.
  • 시작하려면 프리티어를 전역으로 설치해야 한다.
  • sudo npm install -g prettier

10.2.1 프로젝트에서 프리디어 설정하기

  • 프리티어 설정 파일을 프로젝트에 추가하려면 .prettierrc 파일을 만들면 된다.
  • { "semi": true, "trailingComma": "none", "singleQuote": false, "printWidth": 80 }
  • sample.js 파일에 형식화가 필요한 코드를 추가하자.
  • console.log("Prettier Test")
  • 프리티어 검사 진행
    • Code style issues found in the above file(s). Forgot to run Prettier? 메시지를 표시한다.
    • CLI로부터 프리티어가 코드를 형식화하도록 하려면 write 플래그를 전달한다.
    • prettier --write src/sample.js
    • 파일의 내용이 바뀌었음을 확인 하자
    • console.log("Prettier Test");
  • prettier --check src/sample.js Checking formatting... [warn] src/sample.js [warn] Code style issues found in the above file(s). Forgot to run Prettier?

10.2.2 VSCode에서 프리티어 사용하기

  • VSCode를 사용중이라면 프리티어를 편집기 안에 설정하기를 권장한다.
  • 빠르게 설정을 할수 있으며, 코드를 작성하면서 많은 시간을 절약할 수 있다.
  • 프리티어 VSCode 확장 플러그인을 설치한다.
  • 설정에 접근하려면 Code → Preference → Settings → Extensions → Prettier
    • 오른쪽 상단에 작은 종이 모양 아이콘을 클릭하면 JSON으로 설정할 수 있다.
    • { "editor.formatOnSave": true }
    • 파일을 저장할 때마다 프리디어가 .prettierrc 디폴트 값에 따라 파일을 형식화해주낟.
    • 프로젝트 안에 .prettierrc 파일이 없더라도 기본 값으로 설정할 수 있다.

10.3 리액트 애플리케이션을 위한 타입 검사

  • 큰 애플리케이션을 다루는 경우 어떤 유형의 버그를 정확히 잡아내기 위해 타입 검사를 포함시키고 싶을 수도 있다.
  • 리액트 앱에서 타입 검사를 수행하는 데는 prop-types 라이브러리, 플로우, 타입스크립트 라는 3가지 방법이 있다.

10.3.1 PropTypes

  • PropTypes는 코어 리액트 라이브러리의 일부분이였으며, 리액트 앱의 타입을 검사할 때 권장되는 방법이 었다.
  • 요즘은 플로우나 타입스크립트 같은 다른 해법이 대두되면서 리액트 번들 크기를 줄이기 위해서 PropTypes 기능이 별도 라이브러리로 분리됐다.
  • 하지만 PropTypes도 널리 쓰이고 있다.
  • npm install prop-types --save-dev
  • 테스트 코드
    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import App from './App';
    import reportWebVitals from './reportWebVitals';
    
    ReactDOM.render(
      
        
      ,
      document.getElementById('root')
    );
    
    // If you want to start measuring performance in your app, pass a function
    // to log results (for example: reportWebVitals(console.log))
    // or send to an analytics endpoint. Learn more: <https://bit.ly/CRA-vitals>
    reportWebVitals();
    
  • import './App.css'; import PropTypes from "prop-types" function App({ name }) { return ( <div> <h1>{ name }</h1> </div> ); } App.propTypes = { name: PropTypes.string } export default App;
  • 여러 값을 넘길 수 있다.
    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import App from './App';
    import reportWebVitals from './reportWebVitals';
    
    ReactDOM.render(
      
        
      ,
      document.getElementById('root')
    );
    
    // If you want to start measuring performance in your app, pass a function
    // to log results (for example: reportWebVitals(console.log))
    // or send to an analytics endpoint. Learn more: <https://bit.ly/CRA-vitals>
    reportWebVitals();
    
  • import './App.css'; import PropTypes from "prop-types" function App({ name, using }) { return ( <div> <h1>{ name }</h1> <p> {using ? "used here" : "not used here"} </p> </div> ); } App.propTypes = { name: PropTypes.string, using: PropTypes.bool } export default App;
  • 사용할 수 있는 타입들
    • PropTypes.array
    • PropTypes.object
    • PropTypes.bool
    • PropTypes.func
    • PropTypes.number
    • PropTypes.string
    • PropTypes.symbol
  • 값이 들어 있는지 확인하고 싶다면 isRequired를 붙이면 된다.
    ReactDOM.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>,
      document.getElementById('root')
    );
    
    • 콘솔에 나오는 오류
    index.js:1 Warning: Failed prop type: The prop `name` is marked as required in `App`, but its value is `undefined`.
    
  • App.propTypes = { name: PropTypes.string.isRequired, using: PropTypes.bool }
  • 값만 제공되면 제공된 값의 타입이 무엇이든 상관 없는 경우도 있다.
    • bool, string, number 등 다양한 타입이 들어갈 수 있다.
    • undefined가 아니면 된다.
    App.propTypes = {
      name: PropTypes.any.isRequired,
    }
    
  • 특정한 값이면 되야하는 경우
  • import './App.css'; import PropTypes from "prop-types" function App({ status }) { return ( <div> <h1> We`re {status === "Open" ? "Open!" : "Closed!"} </h1> </div> ); } App.propTypes = { status: PropTypes.oneOf(["Open", "Closed"]) } export default App;

<aside> 💡 콘솔에서만 오류가 발생하는 것 같음...

</aside>

10.3.2 플로우

  • 플로우는 페이스북 오픈 소스에 의해 유지되는 타입검사 라이브러리
  • 정적 타입 애너테이션을 사용해 오류를 검사하는 도구
  • 프로젝트 생성
  • npx create-react-app in-th-flow
  • 플로우를 프로젝트에 추가
  • npm install --save flow-bin
  • package.json의 scripts 키에 프로퍼티를 추가 하자
  • { "name": "in-th-flow", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", "react": "^17.0.2", "react-dom": "^17.0.2", "react-scripts": "4.0.3", "web-vitals": "^1.1.2" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", **"flow": "flow"** }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
  • 이후 .flowconfig 파일을 작성해야한다. 다음 명령어를 통해 만들어보자<aside> 🔥 여기부터 따라해도 안됨
  • </aside>
  • npm run flow init
  • 플로우에서 가장 멋진 기능은 플로우를 점진적으로 적용할 수 있다는 점이다.
  • 전체 프로젝트에서 타입 검사를 한꺼번에 적용하는 일은 너무 큰 일이다. 하지만 플로우를 사용하면 꼭 프로젝트 전체에 타입 검사를 적용할 필요가 없다.
  • 타입 검사를 하고 싶은 파일 맨 위에 //@flow라는 줄을 추가하면 된다.
  • VSCode extention for Flow라는 확장 프로그램을 설치해서 코드 완성과 파라미터 힌터를 제공하게 할 수도 있다.
  • index.js 파일을 수정 해보자
  • //@flow import React from 'react'; import ReactDOM from 'react-dom'; function App(props) { return ( <div> <h1>{props.item}</h1> </div> ) } ReactDOM.render( <App item="jacket" />, document.getElementById('root') );
  • 프로퍼티의 타입을 정의
  • //@flow import React from 'react'; import ReactDOM from 'react-dom'; type Props = { item: string } function App(props: Props) { return ( <div> <h1>{props.item}</h1> </div> ) } ReactDOM.render( <App item="jacket" />, document.getElementById('root') );

???? 여기부터는 실행이 안되니...

10.3.3 타입스크립트

  • 타입스크립트는 리액트 애플리케이션을 타입체킹하고 싶을 때 쓸수 있는 또 다른 유명한 도구이다.
  • 타입스크립트는 자바스크립트의 하위집합이면 오픈 소스 언어이다.
  • 마이크로소프트가 타입스크립트를 만들었다.
  • 타입스크립트를 만든 목적은 큰 프로젝트에서 개발자가 좀 더 빠르게 버그를 찾고 더 빠르게 개발 이터레이션이 가능하게 돕는 것이다.
  • create-react-app은 타입스크립트 템플릿을 제공한다.
  • 프로젝트 생성
  • npx create-react-app my-type --template typescript
  • 프로젝트 구성 요소 확인
    • src 디렉터리 안에 있는 파이들의 확장자가 .ts나 .tsx라는 점에 유의하자.
    • .tsconfig.json 파일을 볼 수 있다.
      • 타입 스크립트 관련 설정이 담겨있다.
    • package.json 파일을 보면 타입스크립트 라이브러와 Jest, 리액트, 리액트DOM 등을 위한 정의와 타입스크립트와 관련있는 새로운 의존 관계가 추가 됬음을 알 수 있다.
      • @types/ 로시작하는 의존 관계 라이브러리의 타입 정의를 뜻한다.
  • 이전 코드를 넣어 보자
    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import App from './App';
    import reportWebVitals from './reportWebVitals';
    
    ReactDOM.render(
      <React.StrictMode>
        <App item="jacket"/>
      </React.StrictMode>,
      document.getElementById('root')
    );
    
    reportWebVitals();
    
    • 아래와 같은 오류를 볼 수 있다.
    Parameter 'props' implicitly has an 'any' type.  TS7006
    
  • import './App.css'; function App(props) { return ( <div> <h1>{props.item}</h1> </div> ); } export default App;
  • App 컴포넌트에 타입 규칙을 추가
  • import './App.css'; function App(props: AppProps) { return ( <div> <h1>{props.item}</h1> </div> ); } type AppProps = { item: string } export default App;
  • 필요하다면 props를 구조 분해할 수도 있다.
  • import './App.css'; function App({ item }: AppProps) { return ( <div> <h1>{item}</h1> </div> ); } type AppProps = { item: string } export default App;
  • 타입의 규칙을 깰 수 있다.
    • 아래와 같은 오류 발생
    /Users/chihwan/Documents/workspaces/react/testing/my-type/src/index.tsx
    TypeScript error in /Users/chihwan/Documents/workspaces/react/testing/my-type/src/index.tsx(9,10):
    Type 'number' is not assignable to type 'string'.  TS2322
    
    • 오류가 발생하면 어디서 문제가 발생했는지 정확히 알 수 있다.
      • 디버깅시 유용하다.
  • import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( <React.StrictMode> <App item={1}/> </React.StrictMode>, document.getElementById('root') ); reportWebVitals();
  • 타입스크립트를 사용하면 프로퍼티 검증 외에도 여러 가지로 편리하다.
  • 타입스크립트의 타입추론을 사용하면 훅 값에 대한 타입검사를 쉽게 할 수 있다.
  • fabricColor의 상태 값이 처음에 purple이라고 가정하자.
    • fabricColor의 타입이 정의 되지 않았다 하지만 타입스크립트는 최초 상태의 타입과 일치해야 한다고 추론한다.
    import { useState } from 'react'
    
    function App({ item }: AppProps) {
      const [fabricColor, setFabricColor] = useState("purple")
      return (
        <div>
          <h1>{fabricColor}</h1>
          <button onClick={() => setFabricColor("blue")}>
            Make the Jacket Blue
          </button>
        </div>
      );
    }
    
    type AppProps = {
      item: string
    }
    
    export default App;
    
    • 문자가 아닌 숫자를 입력하면 오류가 발생한다.
      /Users/chihwan/Documents/workspaces/react/testing/my-type/src/App.tsx
      TypeScript error in /Users/chihwan/Documents/workspaces/react/testing/my-type/src/App.tsx(8,45):
      Argument of type 'number' is not assignable to parameter of type 'SetStateAction<string>'.  TS2345
      
           6 |     <div>
           7 |       <h1>{fabricColor}</h1>
        >  8 |       <button onClick={() => setFabricColor(3)}>
             |                                             ^
           9 |         Make the Jacket Blue
          10 |       </button>
          11 |     </div>
      
    • import { useState } from 'react' function App({ item }: AppProps) { const [fabricColor, setFabricColor] = useState("purple") return ( <div> <h1>{fabricColor}</h1> <button onClick={() => setFabricColor(3)}> Make the Jacket Blue </button> </div> ); } type AppProps = { item: string } export default App;
  • 타입스크립트를 사용하면 이런 값에 대한 노력을 기울이지 않고도 타입으 ㄹ검사할 수 있다.
  • 더 자세한 타입을 지정할 수도 있다.

10.4 테스트 주도 개발

  • 테스트 주도 개발(TDD)는 기술이 아니다.
  • 모든 개발 과정을 테스트 중심으로 진행해 나가는 습관이라 할 수 있다.

TDD 연습 방법

  1. 테스트를 먼저 작성한다.
  2. 테스트를 실행하고 실패한다.(빨간색)
  3. 테스트를 통과하기 위해 필요한 최소한의 코드를 작성한다.(녹색)
  4. 코드와 테스트를 함께 리팩터링한다.(황금색)
  • TDD는 훅을 테스트할 떄 유용하다.
  • 훅을 테스트할 때 유용 하다.
    • 훅을 직접 작성하기 전에 미리 훅이 어떻게 동작해야 하는지에 대해 생각하는 편이 더 쉽다
  • TDD를 연습해서 습관으로 삼으면 UI와 관계없이 애플리케이션이나 어떤 기능에 대한 전체 데이터 구조를 구축하고 검증할 수 있다.

10.4.1 TDD와 학습

  • TDD를 처음 접하면 테스트를 작성하는 일이 상당히 어려운 일임을 알 수 있을 것이다.
  • TDD의 요령을 파악하기 전에는 테스트를 작성하기 전에 코드를 작성해도 괜찮다.
  • 작은 분량을 대상으로 개발을 진행하고, 그 코드에 대해 테스트를 몇가지 작성하는 방식을 반복하자.
  • 언어에 대한 감을 잡고 테스트에 대해 감을 잡으면 테스트를 먼저 쓰는 게 훨씬 쉬워질 것이다.

10.5 제스트 사용하기

  • TDD 프레임워크는 아무것이나 사용해도 되지만 공식 리액트 문서에는 제스트를 권장한다.
  • 제스트는 JSDOM을 통해 DOM에 접근할 수 있는 자바스크립트 테스트 러너이다.
  • 리액트가 렌더링한 내용이 맞는지 검사하려면 DOM에 접근할 수 있는게 중요하다.

10.5.1 create-react-app과 테스트

  • create-react-app으로 초기화된 프로젝트에는 기본적으로 jest 패키지가 설치된다.
  • npx create-react-app testing
  • src 폴더에 functions.js와 functions.test.js 라는 2가지 파일을 만들자
    • functions.test.js
      • 첫번째 인자는 테스트의 이름이다.
      • 두번째 인자는 테스트 대상 함수이며 세번째 인자(없어도 됨)는 타임아웃을 지정한다.
        • 디폴트 타임아웃은 5초이다.
    test("Multiplies by two", () => {
        expect();
    });
    
    • functions.js
    export default function tiemsTwo() {
        
    }
    
  • timesTwo를 테스트하는 코드
    • functions.test.js
      • 함수를 테스트할 때는 .toBe 매처를 사용한다.
    import { tiemsTwo } from "./functions"
    
    test("Multiplies by two", () => {
        expect(tiemsTwo(4)).toBe(8);
    });
    
    npm test
    
    # or
    npm run test
    
    • 오류 발생
      • 테스트 실패에 대한 자세한 내용을 표시해준다.
    FAIL  src/functions.test.js
      ✕ Multiplies by two (3 ms)
    
      ● Multiplies by two
    
        expect(received).toBe(expected) // Object.is equality
    
        Expected: 8
        Received: undefined
    
          2 |
          3 | test("Multiplies by two", () => {
        > 4 |     expect(tiemsTwo(4)).toBe(8);
            |                         ^
          5 | });
          6 |
          7 |
    
          at Object.<anonymous> (src/functions.test.js:4:25)
    
    Test Suites: 1 failed, 1 total
    Tests:       1 failed, 1 total
    Snapshots:   0 total
    Time:        2.794 s
    Ran all test suites related to changed files.
    
    Watch Usage: Press w to show more.
    
  • 테스트를 통과하기
    • 테스트 통과
    PASS  src/functions.test.js
      ✓ Multiplies by two (1 ms)
    
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        2.233 s
    Ran all test suites related to changed files.
    
    Watch Usage: Press w to show more.
    
  • export default function tiemsTwo(a) { return a * 2; }
  • .toBe 매처는 주어진 한 가지 값과 결과가 일치하는지 테스트 해준다.
  • 객체나 배열을 테스트하고 싶다면 .toEqual을 사용할 수 있다.
  • 배열을 테스트하는 코드
    • 라스베가스의 가이 피에리 레스토랑의 메뉴가 있다. 고객이 원하는 음식을 먹고 값을 지불하게 하려면 주문한 메뉴에 해당하는 객체의 목록을 만드는 일이 중요하다.
    • 테스트를 먼저 채워 넣자
    import { tiemsTwo, order } from "./functions"
    
    test("Multiplies by two", () => {
        expect(tiemsTwo(4)).toBe(8);
    });
    
    const menuItems = [
        {
            id: "1",
            name: "Tatted Up Turkey Burger",
            price: 19.5
        },
        {
            id: "2",
            name: "Lobster Lollipops",
            price: 16.5
        },
        {
            id: "3",
            name: "Motley Que Pulled Pork Sandwich",
            price: 21.5
        },
        {
            id: "4",
            name: "Trash Can Nachos",
            price: 19.5
        }
    ];
    
    test("Build an order object", () => {
        const result = {
            orderItems: menuItems,
            total: 77
        }
        expect(order(menuItems)).toEqual(result)
    })
    
    • 함수를 채워 넣자
    export function tiemsTwo(a) {
        return a * 2;
    }
    
    export function order(items) {
        const total = items.reduce((price, item) => price + item.price, 0)
        return {
            orderItems: items,
            total
        }
    }
    
  • describe로 테스트들을 감싸면 테스트 러너가 테스트를 묶은 블록을 만들어준다.
    • 테스트를 많이 작성할수록 describe 블록으로 테스트를 묶는 것이 더 유용한 개선이 될 것이다.</aside>
    • <aside> ❓ 개별 테스트가 더 좋은것 아닌가? 테스트 단위를 적개 하기 위해서 다른 의견 : 테스트를 그룹핑 할수 있다. - 초기화 관련(beforEach 등) 작업을 할수 있기 때문에 의미 있음
    export function tiemsTwo(a) {
        return a * 2;
    }
    
    export function sum(a, b) {
        return a + b;
    }
    
    export function subtract(a, b) {
        return a / b;
    }
    
    import { tiemsTwo, sum, subtract } from "./functions"
    
    describe("Math functions", () => {
        test("Multiplies by two", () => {
            expect(tiemsTwo(4)).toBe(8);
        })
        test("Adds two numbers", () => {
            expect(sum(4, 2)).toBe(6);
        })
        test("Subtracts two numbers", () => {
            expect(subtract(4, 2)).toBe(2);
        })
    })
    
    • 테스트 결과
    Math functions
        ✓ Multiplies by two (1 ms)
        ✓ Adds two numbers
        ✓ Subtracts two numbers
    

테스트를 먼저 작성하고, 테스트를 통과하기 위한 코드를 작성, 테스트에 통과하면 코드를 살펴보면서 성능향상이나 코드 가독성을 개선하기 위한 리팩터링을 한다.

10.6 리액트 컴포넌트 테스트하기

  • 리액트 컴포넌트를 테스트 할 수 있는 방법을 알아보자.
    • 리액트 컴포넌트는 DOM을 만들고 갱신할 때 리액트가 따라야 하는 명령을 제공한다.
    • 렌더링하고 결과로 만들어지는 DOM을 검사하면 컴포넌트를 테스트 할 수 있다.
  • 테스트를 브라우저에서 실행하지 않는다.
    • 터미널에서 Node.js를 사용해 실행한다.
    • Node.js에서는 표준적으로 DOM을 제공하는 브라우저와 달리 DOM API를 제공하지 않는다.
    • 제스트에서는 jsdom이라는 npm 패키지가 함께 들어 있다.
  • Star.js에 있는 Star 컴포넌트를 다시 살펴보자.
    • index.js
    import React from 'react';
    import ReactDOM from 'react-dom';
    import Star from './Star';
    
    ReactDOM.render(
      <Star />,
      document.getElementById('root')
    );
    
  • import { FaStar } from 'react-icons/fa' export default function Start({ selected = false }) { return ( <FaStar color={selected ? "red" : "grey"} id="star" /> ) }
  • Star.test.js 파일에서 Star를 테스트 해보자
    • div를 생성하고 Star 컴포넌트를 렌더링
    • 생성된 div 안에 svg 엘리먼트를 선택하면 결과가 참으로 나오게 된다.
    import React from "react";
    import ReactDOM from "react-dom";
    import Star from "./Star";
    
    test("renders a star", () => {
        const div = document.createElement("div");
        ReactDOM.render(<Star />, div)
        expect(div.querySelector("svg")).toBeTruthy()
    })
    
    • 테스트가 실패하는 코드
    test("renders a star", () => {
        const div = document.createElement("div");
        ReactDOM.render(<Star />, div)
        expect(div.querySelector("notrealthing")).toBeTruthy()
    })
    
  • create-react-app 프로젝트 생성시 @testing-library에 있는 몇가지 패키지가 설치 된다.
  • { "name": "testing", "version": "0.1.0", "private": true, "dependencies": { **"@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3",** "react": "^17.0.2", "react-dom": "^17.0.2", "react-icons": "^4.3.1", "react-scripts": "4.0.3", "web-vitals": "^1.1.2" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
  • 리액트 테스팅 라이브러리는 켄트 C, 도즈가 좋은 테스트 실무를 권장하고 리액트 생태계의 일부분으로 테스트 도구를 제공하기 위해 시작한 프로젝트이다.
  • 테스팅 라이브러리는 Vue, Svelte, Reason, Angular 등 여러 테스트 패키지들을 모아둔 프로젝트다
  • 테스팅 라이브러리를 추가하여 더 좋게 만들자
    • toBeTruthy 대신 toHaveAttribute를 사용해보자.
    import React from "react";
    import ReactDOM from "react-dom";
    import Star from "./Star";
    
    import { toHaveAttribute } from "@testing-library/jest-dom"
    
    expect.extend({ toHaveAttribute })
    
    test("renders a star", () => {
        const div = document.createElement("div");
        ReactDOM.render(<Star />, div)
        expect(
            div.querySelector("svg")
        ).toHaveAttribute("id", "hotdog")
    })
    
    <aside> ❓ 에러가 나는데.... 라이브러리 및 import를 변경해보자
    • 커스텀 매처를 둘 이상 사용하려면 아래와 같이 하면 된다.
    import {
    	toHaveAttribute,
    	toHaveClass
    } from "@testing-library/jest-dom"
    
    expect.extend({ toHaveAttribute, toHaveClass })
    expect(you).toHaveAttribute("evenALittle")
    
    • 커스텀 매처를 전체 추가하려면 extend-expect 라이브러리를 추가하자
    import React from "react";
    import ReactDOM from "react-dom";
    import Star from "./Star";
    import "@testing-library/jest-dom/extend-expect"
    
    test("renders a star", () => {
        const div = document.createElement("div");
        ReactDOM.render(<Star />, div)
        expect(
            div.querySelector("svg")
        ).toHaveAttribute("id", "hotdog")
    })
    
    <aside> ❓ 이것도 에러...
    • create-react-app을 사용하면 테스트 파일안에서 extend-expect를 임포트할 필요 없다
      • setuTest.js에 import '@testing-library/jest-dom/extend-expect'; 추가 되어 있음
      <aside> ❓ 내쪽에서는 import '@testing-library/jest-dom'; 이것만 있는데...????
    • </aside>
  • </aside>
  • </aside>

10.6.1 쿼리

  • 쿼리 테스트를 위해 Star 컴포넌트에 제목을 포함시키자
  • import { FaStar } from 'react-icons/fa' export default function Start({ selected = false }) { return ( <> <h1>Great Star</h1> <FaStar color={selected ? "red" : "grey"} id="star" /> </> ) }
  • h1안에 제대로 텍스트가 들어갔는지 테스트 해보자
    import React from "react";
    import Star from "./Star";
    
    import { render } from '@testing-library/react'
    
    test("renders an h1", () => {
        const { getByText } = render(<Star />)
        const h1 = getByText(/Great Star/);
        expect(h1).toHaveAttribute("Great Star")
    })
    
    <aside> ❓ 이것도 에러...
  • </aside>
  • 리액트 테스팅 라이브러이에 있는 render라는 함수를 쓰면 테스트에 도움이 된다.

10.6.2 이벤트 테스트

  • Checkbox 컴포넌트의 이벤트를 테스트 해보자
  • import { useReducer } from "react"; export function Checkbox() { const [checked, setChecked] = useReducer( checked => !checked, false ) return ( <> <label> {checked ? "checked" : "not checked"} <input type="checkbox" value={checked} onChange={setChecked} /> </label> </> ) }
  • 테스트를 통해 체크박스를 클릭하고 checked의 값이 디폴트 값인 true에서 false로 바뀌는지 살펴보는 것이다.
    • getByLabelText를 사용해서 checkbox 엘리먼트를 찾아 내자
    • fireEvent를 사용해 checkbox를 클릭하고 checked 프로퍼티가 true로 변했는지 확인
    • 이벤트를 다시 발생시키고 false로 변경이 되었는지 확인
    import React from "react"
    import { render, fireEvent } from "@testing-library/react"
    import { Checkbox } from "./Checkbox"
    
    test("Selecting the ceckbox should change the value of checked to true", () => {
        const { getByLabelText } = render(<Checkbox />)
        const checkbox = getByLabelText(/not checked/)
        fireEvent.click(checkbox)
        expect(checkbox.checked).toEqual(true)
        fireEvent.click(checkbox)
        expect(checkbox.checked).toEqual(false)
    })
    
  • 레이블이 있을 경우 쉽게 엘리먼트를 찾고 테스트를 할수 있었다.
    • DOM 엘리먼트를 쉽게 찾을수 없을 경우를 위해 DOM 엘리먼트를 쉽게 찾을 수 있도록 애트리뷰트를 하나 추가할 수 있다.
    import { useReducer } from "react";
    
    export function Checkbox() {
        const [checked, setChecked] = useReducer(
            checked => !checked,
            false
        )
    
        return (
            <input
                type="checkbox"
                value={checked}
                onChange={setChecked}
                data-testid="checkbox"
            />
        )
    }
    
  • getByTestId라는 함수를 써서 엘리먼트를 쿼리 할 수 있다.
  • import React from "react" import { render, fireEvent } from "@testing-library/react" import { Checkbox } from "./Checkbox" test("Selecting the ceckbox should change the value of checked to true", () => { const { getByTestId } = render(<Checkbox />) const checkbox = getByTestId("checkbox") fireEvent.click(checkbox) expect(checkbox.checked).toEqual(true) fireEvent.click(checkbox) expect(checkbox.checked).toEqual(false) })
  • DOM 엘리먼트에 접근하기 위한 방법이 마땅치 않은 경우에 아주 유용하다.

10.6.3 코드 커버리지 사용하기

  • 코드 커버리지는 테스트가 소스 코드 중 몇 줄을 실제로 테스트하는지 보고하는 과정을 말한다.
  • 코드 커버리지는 충분히 테스트를 작성했는지 보여주는 지표가 될 수 있다.
  • 제스트에는 이스탄불이라는 자바스크립트 도구가 들어 있다.
  • 이스탄불은 테스트를 분석해서 문장, 분기, 함수, 줄을 테스트 했는지 표시해준다.
  • 제스트를 실행하면서 코드 커버리지를 검사하려면 coverage 플래그를 추가 해야한다.
    --------------|---------|----------|---------|---------|-------------------
    File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    --------------|---------|----------|---------|---------|-------------------
    All files     |   81.81 |        0 |    87.5 |      80 |                   
     Checkbox.js  |     100 |      100 |     100 |     100 |                   
     Star.js      |       0 |        0 |       0 |       0 | 4                 
     functions.js |     100 |      100 |     100 |     100 |                   
     index.js     |       0 |      100 |     100 |       0 | 5                 
    --------------|---------|----------|---------|---------|-------------------
    
  • jest --coverage # 또는 npm test -- --coverage
  • 제스트는 브라우저에서 볼 수 있는 테스트 보고서도 만들어 준다.
    • 프로젝트 Root에 coverage/lcov-report/index.html 파일이 생성됨
  • 코드 커버리지는 100%달성하는 경우는 일반적이지 않다.
  • 85%를 목표로 하는 편이 좋다
  • 코드의 품질을 유지하기 위한 방법중 하나는 단위 테스팅이다.
  • 단위 테스팅을 통해 애플리케이션이 의도대로 작동하는지 검증할 수 있다.
728x90

'자바스크립트 > 러닝 리엑트' 카테고리의 다른 글

리액트와 서버  (1) 2023.11.13
리액트 라우터  (0) 2023.11.13
Suspense  (0) 2023.11.13
데이터 포함시키기  (0) 2023.11.13
훅스 컴포넌트 개선하기  (1) 2023.11.13