ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ReactJS | Test 개요 및 Component Test 기초
    JavaScript/React JS 2020. 5. 18. 12:29

    TEST 테스트

    테스트는 어떤 가설을 검증해 나가는 과정이며 고품질의 소프트웨어를 개발하기 위해선 꼭 필요한 기본 중의 기본


    테스트의 종류

    피라미드 테스트에서의 테스트 단위
    • 단위(Unit): 단위 테스트는 기능의 개별적인 단위를 테스트, 리팩토링에 도움이 되며 모듈화를 증진
    • 서비스: 서비스 테스트는 기능의 집합에 집중, 범위의 규모나 테스트 수행 대상에 관한 집중도 수준이 매우 다양
    • 통합(Integration): 애플리케이션의 여러 부분을 통합해서 테스트하는 높은 수준의 테스트에 집중, 이 테스트는 인터페이스 자체를 이용해서 테스를 진행

    테스팅 트로피

    Kent C. Dodds에 의해 잘 알려진 테스팅 트로피는 프론트엔드 테스트에서 두각을 보이는 방법

    통합 테스트가 투자 대비 얻는 것이 가장 많기 때문에 가장 큰 비중을 차지해야 한다고 권함

     

    테스팅 트로피

    • End-to-End 테스트: 사용자 입장에서 전체 애플리케이션이 잘 동작하는 지 테스트하는 것
    • Integration(통합) 테스트: 실제 DB, 브라우저 없이 큰 규모의 기능이나 하나의 페이지가 잘 작동하는 지 테스트하는 것
    • Unit(단위) 테스트: 기능의 개별적인 단위나 하나의 컴포넌트를 테스트함
    • Static 테스트: 구문 오류, 나쁜 코드 스타일, 잘못된 API 사용 등을 잡아줌

    참고 사이트: [https://delivan.dev/react/modern-react-testing-part-1/]


    테스트는 왜 필요한가?

    TDD와 같이 테스트를 1급 요소로 취급하는 소프트웨어 개발 방법도 존재한다는 것은 테스트가 중요한 요소임에 틀림없다는 것의 증거가 됨

    1. 제대로 동작하는 소프트웨어를 작성하기 위함
      테스트를 수행하면서 소프트웨어에 대한 가설을 다시 되짚어 볼 수 있음
    2. 일반적으로 소프트웨어를 테스트하는 과정을 통해 더 나은 코드를 작성할 수 있음
      테스트를 진행하다 보면 코드를 더 잘 이해할 수 있게 됨
    3. 소프트웨어 개발 주기에 테스트를 통합함으로써 코드를 더 자주 릴리즈할 수 있게 됨
    4. 코드를 리팩토링하거나 다른 위치로 옮길 때 큰 도움이 됨

    TDD(Test Driven Development)

    테스트를 중요한 요소로 인식하고 개발 과정의 처음부터 끝까지 테스트를 고려하며, 테스트를 이용해 어떤 작업이 완료되었는지를 결정하는 방법

    • 실패하는 테스트(아직 구현되지 않은 가설을 검증하는 테스트)를 먼저 작성
    • 테스트를 통과하기에 충분한 수준의 코드를 먼저 작성
    • 반복되는 요소들을 리팩토링을 통해 제거
    • 다음 기능을 구현하는 과정 반복

    리액트 테스트 구성

    리액트 애플리케이션을 테스트하려면 몇 가지 라이브러리가 필요
    CRA에는 Jest 테스팅 프레임워크가 기본적으로 내장되어 있음
    Jest는 페이스북 엔지니어들이 개발한 테스팅 프레임워크


    테스트 실행기(test runner)

    • 테스트를 실행할 도구
    • 한 번에 여러 개의 테스트를 실행하거나 테스트의 성공과 실패 여부를 더 나은 방법으로 알려줌
    • Jest나 Mocha, Jasmin 등의 테스트 라이브러리들이 주로 사용됨

    테스트 더블(test doubles)

    • 인프라스트럭처 중 문제가 생길 수 있거나 예측할 수 없는 부분을 예측대로 동작하는 '가짜(fake)' 함수로 바꿔주는 도구
    • Jest의 모조 객체 사용 가능
    • Sinon 같은 다른 라이브러리도 있음

    검증 라이브러리(assertion libraries)

    • 자바스크립트만으로도 코드에 대한 검증은 가능하지만 예외적인 상황이 많기 때문에 개발자들은 코드에 대한 검증을 더 쉽게 할 수 있는 도구를 만들어냄
    • Jest 역시 내장된 검증 메서드를 제공

    환경 구성 도움 도구들(environment helpers)

    • 브라우저 환경에서 동작하는 코드에 대한 테스트를 실행하려면 조금 다른 도구들이 필요함
    • 리액트 컴포넌트에서의 환경 모방은 Enzyme이나 React-test-render 라이브러리 이용

    Enzyme

    리액트 컴포넌트를 더 쉽게 테스트할 수 있게 도와줌

    • 여러 가지 종류의 컴포넌트와 HTML 요소에 대한 질의 지원
    • 컴포넌트의 속성값을 읽거나 쓰기 지원
    • 컴포넌트 상태의 조회와 설정 등을 수행하는 견고한 API 지원

    React-test-render

    Enzyme과 유사한 기능 제공

    • 컴포넌트의 스냅샷을 생성하는 기능도 제공

    커버리지 도구(Coverage tools)

    • 커버리지 도구를 통해 코드가 얼마나 잘 테스트되고 있는지를 결정하기 위한 가이드라인을 얻을 수 있음
    • 코드 커버리지가 로직과 기본적인 분석을 대체하는 것은 아니지만,
      코드를 테스트하는 가이드가 되어줌
    • Jest의 내장 커버리지 도구는 Istanbul이라는 유명한 도구를 활용
    • CRA 프로젝트의 경우, pacakage.json의 scripts에서 test이 마지막 부분에 --coverage를 추가하면 커버리지 확인 가능

    컴포넌트 유닛 테스트

    CRA에서 Jest와 Enzyme을 사용해서 컴포넌트를 테스트하는 방법을 다룸


    리액트 컴포넌트 테스트

    리액트 프로젝트에서 컴포넌트 단위로 하나하나 테스트 로직을 작성
    컴포넌트를 테스트할 때는 주로 다음과 같은 형식을 따름

    1. 특정 props 에 따라 컴포넌트가 잘 렌더링되는지 확인
    2. 이전에 렌더링했던 결과와 지금 렌더링한 결과가 일치하는지 확인
    3. 특정 DOM 이벤트를 시뮬레이트하여, 원하는 변화가 제대로 발생하는지 확인
    4. 렌더링된 결과물을 이미지로 저장한 뒤 픽셀을 하나하나 확인해서 모두 일치하는지 확인

    ※ 4번은 주로 스토리북을 활용하며, 여기서는 다루지 않음


    실습 프로젝트 생성

    $ npx create-react-app test-tutorial
    • 프로젝트 생성후 src폴더 내부에 index.js, App.js, setupTests.js 파일을 제외한 나머지 파일은 삭제

    카운터 만들기

    src 폴더에 components 폴더 생성후 Counter.js 파일 작성

    import React, { useState } from "react";
    
    function Counter() {
      const [count, setCount] = useState(0);
    
      const onIncrease = () => {
        setCount((prevCount) => ++prevCount);
      };
    
      const onDecrease = () => {
        setCount((prevCount) => --prevCount);
      };
    
      return (
        <div>
          <h1>카운터</h1>
          <h2>{count}</h2>
          <button onClick={onIncrease}>+</button>
          <button onClick={onDecrease}>-</button>
        </div>
      );
    }
    
    export default Counter;

    App.js 수정

    import React from "react";
    import Counter from "./components/Counter";
    
    function App() {
      return (
        <div>
          <Counter />
        </div>
      );
    }
    
    export default App;
    • App.js 에서 Counter 컴포넌트 렌더링

    index.js 수정

    import React from "react";
    import ReactDOM from "react-dom";
    import App from "./App";
    
    ReactDOM.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>,
      document.getElementById("root")
    );
    • 불필요한 부분 삭제
    • yarn start 명령어로 결과 확인

    이름 등록 컴포넌트 만들기

    이름을 등록할 수 있는 컴포넌트 생성
    components 폴더에 NameForm.js 파일 작성

    import React, { useState } from "react";
    
    function NameForm({ onInsert }) {
      const [name, setName] = useState("");
    
      const onChange = (e) => {
        const { value } = e.target;
        setName((prevName) => value);
      };
    
      const onSubmit = (e) => {
        e.preventDefault();
        onInsert(name);
        setName((prevName) => "");
      };
    
      return (
        <form onSubmit={onSubmit}>
          <label htmlFor="name">이름</label>
          <input type="text" value={name} onChange={onChange} id="name" />
          <button type="submit">등록</button>
        </form>
      );
    }
    
    export default NameForm;

    이름 출력 컴포넌트 만들기

    위에서 등록한 이름을 출력하는 컴포넌트 생성
    components 폴더에 NameList.js 파일 작성

    import React from "react";
    
    function NameList({ names }) {
      return (
        <ul>
          {names.map((name, i) => (
            <li key={i}>{name}</li>
          ))}
        </ul>
      );
    }
    
    export default NameList;

    App.js 에서 렌더링하기

    import React, { useState } from "react";
    import Counter from "./components/Counter";
    import NameForm from "./components/NameForm";
    import NameList from "./components/NameList";
    
    function App() {
      const [names, setNames] = useState(["하이맨", "바이맨"]);
    
      const onInsert = (name) => {
        setNames((prevNames) => prevNames.concat(name));
      };
    
      return (
        <div>
          <Counter />
          <hr />
          <h1>이름 목록</h1>
          <NameForm onInsert={onInsert} />
          <NameList names={names} />
        </div>
      );
    }
    
    export default App;
    • yarn start 명령어로 잘 동작하는지 확인

    스냅샷 테스팅

    스냅샷 테스팅은 컴포넌트를 주어진 설정으로 렌더링하고 그 결과물을 파일로 저장한 뒤
    다음에 테스트를 진행할 때 이전의 결과물과 일치하는지 확인

    • 초기 렌더링 결과도 비교 가능
    • 클래스 컴포넌트의 경우 컴포넌트 내부 메서드 호출 후 각 상황 전후의 결과물 비교 가능

    react-test-render 설치

    $ yarn add --dev react-test-render

    카운터 테스트 코드 작성

    Counter.js를 위한 테스트 코드 작성
    components 폴더에 Counter.test.js 파일 작성

    import React from "react";
    import renderer from "react-test-render";
    import Counter from "./Counter";
    
    describe("Counter", () => {
      let component = null;
    
      it("renders correctly", () => {
        component = renderer.createRenderer(<Counter />);
      });
    
      it("matches snapshot", () => {
        const tree = component.toJSON();
        expect(tree).toMatchSnapshot();
      });
    });
    • yarn test 명령어로 결과 확인

    테스트 주요 키워드

    describe

    • 테스트의 가장 큰 단위
    • describe 내부에 또 다른 describe 선언 가능
    • 첫 번째 인수에 주로 어떤 기능을 검사하는지 문자열로 입력
      ex) Counter 컴포넌트 검사 시, 'Counter'

    it

    • 테스팅 로직의 가장 작은 단위
    • describe 안에 여러 개의 it 작성 가능
    • 첫 번째 인수에 무엇을 검사해야 하는지 문자열로 입력
      ex) 렌더링 확인 시, "renders correctly"

    expect ~ toXX


    스냅샷 생성

    src/components/__snapshots__ 경로에 Counter.test.js.snap 파일 생성

    // Jest Snapshot v1, https://goo.gl/fbAQLP
    
    exports[`Counter matches snapshot 1`] = `
    <div>
      <h1>
        카운터
      </h1>
      <h2>
        0
      </h2>
      <button
        onClick={[Function]}
      >
        +
      </button>
      <button
        onClick={[Function]}
      >
        -
      </button>
    </div>
    `;
    • Counter 컴포넌트가 렌더링된 결과물이 스냅샷으로 저장
    • Counter.js 에서 '카운터'를 '카운터!'로 수정하고 다시 테스트 해보면 스냅샷이 일치하지 않는다는 에러 발생
    • 이 때 VScode가 추천하는 스냅샷 업데이트를 진행하거나 'u'키를 눌러서 스냅샷 업데이트 가능
    • 스냅샷을 업데이트하면 에러는 더 이상 발생하지 않음

    Enzyme 사용하기

    Enzyme은 Airbnb에서 만든 컴포넌트 테스팅 도구로,
    DOM 이벤트를 시뮬레이트하거나 라이프사이클 테스트도 가능


    설치

    $ npm i enzyme enzyme-to-json enzyme-adapter-react-16

    setupTests.js 수정

    enzyme 및 어댑터 적용

    // jest-dom adds custom jest matchers for asserting on DOM nodes.
    // allows you to do things like:
    // expect(element).toHaveTextContent(/react/i)
    // learn more: https://github.com/testing-library/jest-dom
    import TestUtils from "react-dom/test-utils";
    import { configure } from "enzyme";
    import Adapter from "enzyme-adapter-react-16";
    configure({ adapter: new Adapter() });

    package.json

    enzyme-to-json 적용
    enzyme-to-json은 Enzyme에 의해 생성된 스냅샷의 가독성을 높여주는 역할을 함

    {
      // ....
      ,  
      "jest": {
        "snapshotSerializers": [
          "enzyme-to-json/serializer"
        ]
      }
    }

    DOM 시뮬레이션

    NameForm 컴포넌트가 잘 렌더링되고 form, input 태그를 갖는지 테스트
    components 폴더에 NameForm.test.js 파일 작성

    import React from "react";
    import { shallow } from "enzyme";
    import NameForm from "./NameForm";
    
    describe("NameForm", () => {
      let component = null;
    
      it("renders correctly", () => {
        component = shallow(<NameForm />);
      });
    
      it("matches snapshot", () => {
        expect(component).toMatchSnapshot();
      });
    
      describe("insert new text", () => {
        it("has a form", () => {
          expect(component.find("form").exists()).toBe(true);
        });
        it("has an input", () => {
          expect(component.find("input").exists()).toBe(true);
        });
      });
    });
    • 엔자임의 shallow 함수로 컴포넌트 렌더링 가능
    • mount 함수도 있지만 웬만한 기능은 shallow로도 테스트 가능하다고 함
    • 렌더링 후에는 find 메서드와 선택자(selector)를 이용하여 특정 DOM 선택 가능
    • 선택 방식에는 css, prop 값, 컴포넌트, 태그명 등이 있음

    이벤트 시뮬레이션

    NameForm 컴포넌트

    NameForm 컴포넌트에 이벤트를 발생시키고 해당 이벤트에 맞게 값이 변경되었는지 테스트
    NameForm.test.js 파일을 다음과 같이 수정

    import React from "react";
    import { shallow } from "enzyme";
    import NameForm from "./NameForm";
    
    describe("NameForm", () => {
      let component = null;
    
      // 테스트용 onInsert 함수, changed 값을 바꿔줌
      let changed = null;
      const onInsert = (name) => {
        changed = name;
      };
    
      it("renders correctly", () => {
        component = shallow(<NameForm onInsert={onInsert} />);
      });
    
      it("matches snapshot", () => {
        expect(component).toMatchSnapshot();
      });
    
      describe("insert new text", () => {
        it("has a form", () => {
          expect(component.find("form").exists()).toBe(true);
        });
    
        it("has an input", () => {
          expect(component.find("input").exists()).toBe(true);
        });
    
        it("simulates input change", () => {
          const mockedEvent = {
            target: {
              value: "hello",
            },
          };
    
          // 이벤트 시뮬레이트, simulate의 두 번째 매개변수로 이벤트 객체 전달
          component.find("input").simulate("change", mockedEvent);
          expect(component.find("input").prop("value")).toBe("hello");
        });
    
        it("simualtes form submit", () => {
          const mockedEvent = {
            // onSubmit에서 preventDefault를 호출하므로 가짜 함수 추가
            preventDefault: jest.fn(),
          };
          component.find("form").simulate("submit", mockedEvent);
          expect(component.find("input").prop("value")).toBe("");
          expect(changed).toBe("hello");
        });
      });
    });
    • jest.fn() 메서드를 사용하면 mock함수(가짜함수) 생성
    • 함수형 컴포넌트는 상탯값이나 인스턴스를 갖지 않기 때문에 .state()로 상탯값을 조회할 수 없으므로 그 부작용(side effect)을 검사
    • submit 이벤트가 발생하면 제어되는 컴포넌트인 input의 value가 빈문자열로 변경되기 때문에 input 값을 검사함으로써 이벤트가 잘 발생되었는지 간접적으로 확인

    Counter 컴포넌트

    Counter.test.js 파일을 다음과 같이 수정

    import React from "react";
    import { shallow } from "enzyme";
    import Counter from "./Counter";
    
    describe("Counter", () => {
      let component = null;
    
      it("renders correctly", () => {
        component = shallow(<Counter />);
      });
    
      it("matches snapshot", () => {
        expect(component).toMatchSnapshot();
      });
    
      describe("button click", () => {
        it("+ button click", () => {
          component.find("button").first().simulate("click");
          expect(component.find("h2").text()).toBe("1");
        });
    
        it("- button click", () => {
          component.find("button").last().simulate("click");
          expect(component.find("h2").text()).toBe("0");
        });
      });
    });
    • Counter 컴포넌트 역시 함수형 컴포넌트이므로 상탯값을 직접 조회하는 것이 아니라 h2 태그 내부에 텍스트로 렌더링된 값을 비교
    • button이 2개이기 때문에 first(), last() 메서드로 버튼 특정
    • 선택하려는 요소가 여러 개 중 하나이거나 보다 까다로운 조건이라면 findWhere() 메서드 사용 가능
    • useEffect 등의 훅스를 테스트하려는 경우에는 반드시 mount 함수를 사용해야 함

    참고 도서 / 사이트

    1. [ 리액트 인 액션 / 저자_ 마크 티에렌스 토마스 / 출판사_ 제이펍 ]
    2. [ 벨로퍼트님 블로그 https://velopert.com/3587 ]
    3. [ 벨로퍼트님 블로그 https://velog.io/@velopert/react-testing-with-enzyme ]
    4. [ Aashish Manandhar님 블로그 https://medium.com/@acesmndr/testing-react-functional-components-with-hooks-using-enzyme-f732124d320a ]

    고품질 소프트웨어의 기본이라고 하는 테스트의 기초에 대해 정리해보았습니다.

    튜토리얼만 체험해봤을 뿐인데도 쉽지않다고 느껴지는 과목입니다.

    하지만 테스트 코드를 작성하다보면 기존 코드의 구조에 대해서도 다시 한 번 생각해 보게 되므로

    공부에는 큰 도움이 되는 것 같습니다.

    'JavaScript > React JS' 카테고리의 다른 글

    ReactJS | Json-Server & Concurrently  (0) 2020.04.21
    ReactJS | Firebase로 React 프로젝트 DB 연동 및 배포  (0) 2020.04.10
    ReactJS | Proxy  (0) 2020.03.30
    ReactJS | Component Styling  (0) 2020.02.28
    ReactJS | Component 개요  (0) 2020.02.28

    댓글

Designed by Tistory.