@testing-library๋ ์ฌ์ฉ์ ์ค์ฌ UI ์ปดํฌ๋ํธ ํ
์คํธ์ ๋์์ ์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์งํฉ์ด๋ค.
ํ ์คํธ๋ ์ปดํฌ๋ํธ์ ์์๋ฅผ ์ ํํ ํ, ์ด๋ฒคํธ API๋ user-event๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํค๊ณ ํ์ด์ง์์ ์ํธ์์ฉ์ ์๋ฎฌ๋ ์ด์ ํ๊ฑฐ๋ ์์์ ๋ํ assertion์ ๋ง๋ค ์ ์๋ค.
์ฟผ๋ฆฌํจ์์ suffix๋ก๋ role, labelText, placeholderText, text, displayView, altText, title, testid๋ฅผ ์ ํํ ์ ์๋ค
getByRole("button", {name: /submit/i})์ ํํ๋ฅผ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉํ๋ค.์ฟผ๋ฆฌ ํจ์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฒซ๋ฒ์งธ ์ธ์์ container DOM์ ์ ๋ฌํด์ผ ํ๋ค.
Testing library์์๋ screen์ ํตํด์ ์ฟผ๋ฆฌ ํจ์๋ฅผ ์ฌ์ฉํ ์๋ ์๋ค.
import {screen, getByLabelText} from '@testing-library/dom';
// with screen
const inputNode1 = screen.getByLabelText("Username");
// without screen
const inputNode2 = getByLabelText(document.querySelector("#app"), 'Username');
screen.getByText('Hello World', {exact: false}); // true์ธ ๊ฒฝ์ฐ full match, false์ธ ๊ฒฝ์ฐ substring match or ignore casescreen.getByText(/Hello World/i)screen.getByText((content, element) => content.startsWith("Hello"))import { logRoles } from '@testing-library/dom';
logRoles(document.createElement("nav"));
contextAPI๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, ํด๋น ์ค์ ์ ์ ์ฉํด์ผ ํ๋ค.
// test-utility.tsx
import {render, RenderOptions} from '@testing-library/react'
import {ThemeProvider} from 'my-ui-lib'
import {TranslationProvider} from 'my-i18n-lib'
import defaultStrings from 'i18n/en-x-default'
// ์์
const AllTheProviders = ({children}: {children: React.ReactNode}) => {
return (
<ThemeProvider theme="light">
<TranslationProvider messages={defaultStrings}>
{children}
</TranslationProvider>
</ThemeProvider>
)
}
// render ํจ์์ AlltheProviders๊ฐ wrapping ๋๋ค.
// test ํ์ผ์์ render๋ฅผ importํ ๋, @testing-library/react๊ฐ ์๋ test-utility.tsx๋ฅผ importํด์ ์ฌ์ฉํ๋ฉด ๋๋ค.
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, {wrapper: AlltheProviders, ...options})
ํ ์คํธ ํ ์ปดํฌ๋ํธ๋ฅผ ๋งค๊ฐ๋ณ์๋ก ์ ๋ฌํ๋ค.
๋ฐํ๋๋ { container }๋ logRoles๋ฅผ ํตํด ํ์๋ ์ ์๋ค.
const {container} = render(<Options />);
logRoles(container);
@testing-library/user-event๋ ๋ธ๋ผ์ฐ์ ์์ ์ํธ์์ฉ์ด ๋ฐ์ํ ๊ฒฝ์ฐ, ๋ฐ์ํ๋ ์ด๋ฒคํธ๋ฅผ ์ ์กํ์ฌ ์ฌ์ฉ์ ์ํธ์์ฉ์ ์๋ฎฌ๋ ์ด์
ํ๋ ๋ณด์กฐ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
fireEvents๋ ๋ธ๋ผ์ฐ์ ์ ์ ์์ค dispatchEvent API Wrapper๋ก, ๊ฐ๋ฐ์๊ฐ ๋ชจ๋ ์์์์ ๋ชจ๋ ์ด๋ฒคํธ๋ฅผ ํธ๋ฆฌ๊ฑฐํ ์ ์๋ค. ๋ฌธ์ ๋ ๋ธ๋ผ์ฐ์ ๊ฐ ์ผ๋ฐ์ ์ผ๋ก ํ๋์ ์ํธ์์ฉ์ ๋ํด ํ๋ ์ด์์ ์ด๋ฒคํธ๋ฅผ ํธ๋ฆฌ๊ฑฐํ๋ค๋ ๊ฒ์ด๋ค.
user-event๋ ๊ตฌ์ฒด์ ์ธ ์ฌ์ฉ์์ ์ํธ์์ฉ์ ๋ฐ์์ํฌ ์ ์๋ค. ๊ทธ๋์ user-event๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข๋ค.
์์ง ๊ตฌํ๋์ง ์์ ์ด๋ฒคํธ๋ค์ด ์๋๋ฐ ์ด๋ฌํ ๊ฒฝ์ฐ๋ fireEvent๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
import userEvent from '@testing-library/user-event';
it("test", async () => {
// render ํ๊ธฐ ์ ์ ํธ์ถ
const user = userEvent.setup();
render(<MyComponent />);
const element = screen.getByRole("button", {name: /click/i});
// user event๋ Promise๋ฅผ ๋ฐํํ๋ค
await user.click(element);
})
user.Type ํจ์๋ ์
๋ ฅ ๊ฐ๋ฅํ ์์์ ์
๋ ฅํ ์ ์๋ค.
user.clear ํจ์๋ ํธ์ง ๊ฐ๋ฅํ ์์๋ฅผ ์ฝ๊ฒ ์ง์ธ ์ ์๋ค.
it("test", () => {
const user = userEvent.setup();
...
const element = screen.getByRole("textbox")
await user.clear(element);
await user.type(element, 'new Text!');
})
user.click ํจ์๋ element๋ฅผ ํด๋ฆญํ ์ ์๋ค
์ถ๊ฐ ํจ์๋ ์ฌ๊ธฐ์ ํ์ธํ๋ฉด ๋๋ค.
jest-dom์ Jest์ ๋ํ ์ฌ์ฉ์ ์ ์ DOM ์์ ๋งค์ฒ๋ฅผ ์ ๊ณตํ๋ Testing Library ์ค ํ๋์ด๋ค.
์ฌ์ฉํ๋ ๊ณณ์์ import '@testing-library/jest-dom'๋ฅผ ํ๋ค.
test(์น ํ์ด์ง์ ๋์ ๋ณด์ด๋ ์์๋ค์ ํ ์คํธ), it, description, expect ๋ฑ์ ํจ์๋ฅผ ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด ์ฌ์ฉํ ์ ์๋ค
eslint-plugin-testing-library, eslint-plugin-jest-dom ๋ ์ฌ์ฉ์๊ฐ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ๋ฐ๋ฅด๊ณ ์ผ๋ฐ์ ์ธ ์ค์๋ฅผ ์์ธกํ๋๋ฐ ๋์์ด ๋๋ ESLint ํ๋ฌ๊ทธ์ธ
์ฐธ๊ณ : eslint-plugin-testing-library, eslint-plugin-jest-dom
Vite๋ก ๊ตฌ๋๋๋ ํ ์คํธ๋ฌ๋
// vite.config.ts
...
export defineConfig({
...
test: {
globals: true,
environment: "jsdom",
setupFiles: "./src/setupTests.js",
// you might want to disable the `css: true` line, since we don't have
// tests that rely on CSS -- and parsing CSS is slow.
// I'm leaving it in here becasue often people want to parse CSS in tests.
css: true,
},
...
})
ํ ์คํธ์์ API๋ฅผ ๋ชจํนํ๊ธฐ ์ํด์ ์ฌ์ฉํ๋ค.
// src/mocks/handlers.js
import { http, HttpResponse } from 'msw'
export const handlers = [
// Intercept "GET https://example.com/user" requests...
http.get('https://example.com/user', () => {
// ...and respond to them using this JSON response.
return HttpResponse.json({
id: 'c7b3d8e0-5e0b-4b0f-8b3a-3b9f4b3d3b3d',
firstName: 'John',
lastName: 'Maverick',
})
}),
]
// setupTests.js
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
const server = setupServer(...handlers)
beforeAll(() => {
// Start the interception.
server.listen()
})
afterEach(() => {
// Remove any handlers you may have added
// in individual tests (runtime handlers).
server.resetHandlers()
})
afterAll(() => {
// Disable request interception and clean up.
server.close()
})
ํ
์คํธ ํ ๋, ์ค์ฒฉ์ ์ง์ํ์
Testing Library
Vitest
Mock Service Worker