✅ 참고자료
해당 글은 코딩앙마 유튜브의 React Testing Library 강의를 참고하여 작성했습니다
전체 코드를 볼 수 있는 저의 Github URL입니다 https://github.com/yeafla530/jest-practice
이 전 글을 보실 분들은 아래를 눌러주세요
✅ react-test-library
🔴 특징
- 렌더링 결과에 집중
- 실제 DOM에 대해 신경을 많이 쓰고 컴포넌트의 인스턴스에 대해 신경쓰지 않고, 실제 화면에 무엇이 보여지는지, 어떤 이벤트가 발생했을 때 화면에 원하는 변화가 생겼는지 이런것을 확인하기에 더 최적화 되어 있음
- jest-dom을 이용해 DOM에 관련된 `matcher`를 추가해줌
- react 공식문서에서도 추천하는 testing방법
- react-test-library 모듈이 @testing-library/react로 옮겨짐
🔴 react 폴더 생성
-- create react app
npx create-react-app rtl-tutorial
npm install @testing-library/any-framework
🔴 기본 문법: 찾기
👉getBy : 하나의 요소만 가져올 수 있다
- getByText(): text로 가져오기
- /로그인 해주세요/ : 일부 text만 작성가능
- "로그인 해주세요" : `""`로 작성할 경우 일부 text만 적으면 찾지 못함
test("제목이 있다", () => {
render(<MyPage />)
const titleEl = screen.getByText("안녕")
expect(titleEl).toBeInTheDocument()
})
- getByRole(): HTML 요소로 가져오기
요소가 여러개일때는 못가져옴
level을 통해 몇번째 인자를 가져올지 설정가능
- h1 ~ h6 : heading
- button: button
- input, textarea : textbox
- a : link
- checkbox: checkbox
- radio: radio
- select: combobox
export default function MyPage() {
return (
<div>
<div>
<h1>안녕</h1>
<h2>world</h2>
</div>
<div>
<label htmlFor="username">이름</label>
<input type="text" id="username"/>
</div>
<div>
<label htmlFor="profile">자기소개</label>
<textarea id="profile"/>
</div>
</div>
)
}
import {render, screen} from "@test-library/react"
import MyPage from "./MyPage"
// 요소가 여러개일 때 level을 통해 하나의 요소만 정함
test("제목이 있다", () => {
render(<MyPage />)
// getByRole : heading이 여러개면 못가져옴
// level: heading요소중 첫번째것 (h1)
const titleEl = screen.getByRole("heading", {
level: 1,
})
expect(titleEl).toBeInTheDocument()
})
// textbox가 여러개일 경우 label의 name을 통해 하나의 요소만 찾아낸다
test("input요소가 있다", () => {
render(<MyPage/>)
const inputEl = screen.getByRole("textbox", {
name: "자기소개",
})
expect(inputEl).toBeInTheDocument();
})
- getByAltText(): 이미지의 alt text가져오기
test("로고 이미지가 잘 나온다", () => {
render(<App/>)
const logoEl = screen.getByAltText("logo")
expect(logoEl).toBeInTheDocument()
})
- getByLabelText(): label의 text를 이용해 textbox를 찾아준다
<div>
<label htmlFor="profile">자기소개</label>
<textarea id="profile"/>
</div>
test("input요소가 있다", () => {
render(<MyPage/>)
// label의 textbox를 찾아줌
// 자기소개 label이 여러개일 때, selector를 이용해 textarea인지 input인지 설정가능
// readOnly가 아닐 경우 onChange를 넣어야 에러가 나지 않음
const inputEl = screen.getByLabelText("자기소개", {
selector: "textarea"
})
expect(inputEl).toBeInTheDocument();
})
- getByDisplayValue() : textbox의 value를 찾아줌
<div>
<label htmlFor="username">이름</label>
<input type="text" id="username" value="Tom" readOnly/>
</div>
test("getByDisplayValue로 요소찾기", () => {
render(<MyPage/>)
const inputEl = screen.getByDisplayValue("Tom")
expect(inputEl).toBeInTheDocument();
})
- getByTextId(): 요소안의 data-testid의 값으로 찾아줌
{/* 의미없는요소 */}
<div data-testid="my-div"/>
// 최후의 수단
test("my div가 있다", () => {
render(<MyPage/>)
const inputEl = screen.getByTestId("my-div")
expect(inputEl).toBeInTheDocument()
})
👉 getAllBy: DOM특정 모든 요소들 가져오기
매칭되는 요소들의 배열을 반환하고 일치하는게 없다면 에러가 난다
- getByAllRole(listitem)
- toHaveLength로 개수 체크 가능
- 만약 빈 배열로 넘겨줬다면, li가 생성되지 않아 에러가 남
const users = ["Tom", "Jane", "Mike"]
test("li는 3개 있다", () => {
render(<UserList users={users}/>)
const liElements = screen.getAllByRole("listitem")
expect(liElements).toHaveLength(users.length); // 개수 체크
})
👉 queryBy / queryAllBy: 없는 요소 찾기에 적합
요소가 없는 경우 에러를 반환하지 않고, null이나 빈배열을 반환한다
없는 요소를 찾는 경우 적합하다
- queryByRole / queryAllByRole
// null 반환
test("queryByRole 빈 배열을 넘겨준 경우 요소에 없다", () => {
render(<UserList users={[]}/>)
const liElements = screen.queryByRole("listitem")
expect(liElements).not.toBeInTheDocument()
})
// 빈 배열을 반환한다
test("queryAllByRole 빈 배열 넘겨준 경우0개", () => {
render(<UserList users={[]}/>)
const liElements = screen.queryAllByRole("listitem")
expect(liElements).toHaveLength(0); // 개수 체크
})
👉 findBy : Promise반환
Promise를 반환, 찾는 요소가 있으면 resolve, 없으면 reject
최대 1초를 기다리며 해당 요소가 있는지 판별
- findByRole : 요소를 시간 안에 찾을 수 있는지 체크
import {useState, useEffect} from "react"
export default function UserList({users}) {
const [showTitle, setShowTitle] = useState(false)
useEffect(()=>{
setTimeout(()=>{
setShowTitle(true)
}, 500)
}, [])
return (
<>
{showTitle && <h1>사용자 목록</h1>}
<ul>
{users.map(user => (
<li key={user}>{user}</li>
))}
</ul>
</>
)
}
test("잠시 후 제목이 나타난다", async () => {
render(<UserList users={users}/>)
const titleEl = await screen.findByRole("heading", {
name: "사용자 목록"
}, {
// 시간 변경하고 싶은 경우 timeout요소 추가
timeout: 2000
})
expect(titleEl).toBeInTheDocument();
})
🔴 기본 문법 : 유저 이벤트
package.json "@testing-library/user-event": "^13.5.0", 13버전은 더이상 지원 안함
14버전으로 업데이트
// package.json에서 user-event삭제 후
npm install --save @testing-library/user-event
👉 userEvent
- Promise를 반환하기 때문에 async, await 비동치 처리
버튼을 누를때마다 login, logout이 변경되는 코드 테스트
import {useState} from "react"
export default function Login() {
const [isLogin, setIsLogin] = useState(false)
const onClickHandler = () => {
setIsLogin(!isLogin)
}
return(
<>
<button onClick={onClickHandler}>{isLogin ? "Logout" : "Login"}</button>
</>
)
}
import userEvent from '@testing-library/user-event'
describe("Login test", () => {
test("처음에는 Login버튼이 있다", () => {
render(<Login/>)
const btnEl = screen.getByRole("button")
expect(btnEl).toHaveTextContent("Login")
})
const user = userEvent.setup()
test("click button", async () => {
render(<Login/>)
const btnElement = screen.getByRole("button")
await user.click(btnElement)
expect(btnElement).toHaveTextContent("Logout")
})
test("tab, space, enter 동작", async () => {
render(<Login />)
const btnEl = screen.getByRole("button")
expect(btnEl).toBeInTheDocument()
await user.tab()
screen.debug()
await user.keyboard(" ")
await user.keyboard(" ")
screen.debug()
await user.keyboard("{Enter}")
screen.debug()
expect(btnEl).toHaveTextContent("Logout")
})
})
✅ MSW를 활용한 mock API 테스트
msw란?
mock service worker의 약자
MSW는 API 요청을 가로채서 사전에 설정해둔 목업 데이터를 넘겨주도록 설정해 주는 도구
마치 브라우저에서 마치 백엔드 API인척 하여 프론트엔드의 요청에 가짜 데이터를 응답해주는 서비스
왜 msw를 사용하는가?
테스트를 작성하다보면 더미 데이터를 만들거나 많은 API를 모킹해가며 테스트를 진행해야한다.
그리고 이건 꽤 많은 비용을 발생시키는 작업이다.
이런 문제를 해결하기 위해 MSW도구를 통합 테스트에 도입하여 활용할 수 있다.
msw 설치
npm install msw --save-dev
폴더구조
├── src
│ ├── mocks
│ │ ├── handlers.ts
│ │ └── server.ts
│ ├── setupTests.ts
순서
- TodoList.js 생성
- TodoList.test.js 생성
- /mocks/server.js 생성
- /mocks/handlers.js 생성
- setupTests.js 코드 넣기
- TodoList.test.js 코드 수정
1. TodoList.js 생성
`https://jsonplaceholder.typicode.com/todos` 에서 더미 데이터 가져와서 todoList에 저장
import {useEffect, useState} from "react"
import fetch from "node-fetch";
export default function TodoList() {
const [todoList, setTodoList] = useState([])
const [errorMsg, setErrorMsg] = useState("")
useEffect(()=>{
fetch('https://jsonplaceholder.typicode.com/todos')
.then(res => res.json())
.then(json => {
setTodoList(json)
})
.catch(()=>{
setErrorMsg("에러 발생..")
})
}, [])
return( <>
<h1>Todo</h1>
{errorMsg ? <p>{errorMsg}</p> : (
<ul>
{todoList.map(todo => (
<li
key={todo.id}
style={{
textDecoration: todo.completed ? "line-through" : undefined
}}>
{todo.title}
</li>
))}
</ul>
)}
</>)
}
2. TodoList.test.js
import { render, screen } from '@testing-library/react';
import TodoList from './TodoList';
import {server} from "../mocks/server"
import {rest} from "msw"
describe('TodoList', () => {
test("Todo라는 제목이 있다", () => {
render(<TodoList/>)
const titleEl = screen.getByText("Todo")
expect(titleEl).toBeInTheDocument()
})
})
3. /mocks/server.js 생성
msw를 사용하기 위한 기본 설정
import { setupServer } from 'msw/node'
import {handlers} from "./handlers"
export const server = setupServer(...handlers)
4. /mocks/handlers.js 생성
해당 파일에서 낚아챌 api 응답 설정
import { rest } from 'msw'
export const handlers = [
// Match a GET request to a third-party server.
rest.get('https://jsonplaceholder.typicode.com/todos', (req, res, ctx) => {
return res(
ctx.status(200), // 500
ctx.json([
{
id: 1,
title: '청소',
completee: true
},
{
id: 2,
title: '설거지',
completee: true
},
{
id: 3,
title: '숙제',
completee: false
},
])
)
}),
]
5. setupTests.js 코드 설정
server.js를 사용하여 mocking test 실시
// src/setupTests.js
import { server } from './mocks/server.js'
// Establish API mocking before all tests.
beforeAll(() => server.listen())
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers())
// Clean up after the tests are finished.
afterAll(() => server.close())
6. TodoList.test.js 수정
msw를 이용한 코드 작성 및 강제로 에러가 나는 상황 테스트
import { render, screen } from '@testing-library/react';
import TodoList from './TodoList';
import {server} from "../mocks/server"
import {rest} from "msw"
describe('TodoList', () => {
test("Todo라는 제목이 있다", () => {
render()
const titleEl = screen.getByText("Todo")
expect(titleEl).toBeInTheDocument()
})
// 에러 설정한 test가 먼저나오더라도
// 다음 test에 영향을 주지 않음
test("에러가 났을 때 에러 메세지를 보여준다", async () => {
server.use(
// Match a GET request to a third-party server.
rest.get('https://jsonplaceholder.typicode.com/todos', (req, res, ctx) => {
return res(
ctx.status(500)
)
}),
)
render()
const error = await screen.findByText("에러 발생..")
expect(error).toBeInTheDocument()
})
// handler.js 코드에 작성한 3개의 list가 나오게 됨
test("리스트가 잘 나온다 (3개)", async () => {
render()
const list = await screen.findAllByRole("listitem")
expect(list).toHaveLength(3)
})
})