'๋จ์ ํ ์คํธ'๋?
์ฑ์์ ํ ์คํธ ๊ฐ๋ฅํ ๊ฐ์ฅ ์์ ์ํํธ์จ์ด๋ฅผ ์คํํด ์์๋๋ก ๋์ํ๋์ง ํ์ธํ๋ ํ ์คํธ.
- ํ๋ก ํธ์์๋ ๊ฐ๋ น ๋จ์ผํจ์์ ๊ฒฐ๊ณผ๊ฐ, ๋จ์ผ ์ปดํฌ๋ํธ์ ์ํ๋ ํ์ ๋ฑ์ด ๋ ์ ์๋ค.
- '๋จ์ผ' : ์ปดํฌ๋ํธ๋ผ๋ฆฌ์ ์ํธ์์ฉ์ ๊ฒ์ฆํ๊ธฐ๋ณด๋ค๋ ๊ฐ๊ฐ์ ํ์๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฒ์ฆํ๋ค.
ํ๋ก ํธ(๋ฆฌ์กํธ)์์๋ ๋ฒํผ, ํ ์คํธ ์ธํ, ์บ๋ฌ์ , ์์ฝ๋์ธ ๋ฑ์ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฌ์ฉํ๋ ๊ฒฝ์ฐ๊ฐ ๋ค๋ฐ์ฌ์ธ๋ฐ ์ด๋ฐ ์ปดํฌ๋ํธ๋ค์ด ๋จ์ ํ ์คํธ์ ์์ฃผ ์ ํฉํ ์ปดํฌ๋ํธ์ด๋ค. (Atomic ์ปดํฌ๋ํธ๋ผ๊ณ ๋ถ๋ฅด๊ธฐ๋ ํ๋ค)
AAA ํจํด
ํ ์คํธ ์ฝ๋ ์์ฑ์๋ ์ฌ๋ฌ ํจํด์ด ์๋ค. AAA ํจํด์ด๋ Arrange-Act-Assert ํจํด์ ์ค์๋ง.
Arrange : ํ ์คํธ๋ฅผ ์ํ ํ๊ฒฝ ๋ง๋ค๊ธฐ (์ปดํฌ๋ํธ ๋ ๋๋ง ํ๋ ์ฝ๋ ๋ฑ)
- testing-library ์ฌ์ฉ
Act : ํ ์คํธํ ๋์ ๋ฐ์
Assert : ์ฌ๋ฐ๋ฅธ ๋์์ด ์คํ๋์๋์ง ๋๋ ๋ณ๊ฒฝ์ฌํญ ๊ฒ์ฆ - vitest ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
it('className prop์ผ๋ก ์ค์ ํ css class๊ฐ ์ ์ฉ๋๋ค.', async () => {
// Arrange
// - className์ ์ง๋ ์ปดํฌ๋ํธ ๋๋๋ง
await render(<TextField className={`my-class`} />);
// Act
// - ํด๋ฆญ์ด๋ ๋ฉ์๋ ํธ์ถ, prop ๋ณ๊ฒฝ ๋ฑ๋ฑ์ ๋ํ ์์
// Assert
// - ๋ ๋๋ง ํ DOM์ ํด๋น class๊ฐ ์กด์ฌํ๋์ง ๊ฒ์ฆ
expect(screen.getByPlaceholderText('ํ
์คํธ๋ฅผ ์
๋ ฅํด ์ฃผ์ธ์.')).toHaveClass(
'my-class',
);
});
ํ ์คํธ ํ๊ฒฝ๊ณผ ๋งค์ฒ
ํ ์คํธ ํ๋ ์์ํฌ : ํ์คํฌ ๋ฌ๋, ์ด์ค์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ํ๋ฌ๊ทธ์ธ ๋ฑ ํ ์คํธ ์คํ ๋ฐ ๊ฒ์ฆ์ ํ์ํ ํ๊ฒฝ์ ์ ๊ณตํ๋ ๋๊ตฌ (Vitest, Cypress ๋ฑ)
Vitest ์ค์
vite.config.js
ํ์ผ์ test
ํ๋์ ์ ์ํ๊ฑฐ๋, ์ฝ๋๋์ด ๋ง๋ค๋ฉด vitest.config.js
๋ผ๋ ํ์ผ์ ๋ง๋ค๊ธฐ๋ ํ๋ค.
//vite.config.js
export default defineConfig({
plugins: [react(), eslint({ exclude: ['/virtual:/**', 'node_modules/**'] })],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/utils/test/setupTests.js',
},
resolve: {
alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }],
},
});
JSDOM
ํ
์คํธ๋ Node.js ํ๊ฒฝ์์ ์คํ๋๋๋ฐ, ๋ธ๋ผ์ฐ์ ์์ด HTML์ ๊ตฌ์กฐ๋ฅผ ํ์ธํ ์ ์๊ฒ ๋๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ.
screen.debug()
ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ๋ ๋๋ง ๊ฒฐ๊ณผ๋ฌผ์ HTML ํ๊ทธ๋ค๋ก ๋ํ๋ด์ค๋ค.
<body>
<div>
<input
class="text-input my-class"
placeholder="ํ
์คํธ๋ฅผ ์
๋ ฅํด ์ฃผ์ธ์."
type="text"
value=""
/>
</div>
</body>
it()
ํจ์
ํ
์คํธ๊ฐ ํต๊ณผํ๊ธฐ ์ํ ์กฐ๊ฑด์ ๊ธฐ์ ํ์ฌ ๊ฒ์ฆ์ ์คํํ๋ค.
it ํจ์๋ ๊ฐ๋จํ test ํจ์์ alias. test ํจ์์์ ๊ธฐ๋ฅ์์ ์ฐจ์ด๋ ์๋ค.
describe()
๊ธฐ๋ณธ์ ์ผ๋ก itํจ์๋ก ์ ์๋ ํ
์คํธ ํ์ผ์ '๋ฃจํธ'์ด๊ธฐ ๋๋ฌธ์ ์ต์์ ์ปจํ
์คํธ์์ ํ
์คํธ๊ฐ ์คํ๋๋ค.
describe ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ it ํจ์๋ค์ ๊ทธ๋ฃน์ง์ ์ ์๋ค.
๋งค์ฒ Matcher
๊ธฐ๋ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ์ฆํ๊ธฐ ์ํด ์ฌ์ฉ๋๋ API ์งํฉ. (์์ expect().toHaveClass()
)
setup ๊ณผ teardown
ํ ์คํธ์ ์์น์ค '๋ชจ๋ ํ ์คํธ๋ ๋ ๋ฆฝ์ ์ผ๋ก ์คํ๋์ด์ผ ํ๋ค'๋ ์์น์ด ์๋ค.
๊ฐ๋ น ํ
์คํธ A์ B๊ฐ ์๋ค๊ณ ํ๊ณ , ํ
์คํธ A๋ฅผ ์คํํ๊ณ B๋ฅผ ์คํํ๋ฉด ์ฑ๊ณตํ์ง๋ง ์์๋ฅผ ๋ฐ๊ฟ B๋ฅผ ๋จผ์ ์คํํ๋ฉด ์คํจํ๋ ๊ฒฝ์ฐ๊ฐ ์๋ค. ์ด๋ฐ ๊ฒฝ์ฐ ์ด ํ
์คํธ ์ฝ๋๋ ์๋ชป ์์ฑ๋๊ฒ์ด๋ค. (์ ์ญ๋ณ์๋ฅผ ์ฌ์ฉํด ์กฐ๊ฑด ์ฒ๋ฆฌ๋ฅผ ํ์ ์ ์๋ค)
ํ
์คํธ๋ค์ ์์, ๋ค๋ฅธ ํ
์คํธ์ ์คํ ์ฌ๋ถ์ ์๊ด ์์ด ์์ ํ ๋
๋ฆฝ์ ์ผ๋ก ์ํ๋์ด์ผ ํ๋ค.
์ด๋ฐ ๋
๋ฆฝ์ฑ์ ๋ณด์ฅํ๊ธฐ ์ํด setup๊ณผ teardown์ด ์ฌ์ฉ๋๋ค.
setup : ํ
์คํธ๋ฅผ ์คํํ๊ธฐ ์ ์ํํด์ผ ํ๋ ์์
.
- beforeEach : ํ
์คํธ ํ์ผ ํน์ ์ค์ฝํ(describeํจ์) ๋ด์ ๋ชจ๋ ํ
์คํธ๊ฐ ์คํ๋๊ธฐ ์ ์ ํธ์ถ
- beforeAll : ํ์ผ ํน์ ์ค์ฝํ ๋ด์ ํ
์คํธ๊ฐ ์คํ๋๊ธฐ ์ ๋จ ํ๋ฒ๋ง ํธ์ถ
teardown : ํ
์คํธ๋ฅผ ์คํํ ๋ค ์ํํด์ผ ํ๋ ์์
.
- afterEach
- afterAll
React Testing Library์ ์ปดํฌ๋ํธ ํ ์คํธ
ํน์ ๊ฐ๋ฐ(๋ฆฌ์กํธ ์ธ ๋ทฐ,์ค๋ฒจํธ์์๋), ํ ์คํธ ํ๋ ์์ํฌ์ ์ข ์๋์ง๋ ์๋๋ค.
Testing Library์ ํต์ฌ ์ฒ ํ์ UI ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉ์๊ฐ ์ฌ์ฉํ๋ ๋ฐฉ์์ผ๋ก ํ ์คํธ ํ๋๊ฒ.
- ์ฌ์ฉ์๊ฐ ์ฌ์ฉํ๋ ๋ฐฉ์ : DOM ๋ ธ๋ ์กฐํ, ์ฌ์ฉ์์ ๋น์ทํ ๋ฐฉ์์ผ๋ก ์ด๋ฒคํธ ๋ฐ์.
- ํ ์คํธ ์ฝ๋ ์์ฑ ์์น์ค ์ธํฐํ์ด์ค๋ฅผ ๊ธฐ์ค์ผ๋ก ์์ฑํด์ผํ๋ค๋ ์์น๊ณผ ์ผ์น.
- getBy api์ค role, label-text, image-alt-text ์ฒ๋ผ ์ฌ์ฉ์ ๊ด์ ์ ๊ฐ๊น์ด api๋ฅผ ์ฌ์ฉํ๋ค.
userEvent
// TextField.spec.jsx
import render from '@/utils/test/render';
it('ํ
์คํธ๋ฅผ ์
๋ ฅํ๋ฉด onChange prop์ผ๋ก ๋ฑ๋กํ ํจ์๊ฐ ํธ์ถ๋๋ค', async () => {
const { user } = await render(<TextField />);
const textInput = screen.getByPlaceholderText('์ํ๋ช
์ ์
๋ ฅํด ์ฃผ์ธ์.');
await user.type(textInput, 'test');
...
});
// render.jsx
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
export default async component => {
const user = userEvent.setup();
return {
user,
...render(component),
};
};
userEvent.setup()
ํจ์๊ฐ ๋ฐํํ๋ user
๊ฐ์ฒด๋ ์ฌ์ฉ์์ ํ๋์ ์๋ฎฌ๋ ์ด์
ํ๋ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํฌ ์ ์๋ค.
spy ํจ์
vi.fn()
ํจ์๋ฅผ ํธ์ถํด ๋ง๋ค ์ ์๋ค.
์คํ์ด ํจ์๋ ํ
์คํธ ์ฝ๋์์ ํน์ ํจ์๊ฐ ํธ์ถ๋์๋์ง, ํจ์์ ์ธ์๋ก ์ด๋ค ๊ฒ์ด ๋์ด์๊ณ ์ด๋ค ๊ฐ์ ๋ฐํํ๋์ง ๋ฑ ๋ค์ํ ๊ฐ๋ค์ ์ ์ฅํ๊ณ ์๋ค.
์ปดํฌ๋ํธ ์์์ ์คํ๋๋ ํจ์๋ฅผ ๊ฒ์ฆํ๊ธฐ ์ํด ํ์๋ค.
it('ํ
์คํธ๋ฅผ ์
๋ ฅํ๋ฉด onChange prop์ผ๋ก ๋ฑ๋กํ ํจ์๊ฐ ํธ์ถ๋๋ค', async () => {
const spy = vi.fn();
const { user } = await render(<TextField onChange={spy} />);
const textInput = screen.getByPlaceholderText('ํ
์คํธ๋ฅผ ์
๋ ฅํด ์ฃผ์ธ์.');
await user.type(textInput, 'test');
expect(spy).toHaveBeenCalledWith('test');
});