본문 바로가기

React/실험실

[React] Test Coverage

들어가며

프론트엔드 코드를 많이 작성하지만 내가 작성한 코드를 직접 테스트하는 경우는 있지만

아직까지 Jest 등을 활용해서 테스트를 해본적은 없다. 

 

그래서 이번에 테스트를 어떤 방법으로 해야할지 공부하다가 좋은 글을 발견해서 

나만의 글로 정리해보려고 한다. 

 

테스트의 종류 

소프트웨어 개발에서 이야기하는 테스트는 일반적으로 unit test, integration test, e2e test이다. 

 

unit test

unit test는 말그대로 유닛 테스트, 단위 테스트라고 이야기하는데, 

함수, 클래스 단위의 모듈을 테스트하고 프론트엔드에서 컴포넌트에 해당하는 파일의 테스트를 말한다.

 

특정 컴포넌트를 렌더링해서 깨지지 않는지 확인하는 것도 유닛 테스트가 될 수 있다. 

 

integration test

integration test, 통합 테스트는 여러 모듈의 연결이 있는 코드를 테스트한다. 

프론트엔드에서는 컨테이너에 해당하는 파일의 테스트를 말한다. 

 

페이지 자체를 테스트하는 경우에는 여기에 해당한다. 

 

e2e test

e2e test, end to end test는  모듈과 코드 입상에서의 품질을 보증하던 위 2가지 방식과는 다르게

코드를 통해 사용자 입장에서 테스트를 하는 방법을 말한다. 

 

Cypress, Selenium 같은 툴을 사용해서 실제 사용자처럼 개발 환경으로 구성된 사이트에 들어가

테스트를 하는 코드를 작성하면 e2e 라고 한다. 

 

unit test와 integration test는 코드 레벨에서 테스트를 진행하기 때문에 같은 라이브러리를 쓰고 

같은 프로젝트에서 작업하기 때문에 실제 테스트 코드를 작성할 때 영역이 겹치기도 한다. 

 

하지만 e2e test는 사용자 입장에서 실제 렌더링을 해서 테스트하기 때문에 테스트 시간이 길고

별도의 프로젝트 파일에서 테스트하는 코드를 작성하기도 한다. 

 

테스트 환경 설정

react testing library 

react testing library는 cra, create-react-app에 기본으로 설치되어 있다.

그래서 cra를 사용하는 경우에는 굳이 설치할 필요가 없고 만약 수동으로 설치하고 싶다면 

npm i @testing-library/~

으로 설치하면 된다.

뒤에 ~도 사용한 이유는 필요한 패키지가 다를 수 있기 때문이고 만약 cra와 동일한 환경을 원하면 

@testing-library/jest-dom
@testing-library/react
@testing-library/user-event

 3개를 설치하면 된다. 

 

그리고 Hook을 테스트하기 위해서는 cra라도 하나의 패키지를 별도로 설치해야한다. 

npm install --save-dev @testing-library/react-hooks

 

jest 

react testing library는 기본적으로 jest로 세팅한다. 

jest의 핵심 기능으로는 expect와 mock이 있다. 

 

expect의 경우 react testing library의 jest-dom을 사용하면 dom 객체를 확인하는 함수 등을 사용할 수 있다. 

//@testing-library/jest-dom

const dom = getByText('someting')
expect(dom).toBeInTheDocument() //dom이 있으면 통과

const button = getByTestId('mad-button')
expect(button).toHaveClass('.ok') // .ok 클래스가 있으면 통과
expect(button).not.toHaveAttribute('disalbed') // disabled 상태의 버튼이 아니면 통과

const validVorm = getByTestId('valid-form')
expect(form).toBeValid() // form 이 valid면 통과

 

mock의 경우 테스트에서 유용하게 사용할 수 있는 특정 함수, 패키지를 목킹한다. 

jest.fn, jest.mockImplementation, jest.mockReturnValue로 사용하고, expect로 몇 번 호출됐는지 

어떤 인자로 들어갔는지, 마지막 결과가 어떤지 등 확인이 가능하다. 

const mockFn1 = jest.fn()
mockFn()
expect(mockFn1).toHaveBeenCalledTimes(1) // mock 함수가 1번 호출되면 통과, 함수의 호출 횟수를 확인

const mockFn2 = jest.fn()
const fn2Arg = { name: 'willy', age: 24}
mockFn(fn2Arg)
expect(mockFn2).toHaveBeenLastCalledWith(fn2Arg) // fn2Arg와 동일한 인자가 마지막으로 호출 되었을 때 함수의 인자로 들어왔으면 통과

const mockFn3 = jest.fn().mockReturnValue('hello') //hello를 리턴하는 목업함수
expect(mockFn3()).toBe('hello')

const mockFn4 = jest.fn().mockImplementation((number => number * number)) //특정 로직으로 작동하는 목업함수를 만듬
expect(mockFn4(2)).toBe(4) // 2*2 = 4

 

컴포넌트 테스트 

컴포넌트 테스트는 디자인과 로직을 테스트를 말한다. 

▶ 디자인 테스트 : 디자인의 변화를 검증하는 것이다. 

▶ 로직 테스트 : 컴포넌트 내부에 있는 함수를 검증한다. 

 

컴포넌트 테스트를 할 때는 함수들을 최대한 분리해서 하는 것을 추천한다고 한다. 

 

