FEํ ์คํธ(3) - React Testing Library
๐์์ํ๋ฉฐ
์ด๋ฒ์ vitest์ ํจ๊ป ์ฌ์ฉ๋์ด ์ฌ์ฉ์ ๊ด์ ์์ ํ ์คํธ ํ ์ ์๊ฒ ๋์์ฃผ๋ React Testing Library์ ๋ํด ์์๋ณด๊ณ ์ ํ๋ค.
React Testing Library๋ ์ค์ ์ฌ์ฉ์๊ฐ UI๋ฅผ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ์ด์ ์ ๋ง์ถฐ ํ ์คํธํ๋ ๊ฒ์ ๊ถ์ฅํ๋ฉฐ, ์ด๋ฅผ ๊ตฌํํ๊ธฐ ์ํ API๋ฅผ ์ดํด๋ณด์.
โ DOM ์์ ์ ํ (get~)
DOM ์์๋ฅผ ์ ํํ๋ ๋ฐฉ์ (Queries)๋ ์ธ ๊ฐ์ง ๋ฐฉ์์ด ์๋ค.
์ข ๋ฅ | ์ค๋ช |
---|---|
getBy | ์ผ์นํ๋ ์์๋ฅผ ์ฆ์ ๋ฐํ, ์์๊ฐ ์์ผ๋ฉด ์๋ฌ ๋ฐ์ |
queryBy | ์ผ์นํ๋ ์์ ๋ฐํ, ์์ผ๋ฉด null ๋ฐํ |
findBy | ๋น๋๊ธฐ์ ์ผ๋ก ์์ ์ฐพ์, ์์๊ฐ ์์ ๋ Promise ๊ฑฐ๋ถ, ์๊ฐ์ด ์ง๋๋ฉด ์ฌ์๋ |
โก๏ธgetBy
์ข ๋ฅ | ์ค๋ช |
---|---|
getByRole | ์ ๊ทผ์ฑ ํธ๋ฆฌ ์ด์ฉ. ๊ฐ์ฅ ๊ถ์ฅ๋จ |
getByLabelText | ์ฃผ๋ก ํผ ํ๋์ ๋ผ๋ฒจ ์ด์ฉํด ๊ฒ์ |
getByPlaceholderText | ํ๋ ์ด์ค ํ๋๋ฅผ ์ด์ฉํด ๊ฒ์ |
getByText | ํ ์คํธ๋ฅผ ์ด์ฉํด ์์ ๊ฒ์ |
getByAltText | alt ์์ฑ์ ๊ฐ์ง ์์๋ฅผ ๊ฒ์ |
getByTitle | title ์์ฑ์ ๊ฐ์ง ์์๋ฅผ ๊ฒ์ |
getByTestId | ํ
์คํธ๋ฅผ ์ํด element์ ์ด๋ฆ์ ๋ถ์. data-testid ๋ฅผ ์ฌ์ฉํด ์ ์ |
getByDisplayValue | ํผ์์ ํ์ฌ ์ ๋ ฅ๋ ๊ฐ ๊ธฐ๋ฐ์ผ๋ก ์์ ์ ํ |
โก๏ธscreen
์ด์ ์์์ ์ดํด๋ณธ ๋ด์ฉ์ ํ์ฉํด ์์๋ฅผ ์ฐพ์๋ณด์. DOM ์์๋ฅผ ์ฐพ์ ๋๋ screen
ํค์๋๋ฅผ ์ฌ์ฉํ๋ค. screen์ document.body์ ๋ฐ์ธ๋ฉ๋์ด ์์ด, screen.getBy~ findBy~ ๋ฑ์ ์ฌ์ฉํด DOM์์ ์์๋ฅผ ์ฐพ์ ์ ์๋ค.
1
2
3
4
5
6
7
8
9
10
11
import { screen, render } from "@testing-library/react"
import { describe, it, expect } from "vitest"
import Example from "./Example"
describe("Example ์ปดํฌ๋ํธ ํ
์คํธ", () => {
it("Example ์ปดํฌ๋ํธ ๋ด ์์๋ฅผ ๊ฒ์ํ๋ค.", () => {
render(<Example />)
const element = screen.getByText("๋ด๊ฐ ์ฐพ๋ ์์")
expect(element).toBeInTheDocument()
})
})
โ ์ด๋ฒคํธ ์๋ฎฌ๋ ์ด์
์์์ DOM์์๋ฅผ ์ฐพ๊ณ ๋ ๋๋ง ํ์ผ๋ ์ด๋ฒ์ ์ฌ์ฉ์ ์ด๋ฒคํธ๋ฅผ ์๋ฎฌ๋ ์ด์ ํด๋ณด์.
โก๏ธfireEvent
๊ฐ๋จํ ์ฌ์ฉ์ ์ด๋ฒคํธ (click, change, keydown ๋ฑ)๋ fireEvent๋ฅผ ์ด์ฉํด ์๋ฎฌ๋ ์ด์ ํ ์ ์๋ค.
์๋ ์์ ๋ ๋ฒํผ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ๊ณ ์ฌ์ฉ์๊ฐ ํด๋ฆญํ๋ ์ํฉ์ ํ ์คํธํ๋ค.
โป๊ณต์ ๋ฌธ์์์๋ fireEvent ๋์ userEvent๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํ๋ฏ๋ก, ์๋์ ์์ฑํ userEvent๋ฅผ ์ฃผ๋ก ์ฌ์ฉํ์.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { render, fireEvent, screen } from "@testing-library/react"
import { vi } from "vitest"
const Button = ({ onClick }) => <button onClick={onClick}>ํด๋ฆญํ์ธ์</button>
it("๋ฒํผ ํด๋ฆญ ์ onClick ํจ์๊ฐ ํธ์ถ๋๋ค", () => {
// vitest๋ฅผ ์ด์ฉํด handleClick์ด๋ ํจ์๋ฅผ ๋ชจํนํ๋ค.
const handleClick = vi.fn()
// ๋ฒํผ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋ง ํ๋ค.
render(<Button onClick={handleClick} />)
//text๋ฅผ ๊ธฐ์ค์ผ๋ก ์์๋ฅผ ์ฐพ๊ณ , ํด๋ฆญ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํจ๋ค.
fireEvent.click(screen.getByText("ํด๋ฆญํ์ธ์"))
// handleClick ํจ์๊ฐ 1๋ฒ ํธ์ถ ๋๋ ๊ฒ์ ๊ฒ์ฆํ๋ค.
expect(handleClick).toHaveBeenCalledTimes(1)
})
fireEvent๋ก ๊ฒ์ฆํ ์ ์๋ ์ด๋ฒคํธ๋ ์๋์ ๊ฐ๋ค.
์ด๋ฒคํธ ์ด๋ฆ | ์ค๋ช | ์์ ์ฝ๋ |
---|---|---|
click | ํด๋ฆญ ์ด๋ฒคํธ | fireEvent.click(screen.getByText("ํด๋ฆญ")) |
change | ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋ (์ ๋ ฅ, ์ ํ) | fireEvent.change(screen.getByLabel("์ด๋ฆ"), { target: { value: '์ ์งฑ๊ตฌ' } }) |
keydown | ํค๊ฐ ๋๋ ธ์ ๋ | fireEvent.keyDown(screen.getByText("ํค๋ณด๋ ๋๋ฆ"), { key: 'Enter', code: 'Enter' }) |
keyup | ํค๊ฐ ๋ผ์ด์ก์ ๋ | fireEvent.keyUp(screen.getByText("ํค๋ณด๋ ๋"), { key: 'Enter', code: 'Enter' }) |
keypress | ํค๊ฐ ๋๋ ค์ก์ ๋ | fireEvent.keyPress(screen.getByText("ํค๋ณด๋ ๋๋ ค์ง"), { key: 'a', code: 'KeyA' }) |
focus | ํฌ์ปค์ค๊ฐ ๊ฐ์ ๋ | fireEvent.focus(screen.getByText("ํฌ์ปค์ค")) |
blur | ํฌ์ปค์ค๊ฐ ์ฌ๋ผ์ง ๋ | fireEvent.blur(screen.getByText("๋ธ๋ฌ")) |
submit | ํผ ์ ์ถ ์ด๋ฒคํธ | fireEvent.submit(screen.getByText("ํผ์ ์ ์ถํจ")) |
mouseOver | ๋ง์ฐ์ค๋ฅผ ์์ ์๋ก ์ฌ๋ ธ์ ๋ | fireEvent.mouseOver(screen.getByText("๋ง์ฐ์ค ์ค๋ฒ")) |
mouseOut | ๋ง์ฐ์ค๋ฅผ ์์ ๋ฐ์ผ๋ก ๋บ์ ๋ | fireEvent.mouseOut(screen.getByText("๋ง์ฐ์ค ์์")) |
drag | ๋๋๊ทธํ ๋ | fireEvent.drag(screen.getByText("๋๋๊ทธ ์ด๋ฒคํธ")) |
dragStart | ๋๋๊ทธ๋ฅผ ์์ํ ๋ | fireEvent.dragStart(screen.getByText("๋๋๊ทธ ์์")) |
dragEnd | ๋๋๊ทธ๋ฅผ ๋๋ผ ๋ | fireEvent.dragEnd(screen.getByText("๋๋๊ทธ ๋")) |
drop | ๋๋กญํ ๋ | fireEvent.drop(screen.getByText("๋๋กญ ์์ญ"), { dataTransfer: { files: [new File([], 'a.txt')] } }) |
input | ์ ๋ ฅ ์ค ๋ฐ์ | fireEvent.input(screen.getByText("์
๋ ฅ"), { target: { value: '์
๋ ฅ ์ค' } }) |
doubleClick | ๋๋ธ ํด๋ฆญ ์ด๋ฒคํธ | fireEvent.doubleClick(screen.getByText("๋๋ธ ํด๋ฆญ")) |
contextMenu | ์ปจํ ์คํธ ๋ฉ๋ด(์ค๋ฅธ์ชฝ ํด๋ฆญ) | fireEvent.contextMenu(screen.getByText("์ค๋ฅธ์ชฝ ํด๋ฆญ")) |
touchStart | ํฐ์น ์์ (๋ชจ๋ฐ์ผ ํ๊ฒฝ) | fireEvent.touchStart(screen.getByText("ํฐ์น ์์")) |
touchEnd | ํฐ์น ์ข ๋ฃ (๋ชจ๋ฐ์ผ ํ๊ฒฝ) | fireEvent.touchEnd(screen.getByText("ํฐ์น ์ข
๋ฃ")) |
touchMove | ํฐ์น ์ด๋ (๋ชจ๋ฐ์ผ ํ๊ฒฝ) | fireEvent.touchMove(screen.getByText("ํฐ์น ์ด๋")) |
scroll | ์คํฌ๋กค ๋ฐ์ | fireEvent.scroll(screen.getByText("์คํฌ๋กค")) |
resize | ์ฐฝ ํฌ๊ธฐ ์กฐ์ | fireEvent.resize(window) |
๊ธฐ๋ณธ์ ์ผ๋ก๋ ํด๋น ์์๋ฅผ ์ฐพ๊ณ , ์ํ๋ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์์ผ์ฃผ๋ฉด ๋์ง๋ง, ๋ค์ ์ถ๊ฐ์ ์ผ๋ก ์์ฑ์ ์์ฑํด์ผ ํ๋ ๊ฒฝ์ฐ๋ ์๋ค.
๐ target๊ณผ value
1
2
fireEvent.change(screen.getByLabel("์ด๋ฆ"), { target: { value: "์ ์งฑ๊ตฌ" } })
fireEvent.input(screen.getByText("์
๋ ฅ"), { target: { value: "์
๋ ฅ ์ค" } })
- target: ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ DOM ์์
- value: ํด๋น ์์์ ์ ๋ ฅ๋ ๊ฐ
์๋ฅผ ๋ค์ด, ๋น๋ฐ๋ฒํธ input์ ๊ฒ์ฆํ๋ ์ฝ๋๋ฅผ ์์ฑํ๋ค๊ณ ์๊ฐํด๋ณด์. ๋น๋ฐ๋ฒํธ๊ฐ ์์๋ฌธ์์ ์ซ์๋ง ์์ฑ ๊ฐ๋ฅํ๋ค๋ฉด, ํน์๋ฌธ์ ์ผ๋ ์ ๋๋ก ์ค๋ฅ๋ฅผ ๋ฐ์ํ๋์ง ๊ฒ์ฆํด์ผ ํ ๊ฒ์ด๋ค.
์ฆ ์ด๋ ๊ฒ โํน์ ๊ฐ์ valueโ๋ฅผ ๊ฒ์ฆํด์ผํ ๋ (ํ์ํ ๋) ํด๋น value์ ํ ์คํธ ํด์ผ ํ๋ ๊ฐ์ ๋ฃ์ด์ฃผ๋ ๊ฒ์ด๋ค.
๐ key์ code
- key: ์ค์ ๋ก ์ ๋ ฅ๋ ํค
- code: ๋ฌผ๋ฆฌ์ ์ธ ํค ์์น
1
2
3
4
fireEvent.keyDown(screen.getByText("ํค๋ณด๋ ๋๋ฆ"), {
key: "Enter",
code: "Enter",
})
์ ์ดํด๊ฐ ์๊ฐ๋๋ฐ, ๋๊ฐ์ง ์ํฉ์ ์๊ฐํด๋ณด์.
a์ A๋ฅผ ์ ๋ ฅํ ๋
- Shift ์์ด โaโ๋ฅผ ๋๋ฅด๋ฉด: key: โaโ code: โKeyAโ
- Shift๋ฅผ ๋๋ฅด๊ณ โaโ๋ฅผ ๋๋ฅด๋ฉด: key: โAโ (๋๋ฌธ์ A) code: โKeyAโ (๋ฌผ๋ฆฌ์ ์ผ๋ก ๋์ผํ ํค)
๋ค๋ฅธ ๋ฐฐ์ด์ ํค๋ณด๋๋ฅผ ์ ๋ ฅํ ๋ ์ฐ๋ฆฌ๊ฐ ์ฃผ๋ก ์ฌ์ฉํ๋ ์ฟผํฐ ๋ฐฐ์ด๊ณผ ๋ค๋ฅธ ํค๋ณด๋์ ๊ฒฝ์ฐ ๊ฐ์ ์์น๋ฅผ ํด๋ฆญํ๋๋ผ๋ ์ ๋ ฅ๋๋ ๊ฐ์ ๋ค๋ฅผ ์ ์๋ค. (์ฃผ๋ก code๋ ์ฟผํฐ ๊ธฐ์ค์ผ๋ก ์ ํด์ง๋ค.)
โก๏ธuserEvent
userEvent๋ Testing Library์ ๋๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, ์ค์ ๋ธ๋ผ์ฐ์ ์์ ๋ฐ์ํ ์ ์๋ ์ฌ์ฉ์ ์ํธ์์ฉ์ ํ ์คํธํ๋ ๋๊ตฌ๋ค.
1
2
3
4
5
6
7
8
9
import userEvent from "@testing-library/user-event"
// ๊ฐ๋จํ ์ฌ์ฉ ์
it("click ์ด๋ฒคํธ ํ
์คํธ", async () => {
const user = userEvent.setup()
render(<MyComponent />)
await user.click(screen.getByRole("button", { name: "ํด๋ฆญํ์ธ์!" }))
})
userEvent๋ฅผ ์ฌ์ฉํ๊ธฐ ์ userEvent.setup()
์ ์ฌ์ฉํด ์ฌ์ฉ์ ์ํธ์์ฉ์ ์๋ฎฌ๋ ์ด์
ํ ์ ์๋ ํ๊ฒฝ์ ์ค์ ํ๊ณ , ์ ์ฒด์ ์ธ ์ฌ์ฉ๋ฒ์ fireEvent์ ๋์ผํ๊ฒ ์งํ๋๋ค.
โก๏ธuserEvent vs fireEvent
๊ทธ๋ผ ๋๊ฐ์ง์ ์ฐจ์ด์ ์ ๋ฌด์์ผ๊น?
fireEvent๋ ๋ธ๋ผ์ฐ์ ์ ์ ์์ค API์ธ dispatchEvent๋ฅผ ๊ฒฝ๋์ผ๋ก ๊ฐ์ธ ์ฌ์ฉ์๊ฐ ์ํ๋ ์ด๋ฒคํธ๋ฅผ ์ง์ ํธ๋ฆฌ๊ฑฐํ ์ ์๊ฒ ํ๋ค. ํ์ง๋ง ๋ธ๋ผ์ฐ์ ์์์ ์ด๋ฒคํธ๋ ์ฌ๋ฌ ์ด๋ฒคํธ๊ฐ ์ฐ์์ ์ผ๋ก ๋ฐ์ํ๊ธฐ๋ ํ๋ค.
์๋ฅผ๋ค์ด ์ฌ์ฉ์๊ฐ ํผ์ ์ด์ฉํ๊ณ ์ ํ๋ฉด input์ focusํ๊ณ , ๊ฐ์ ์ ๋ ฅํด ๋ณ๊ฒฝ๋๋ฉฐ, ๋์์ ํค๋ณด๋ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ์๊ฐํด๋ณผ ์ ์๋ค.
user-event๋ fireEvent์ฒ๋ผ ํ๋์ ์ด๋ฒคํธ๊ฐ ์๋๋ผ, ์ฌ์ฉ์์ ์ํธ์์ฉ์ ํ ์คํธ ํ ์ ์๊ฒ ํด์ฃผ๋ฉฐ, ๋ธ๋ผ์ฐ์ ์์ ์ผ์ด๋๋ ๊ฒ์ฒ๋ผ DOM์ ์กฐ์ํ ์ ์๋ค.
๋ฐ๋ผ์ ๊ณต์๋ฌธ์์์๋ user-event๋ฅผ ์ฌ์ฉํ๋๋ก ๊ถ์ฅํ๋ค. ๋ค๋ง ์์ง ์๋ฒฝํ๊ฒ ๋ชจ๋ ์ด๋ฒคํธ๊ฐ ๊ตฌํ๋ ๊ฒ์ ์๋์ด์, ์ด ๊ฒฝ์ฐ fireEvent๋ฅผ ์ฌ์ฉํด ๋ณด์์ ์ธ ์ธก๋ฉด์ ์๋ค.