프론트엔드 테스트

hj·2021년 11월 9일
4

목록 보기
6/6

테스트

테스트는 구현된 애플리케이션이 오류없이 잘 실행되는지 검증하는 절차이다.

테스트 종류

테스트는 되게 다양한데, 프론트에서는 주로 단위 테스트, 통합 테스트, E2E 테스트를 진행한다.

단위 테스트(unit test)는 함수나 클래스 단위로 테스트하는 것을 의미한다. 함수 단위로 테스트 케이스를 만들기 때문에 어디서 문제가 발생했는지 빨리 찾을 수 있다.

  • sociable test: 테스트하려는 유닛과 의존성이 있는 유닛을 포함하여 테스트 진행.
  • solitary test: 테스트하려는 유닛과 의존성이 있는 유닛을 mocking해서 테스트 진행.

통합 테스트(integration test)는 단위 테스트보다 큰 범위의 테스트이다. 여러 개의 단위 테스트가 잘 동작하고 있는지 검증할 수 있다.

  • broad test: 의존성있는 모든 코드를 포함하여 테스트 진행.
  • narrow test: 의존성이 있는 코드는 mocking하여 테스트 진행.

E2E 테스트는 실제 사용자 환경에서 테스트하는 것을 의미한다. 웹 프론트엔드는 브라우저 환경에서 E2E 테스트를 할 수 있다. 3개의 테스트 중 시간이 가장 많이 소요된다. (ex. 기능 테스트, UI 테스트)

테스트 대상

프론트엔드에서 테스트의 대상은 크게 UI, 사용자 이벤트 처리, API 통신, 앱의 상태 관리이다.

UI(시각적 요소)

UI를 테스트하는 것은 개발자가 의도한 대로 UI가 잘 렌더링되는지 검증한다. UI 테스트 종류는 2가지로 나눌 수 있다.

  • 스냅샷 테스트: 작성한 HTML의 구조가 의도한 대로 구성되어있는지 검증하는 테스트이다. (ex. 직접 코드를 작성하여 비교, Jest)
  • 시각적 회귀 테스트: 브라우저에 렌더링된 화면을 캡쳐하여 검증하는 테스트이다. (수동으로 비교 가능: storybook, 자동화된 UI 시각적 회귀 테스트: storybook에서 만든 Chromatic, percy)

=> 시각적 요소(UI) 테스팅을 자동화 하는 것은 비용대비 효율이 좋지 않음?
=> 자동화 대신 수동적으로 테스팅할 수 있는 UI 개발 도구?인 storybook을 사용

시각적 회귀 테스트 (Visual Regression Test)

사용자 이벤트 처리

사용자가 버튼을 누르거나 입력을 하는 등의 이벤트가 잘 처리되는지를 검증한다.
브라우저 환경, Node.js 환경에서 테스트가 가능하다. Node.js 환경에서 테스트하는 경우

API 통신

실제 API 서버 사용(E2E test),

리액트 팀 -> jest 추천

테스트 도구

- Chai
assertion library

Chai에는 TDD 스타일의 assert API, BDD 스타일의 expect, should API 3가지가 있다.

  • assert API node.js의 Assert 모듈과 비슷하다.
assert('foo' !== 'bar', 'foo is not bar');
assert(Array.isArray([]), 'empty arrays are arrays');

assert.equal(3, '3', '== coerces values to strings');

assert 함수의 마지막 파라미터로 메시지를 넣을 수 있는데, 테스트가 통과하지 못했을 때 보여지는 메시지이다.

  • assert API 달리 expectshould API는 체이닝이 가능하다.
    chai에는 to, be, is 등의 language chain을 제공한다. 이것들은 테스트 결과에 영향을 미치지는 않고, 코드의 가독성을 높여준다.

- Mocha
test framework => test runner를 포함하고 있다. assertion library는 포함하고 있지 않기 때문에 chai나 Node.js의 assert module 등을 사용할 수 있다.

- Jest
test framework => All-in-One으로 test runner, assertion library를 포함하고 있다.

  • Jest 설치
    npm install --save-dev jest

  • package.json 수정

    "scripts": {
      "test": "jest"
    },
  • test할 파일 생성
// add.js
const add = (num1, num2) => num1 + num2;

module.exports = add;
  • test 파일 생성: 파일이름.test.js 형식으로 만든다.
/// add.test.js
const { add } = require('./add');

test('2 + 3은 5', () => {
  expect(add(2, 3)).toBe(5);
});

test('2 + 3은 7 아님', () => {
    expect(add(2, 3)).not.toBe(7);
});
  • jest 실행
    npm test

리액트 프로젝트에 테스트 적용

cra를 통해 프로젝트를 생성한 경우에는 jest와 testing-library가 설치되어있다.

카운터 프로젝트

Counter.jsx 파일을 생성하여 카운터 컴포넌트를 만든다.

// Counter.jsx
import React, { useState } from 'react';

function Counter() {
    const [count, setCount] = useState(0);
    const handleIncrease = () => {
        setCount(count + 1);
    };

    const handleDecrease = () => {
        setCount(count - 1);
    };
    return (
        <div>
            <h1>Counter</h1>
            <p data-testid="count">{count}</p>
            <button
                type="button"
                onClick={handleIncrease}
                data-testid="increase"
            >
                +
            </button>
            <button
                type="button"
                onClick={handleDecrease}
                data-testid="decrease"
            >
                -
            </button>
        </div>
    );
}

export default Counter;