먼저 디자인 테스트를 해보자 

import "./style.css";

const Button = ({ styleType = "default", children, ...props }) => {
  return (
    <button className={styleType} {...props}>
      {children}
    </button>
  );
};

export default Button;
// components/style.css
button {
  font-size: 13px;
  line-height: 16px;
  letter-spacing: -0.3px;
  text-align: center;
  margin: 0 10px;
  outline: none;
  cursor: pointer;
  border: 1px solid;
  padding: 4px 20px;
  height: 58px;
  border-radius: 10px;
}

.default {
  background: #fff;
  border-color: black;
  color: black;
}
.danger {
  color: #fff;
  background: red;
  border-color: #ffccc7;
}
.primary {
  color: #fff;
  background: blue;
  border-color: #bae7ff;
}

다음과 같이 테스트를 위한 컴포넌트를 만들었다. 

Button 컴포넌트는 styleType을 통해서 버튼의 class를 지정한다. 

 

이제 Button 컴포넌트를 렌더링하고 실제 컴포넌트가 전달받은 class를 사용하고 있는지 테스트를 해보겠다. 

// components/button.test.js
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import Button from "./";

describe("버튼 테스트", () => {
  test("styleType이 className으로 사용됨", () => {
    render(<Button styleType="danger">버튼</Button>);
    const button = screen.getByRole("button");
    
    expect(button).toHaveClass("danger");
  });
});

render 함수를 사용하면 테스트에서 컴포넌트를 렌더링할 수 있다. 

 

그리고 screen의 getByRole 함수를 사용해서 버튼 태그를 가지고 온다. 

가지고 온 button 태그에 expect의 toHaveClass에 입력한 styleType을 넣어서 가지고 있는지 확인한다. 

 

다음으로 Hook을 테스트 해보자!

import { useState } from "react";

const useSlowState = (initialState = undefined) => {
  const [state, setState] = useState(initialState);

  const changeState = (value) =>
    setTimeout(() => {
      setState(value);
    }, 5000);

  return [state, changeState];
};

export default useSlowState;

세상 의미없는 Custom Hook을 하나 만들었다. 

짧게 설명하자면 useState를 사용해서 state를 만들고 setState는 5초의 딜레이가 있고

그 후 업데이트가 된다. 

 

import { act, renderHook } from "@testing-library/react-hooks";
import useSlowState from "./useSlowState";

describe("useSlowState 테스트", () => {
  test("업데이트를 호출하고 정상적으로 업데이트가 수행됨", async () => {
    const utils = renderHook(() => {
      const [state, setState] = useSlowState(0);

      return { state, setState };
    });

    // 초기값이 세팅 확인
    expect(utils.result.current.state).toBe(0);

    await act(() => utils.result.current.setState(10));
    await utils.waitFor(() => utils.result.current.state !== 0);

    // 업데이트 확인
    expect(utils.result.current.state).toBe(10);
  });
});

Hook 테스트를 위해서 renderHook이라는 함수를 사용했다. 

 

renderHook에는 렌더링을 대기하는 함수와 결과를 확인하는 객체로 이루어져 있다. 

act 함수는 React에서 상태가 업데이트되었으니 다시 렌더링하라고 지시하는 함수이다. 

그리고 waitFor 함수는 변화가 발생할 때까지 기다리는 함수이다. 

 

타이머가 있는 함수 

import { useState } from "react";
import Button from "../button";

const Counter = () => {
  const [count, setCount] = useState(0);
  const countUP = () => setTimeout(() => setCount(count + 1), 1000);
  const countDown = () => setTimeout(() => setCount(count - 1), 1000);

  return (
    <>
      <div>
        숫자: <span data-testid="counter-value">{count}</span>
      </div>

      <Button styleType="danger" onClick={countUP}>
        up
      </Button>
      <Button styleType="primary" onClick={countDown}>
        down
      </Button>
    </>
  );
};

export default Counter;

테스트 코드를 작성할 때 타이머가 필요한 경우가 있다. 

사실 위 커스텀 훅도 마찬가지인 경우이다. 그럴 때 쉽게 테스트하는 방법이 있다. 

 

import Counter from "./";
import { render, screen, fireEvent, act } from "@testing-library/react";
describe("counter", () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });
  test("버튼을 누르고 1초 뒤에 카운트가 올라간다.", async () => {
    render(<Counter />);
    fireEvent.click(screen.getByText("up"));
    expect(screen.getByTestId("counter-value").textContent).toBe("0");

    act(() => {
      jest.advanceTimersByTime(1200); // 1초 이상 지나감
    });

    expect(screen.getByTestId("counter-value").textContent).toBe("1");
  });
  test("버튼을 누르고 1초 뒤에 카운트가 내려간다.", () => {
    //생략
  });
});

advenceTimersByTime은 시간을 넘기는 함수이다. 

 

테스트 전에 미리 세팅을 해주는 beforeEach 단계에서 useFakeTimers로 타이머를 세팅하고 

실제 필요한 곳에서 advanceTimersByTime을 호출해서 시간을 넘길 수 있다. 

 

여기서 act 함수를 통해서 advanceTimersByTime을 사용하는데, 컴포넌트가 비동기로 업데이트가 

발생하기 때문에 act로 감싸줘야한다. 

( 해결 링크 )

 

 

 

반응형