React와 TDD
React 환경에서 테스트하기
- 리액트에서 테스트는 Testing Library, Jest를 이용해서 가능
- 역할이 각각 다르기 때문에 리액트에 대한 테스트를 진행할 때는 두 라이브러리 모두 필요
- 둘 다 리액트에만 사용하는 라이브러리는 아님 뷰 등 다른 프레임워크에서도 사용 가능
- Testing Library에서 리액트용 React Test Libraray를 제공하고 있기 때문에 CRA를 이용해 프로젝트를 생성하면 자동으로 Testing Library 를 이용할 수 있음
→ 테스트를 실행하고 싶은 컴포넌트나 클릭 이벤트 등 다양한 곳에 쓸 수 있음
- Jest는 자바스크립트의 Testing Framework/Test Runner로써, 테스트 파일을 자동으로 찾아 테스트를 실행함
→ 테스트 실행 결과가 기대만큼 올바른 값을 가지고 있는지를 함수를 이용하여 체크 → 테스트 성공 여부를 판단해줌
1. React 기본 테스트 환경 확인하기
1.1 CAR를 이용해서 React 프로젝트를 생성
- npx create-react-app을 이용해서 리액트 프로젝트를 생성했다면 테스트 환경이 설정되어 있기 때문에 테스트가 가능함
- package.json 파일을 확인하면 @testing 이라는 접두사가 붙은 3개의 라이브러리가 아래와 같이 확인 가능
- @testing-library/jest-dom : Jest-dom 제공하는 custom matcher를 사용할 수 있게 도와줌
- @testing-library/react : 컴포넌트 요소를 찾기 위한 쿼리가 포함되어 있음
- @testing-library/user-event : click 등 사용자 이벤트에 이용됨
1.2 테스트 파일 확인하기
// src/App.test.js
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
- test 함수는 Jest 함수로 테스트를 실행할 때 반드시 이용하는 함수임
- test 함수의 첫 번재 인자는 테스트가 어떤 내용인지 나중에 읽어도 테스트 내용을 알 수 있는 설명을 작성함
- 두 번째 인자는 하고자 하는 테스트를 함수의 형태로 넣음
→ 첫번째 줄을 보면 테스트하고자 하는 컴포넌트를 render () 함수로 전달함. react-testing-library에서는 테스트를 진행할 컴포넌트를 render() 함수의 인자로 전달함
→ 두번째 줄에 있는 screen의 다양한 메소드 중 getByText() 메서드를 이용하여 render()에서 가져온 App 컴포넌트 중 "learn react"라는 문자열이 있는지 확인하여 linkElement에 할당하고 있음 ("i"는 Regular Expression으로 "I"를 붙임으로써 대소문자를 구분하지 않게 만들어 줌)
→ 세번째 줄에서는 expect 함수의 인자로 지정한 요소가 document.body에 존재하는지 toBeInTheDocument 함수를 사용하여 체크하고 있음. 여기서 tobeInTheDocument 함수는 matchers 함수라고 부름
- App.test.js 파일 중 이용되고 있는 test 함수, expect 함수는 Jest 함수이고
- toBeInTheDocument는 jest-dom 라이브러리에 포함된 Custom matchers임
// src/setUpTest.js
// 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 '@testing-library/jest-dom';
- jest-dom은 위의 setUpTest.js 파일 내에서 import 되고 있음
- import를 삭제하면 App.test,js의 toBeInTheDocument 함수를 사용할 수 없음!
- Home 컴포넌트 안에 span 태그 안에 learn React 텍스트를 작성했고 i를 붙임으로서 대소문자 구별이 없게 되었음
- Home 컴포넌트는 최상위 App 컴포넌트의 하위 컴포넌트이고 App 컴포넌트는 빈 HTML 파일에 뿌려져서 document.body 안에 learn react를 가진 요소가 있게 됨
- 그래서 테스트 통과
1.3 간단한 테스트 만들어 보기
- 테스트를 실행하기 위해서는 파일명을 <파일명>.test.js 와 같이 생성해야 함(<파일명>.spec.js 도 가능)
- 파일명을 이렇게 작성하고 테스트 하면, Jest 가 테스트 파일로 판단하여 작동
- test 함수의 첫번째 인자에 테스트에 대한 설명을, 두번째 인자에는 테스트 내용을 함수의 형태로 작성하여 테스트에 패스함
- toBe 함수는 matchers 함수 중 하나로 expect 함수에 지정한 값이 toBe 함수에 지정한 값과 일치하는지 체크함
- 지정한 값과 일치하지 않을 경우 테스트가 왜 실패했는지 출력 됨
- test 함수 대신 it 함수를 사용해도 같은 결과가 나옴
- describe 함수를 사용하면 it 함수나 test 함수를 하나의 파일에 여러 개 포함 시킬 수 있음
- describe 함수 블록은 Test Suites라고 불리며 test/it 함수 블록은 Test(Test Case) 라고 함
2. 직접 컴포넌트 생성하여 테스트 하기
2.1 컴포넌트 만들기
- 이전에 CAR를 이용해 생성했던 리액트 앱에 Light 컴포넌트를 추가 해줌
Light.js
import { useState } from "react";
function Light({ name }){
const [light, setLight] = useState(false);
return (
<div>
<h1>
{name} {light ? 'ON' : "OFF"}{' '}
</h1>
<button
onClick={() => setLight(true)}
disabled={light ? true : false}
>ON</button>
<button
onClick={() => setLight(false)}
disabled={!light ? true : false}
>OFF</button>
</div>
);
}
export default Light;
App.js
import './App.css';
import React from 'react'
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import Light from './Light';
.. 생략 ..
function App() {
return (
<BrowserRouter>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/mypage">MyPage</Link>
</li>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
<li>
<Link to="/light">Light</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path='/mypage' element={<MyPage />}/>
<Route path='/dashboard' element={<Dashboard />} />
<Route path='/light' element={<Light name="전원" />} />
</Routes>
</div>
</BrowserRouter>
)
}
export default App;
2.2 컴포넌트 테스트하기
- Light.test.js 파일을 만들고 아래와 같이 작성함
import { render, screen } from '@testing-library/react';
import Light from './Light';
it('renders Light Component', () => {
render(<Light name="전원" />);
const nameElement = screen.getByText(/전원 off/i);
expect(nameElement).toBeInTheDocument();
})
- 위에서 했던 테스트와 거의 동일한 테스트
- render에서 가져온 Light 컴포넌트에 전원 off 문자열이 있는지 확인
it('off button disabled', () => {
render(<Light name="전원" />);
const offButtonElement = screen.getByRole('button', { name: 'OFF' });
expect(offButtonElement).toBeDisabled();
})
- 위의 테스트는 OFF 버튼이 disabled로 되어 있는지 matchers 함수의 toBeDisabled 함수를 이용해 테스트를 작성함
- getByRole을 이용해 button을 지정했고, 버튼이 2개이므로 옵션의 name을 이용하여 OFF 버튼을 찾음
- 현재 OFF 버튼이 disabled 상태이기 때문에 테스트 결과도 PASS로 나옴
it('on button enable', () => {
render(<Light name="전원" />);
const onButtonElement = screen.getByRole('button', { name: 'ON' });
expect(onButtonElement).not.toBeDisabled();
});
- 위의 테스트는 ON 버튼이 disableld가 아니라는 것을 테스트 하기 위한 것
- 간단하게 toBeDisabled 앞에 not을 붙이면 구현할 수 있음
import { fireEvent, render, screen } from '@testing-library/react';
import Light from './Light';
it('change from off to on', () => {
render(<Light name="전원" />);
const onButtonElement = screen.getByRole('button', { name: 'ON' });
fireEvent.click(onButtonElement);
expect(onButtonElement).toBeDisabled();
})
- 버튼 클릭 이벤트의 유무도 테스트로 구현할 수 있음
- fiveEvent를 사용하려면 import를 하고 fiveEvent의 clike 메서드에 전달 인자로 테스트하고자 하는 요소를 전달함