App.js에 카운터 컴포넌트를 렌더링 해준다. setTimeout 함수를 이용해서 3초 후에 카운터 컴포넌트가 렌더링 되도록 해주었다.(로딩)

// App.js
import React, { useEffect, useState } from 'react';
import Counter from './component/Counter';

function App() {
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        setTimeout(() => {
            setLoading(false);
        }, 3000);
    }, []);

    return (
        <div
            style={{
                margin: '1rem',
                padding: '1rem',
            }}
            data-testid="appContainer"
        >
            {loading ? <h2>로딩 중...</h2> : <Counter />}
        </div>
    );
}

export default App;

로딩중로딩 푸

테스트 기본작업

테스트마다 React 트리를 document의 DOM 엘리먼트에 렌더링한다. 테스트가 끝나면 정리(clean up)하고, document 트리에서 엘리먼트를 제거해준다.

jest의 beforeEachafterEach를 이용한다.

import { unmountComponentAtNode } from "react-dom";

let container = null;
beforeEach(() => {
  // DOM 엘리먼트를 렌더링 대상으로 설정
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // 종료시 정리
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

테스트가 끝났을 때 정리(clean up)은 해주어야 테스트가 격리되어 에러가 났을 때 디버깅하기 쉬워진다.

카운터 테스트 코드

컴포넌트 렌더링

test('Counter라는 글자가 렌더링 되는가?', () => {
    render(<Counter />);

    const counterEl = screen.getByText(/Counter/i);
    expect(counterEl).toBeInTheDocument();
});

카운터 컴포넌트가 렌더링 됐는지 테스트하기 위한 코드이다. 카운터 컴포넌트가 잘 렌더링 되었다면 Counter 글자를 포함하고 있을 것이다.

컴포넌트 렌더링

onClick 이벤트 테스트

test('+ 버튼을 눌렀을 때 값이 증가하는가?', () => {
    act(() => {
        render(<Counter />, container);
    });

    // 버튼 엘리먼트를 가져와 클릭 이벤트를 트리거
    const increaseBtn = document.querySelector('[data-testid="increase"]');
    expect(increaseBtn.innerHTML).toBe('+');

    act(() => {
        increaseBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    const count = document.querySelector('[data-testid="count"]');
    expect(count.innerHTML).toBe('1');

    act(() => {
        increaseBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    expect(count.innerHTML).toBe('2');
});

+ 버튼을 눌렀을 때 카운터 값이 1씩 증가하는지 테스트하기 위한 코드이다. act()는 컴포넌트 렌더링, 유저 이벤트, 데이터 fetch 같은 것들이 assert(단언) 처리되어 DOM에 적용되도록 해준다. (ex. 버튼을 클릭(increaseBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));) -> 값이 1 증가 되었나?(단언)(expect(count.innerHTML).toBe('2');)

카운터 1씩 증가
test('- 버튼을 눌렀을 때 값이 감소하는가?', () => {
    act(() => {
        render(<Counter />, container);
    });

    // 버튼 엘리먼트를 가져와 클릭 이벤트를 트리거
    const increaseBtn = document.querySelector('[data-testid="decrease"]');
    expect(increaseBtn.innerHTML).toBe('-');

    act(() => {
        increaseBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    const count = document.querySelector('[data-testid="count"]');
    expect(count.innerHTML).toBe('-1');

    act(() => {
        increaseBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    expect(count.innerHTML).toBe('-2');
});

+ 버튼을 눌렀을 때 카운터 값이 1씩 증가하는지 테스트하기 위한 코드이다.

카운터 1씩 감소

앱 테스트 코드

let container = null;
beforeEach(() => {
    container = document.createElement('div');
    document.body.appendChild(container);
    jest.useFakeTimers();
});

afterEach(() => {
    unmountComponentAtNode(container);
    container.remove();
    container = null;
    jest.useRealTimers();
});

App.js에서 setTimeout 함수를 이용해 3초 후에 카운터 컴포넌트가 렌더링되게 했다. 이를 테스트하기 위해 jest의 useFakeTimers 함수를 사용해야 한다.

test('로딩 전', () => {
    const MockCounterComponent = jest.fn();
    jest.mock('./component/Counter', () => (props) => {
        MockCounterComponent(props);
    });

    act(() => {
        render(<App />);
    });

    act(() => {
        jest.advanceTimersByTime(100);
    });

    const appContainer = document.querySelector('[data-testid="appContainer"]');
    expect(appContainer.innerHTML).toContain('로딩 중...');

    act(() => {
        jest.advanceTimersByTime(5000);
    });

    expect(appContainer.innerHTML).toContain('Counter');
});

앱이 로딩되기 전에 로딩 중...이라는 텍스트가 렌더링 되는지 테스트한다. jest의 advanceTimersByTime()함수를 이용해서 100ms 후에 앱 컴포넌트의 렌더링 상태를 체크한다.

test('로딩 후', () => {
    const MockCounterComponent = jest.fn();
    jest.mock('./component/Counter', () => (props) => {
        MockCounterComponent(props);
    });

    act(() => {
        render(<App />);
    });

    act(() => {
        jest.advanceTimersByTime(5000);
    });

    const appContainer = document.querySelector('[data-testid="appContainer"]');

    expect(appContainer.innerHTML).toContain('Counter');
});

앱이 로딩되고 나서인 3초 후에 카운터 컴포넌트가 렌더링 되는지 테스트한다.

앱 컴포넌트

테스트 전체 코드

reference

0개의 댓글