테스트는 구현된 애플리케이션이 오류없이 잘 실행되는지 검증하는 절차이다.
테스트는 되게 다양한데, 프론트에서는 주로 단위 테스트, 통합 테스트, E2E 테스트를 진행한다.
단위 테스트(unit test)는 함수나 클래스 단위로 테스트하는 것을 의미한다. 함수 단위로 테스트 케이스를 만들기 때문에 어디서 문제가 발생했는지 빨리 찾을 수 있다.
통합 테스트(integration test)는 단위 테스트보다 큰 범위의 테스트이다. 여러 개의 단위 테스트가 잘 동작하고 있는지 검증할 수 있다.
E2E 테스트는 실제 사용자 환경에서 테스트하는 것을 의미한다. 웹 프론트엔드는 브라우저 환경에서 E2E 테스트를 할 수 있다. 3개의 테스트 중 시간이 가장 많이 소요된다. (ex. 기능 테스트, UI 테스트)
프론트엔드에서 테스트의 대상은 크게 UI, 사용자 이벤트 처리, API 통신, 앱의 상태 관리이다.
UI를 테스트하는 것은 개발자가 의도한 대로 UI가 잘 렌더링되는지 검증한다. UI 테스트 종류는 2가지로 나눌 수 있다.
=> 시각적 요소(UI) 테스팅을 자동화 하는 것은 비용대비 효율이 좋지 않음?
=> 자동화 대신 수동적으로 테스팅할 수 있는 UI 개발 도구?인 storybook을 사용
시각적 회귀 테스트 (Visual Regression Test)
사용자가 버튼을 누르거나 입력을 하는 등의 이벤트가 잘 처리되는지를 검증한다.
브라우저 환경, Node.js 환경에서 테스트가 가능하다. Node.js 환경에서 테스트하는 경우
실제 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 달리 expect
과 should
API는 체이닝이 가능하다.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"
},
// add.js
const add = (num1, num2) => num1 + num2;
module.exports = add;
파일이름.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);
});
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의 beforeEach
와 afterEach
를 이용한다.
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');
)
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씩 증가하는지 테스트하기 위한 코드이다.
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초 후에 카운터 컴포넌트가 렌더링 되는지 테스트한다.