React ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉ์ ์
์ฅ์ ๊ฐ๊น๊ฒ ํ
์คํธํ ์ ์๋ ๋๊ตฌ.
when, then์ ๊ฐ๋
์ด ์๋ค.
context('with two numbers', () => {
it('returns sum of two numbers', () => {
//when
const result = add(2)
//then
expect(result).toBe(2);
});
});
์๋ ReactDom..createRoot() ์์ผ๋ก ๋์ ๋ง๋ค์ด์ ๋ ๋ํด์คฌ๋ค๋ฉด
๊ทธ๋ฐ๊ฑฐ ์์ด ํธํ๊ฒ ํ
์คํ
ํ ์ ์๋ render๋ฅผ ์ ๊ณตํ๋ค.
// TextField.tsx
import { useRef } from 'react';
type TextFiledProps={
label:string
placeholder: string
text:string
setFilterText:(value:string) => void
}
export default function TextField({
label, placeholder, text, setFilterText,
}:TextFiledProps) {
const id = useRef(`input=${Math.random()}`);
const handleChange = (e:React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setFilterText(value);
};
return (
<div>
<label htmlFor="input2">
{label}
</label>
<input
id="input2"
type="text"
/>
</div>
);
}
//TextField.test.tsx
import { render, screen } from '@testing-library/react';
import TextField from '../src/components/TextField';
test('TextField', () => {
// given
const setFilterText = () => {
//
};
// when
render(
<TextField
label="Name"
placeholder=""
text=""
setFilterText={setFilterText}
/>,
);
// then ํ๋ฉด์ด ๋์์ผ ํจ
screen.getByLabelText('Name');
// ๋ ์ด๋ธ์ด Search์ธ ์ธํ์ด ์กํ๋ค.
});
label text๊ฐ Search์ธ ์ธํ์ ์ก๋๋ฐ,
label์ htmlFor์ input์ id๊ฐ ๊ฐ์์ผ ํ๋ค. ๊ทธ๋์ผ ๋์ด ์ฐ๊ฒฐ๋ Element๋ก ์ธ์ํ๋ค.
๊ทธ๋ ์ง ์์ผ๋ฉด getByLabelText๋ก ์ฐพ์ง ๋ชปํจ.
์์ ํ ์ผ์นํ์ง ์์๋ get์ผ๋ก ๊ฐ์ ธ์ค๊ณ ์ถ๋ค๋ฉด ์ ๊ท ํํ์์ ์ด์ฉํ๋ค.
// ex
screen.getByLabelText(/Sear/)
์๋์ฒ๋ผ getํด์จ element๋ฅผ ํ
์คํธ ํ ์๋ ์๋ค.
//TextField.text.tsx
const input = screen.getByLabelText('Name');
expect(input.value).toBe('Tester');
// or screengetByDisplayValue('Tester')
// TextField.tsx
return (
<div>
<label htmlFor="input2">
{label}
</label>
<input
id="input2"
type="text"
value="Tester"
/>
</div>
);
๋ณ์์ฒ๋ฆฌ ํด์ค ์ ์๋ค
//TextField.test.tsx
test('TextField', () => {
// given
const label = 'Name';
const text = 'Tester';
const setText = () => {
//
};
// when
render(
<TextField
label={label}
placeholder=""
text={text}
setText={setText}
/>,
);
// then
screen.getByLabelText('Name');
screen.getAllByDisplayValue(text); // value๊ฐ text์ธ ์ธํ์ ๊ฐ์ ธ์ด
});
//TextField.tsx
import { useRef } from 'react';
type TextFiledProps={
label:string
placeholder: string
text:string
setText:(value:string) => void
}
export default function TextField({
label, placeholder, text, setText,
}:TextFiledProps) {
const id = useRef(`input=${Math.random()}`);
const handleChange = (e:React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setFIlter(value);
};
return (
<div>
<label htmlFor={`${id}`}>
{label}
</label>
<input
id={`${id}`}
type="text"
value={text}
placeholder={placeholder}
onChange={handleChange}
/>
</div>
);
}
event
์ฌ๊ธฐ๊น์ง ์ํ์์ ๋ณด๋ฉด handleChange ๋ด์์ setFilter ๋ฑ ์๋ฌด๋ฐ ํจ์๋ฅผ ๋ฃ์ด๋
ํ
์คํธ๋ฅผ ์์ฑํ์ง ์์์ ์๋ฌด๋ฐ ์ด์์ด ์๋ค.
์ด๊ฒ๋ ์ก์์ฃผ๊ณ ์ถ๋ค.
// TextField.test.tsx
//ํ
์คํธ๋ฅผ ์ถ๊ฐํ๋ค.
const input = screen.getByLabelText('Name');
fireEvent.change(input, {
target: { value: 'New Name' },
});
// TextField.tsx
const handleChange = (e:React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setFilter(value); // ์ด ๋ถ๋ถ์ด ์๋ฌ.
// ReferenceError: setFilter is not defined
// ์ด๋ฒคํธ๊ฐ ๋๋ฌ์๋ input ์ e.target.value๊ฐ ๋ฐ๋์ด์ผ ํ๋๋ฐ
// ๊ทธ๋ ๊ฒ ํ setFilter ํจ์๊ฐ ์ ์๋์ง ์์์.
// ๊ทธ๋์ setText(value) ๋ก ๋ฐ๊ฟ์ฃผ๋ฉด ์๋ฌ๋ ํด๊ฒฐ๋๋ค.
// setText ํจ์๋ ๋ด๋ถ ๊ตฌํ์ด ๋์ด์์ง๋ ์์๋ ๋ง์ด๋ค.
// setText() ์ด๋ ๊ฒ value๋ฅผ ์ ๋ฌํ์ง ์์๋ ์๋ฌ๋ ์๋ค.
// ๋ฑ handleChange ํจ์ ์์ ์๋ฌ๊ฐ ์๋์ง ์๋์ง๋ง ํ๋ณ.
};
return (
<div>
<label htmlFor={`${id}`}>
{label}
</label>
<input
id={`${id}`}
type="text"
value={text}
placeholder={placeholder}
onChange={handleChange}
/>
</div>
)
setText๊ฐ ๋ถ๋ ธ๋์ง ํ์ธํ๊ณ ์ถ์ด
import { fireEvent, render, screen } from '@testing-library/react';
import TextField from '../src/components/TextField';
test('TextField', () => {
// given
const label = 'Name';
const text = 'Tester';
let called = false; // ์ถ๊ฐ
const setText = () => { // setText๊ฐ ์คํ๋๋ฉด ์ผ์ด๋ effect
called = true;
};
// when
render(
<TextField
label={label}
placeholder="name is harry"
text={text}
setText={setText}
/>,
);
screen.getByLabelText('Name');
screen.getAllByDisplayValue(text);
screen.getAllByPlaceholderText(/name/);
const input = screen.getByLabelText('Name');
fireEvent.change(input, {
target: { value: 'New Name' },
});
expect(called).toBeTruthy(); // ์ถ๊ฐ : called ๊ฐ true์ผ๋๋ง ํต๊ณผ
});
// TextField.tsx
export default function TextField({
label, placeholder, text, setText,
}:TextFiledProps) {
const id = useRef(`input=${Math.random()}`);
const handleChange = (e:React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
// setText(value);
// ์ฃผ์์ฒ๋ฆฌ๋ก ํจ์ ์คํ๋์ง ์์ ์๋ฌ๋จ.
};
return (
<div>
<label htmlFor={`${id}`}>
{label}
</label>
<input
id={`${id}`}
type="text"
value={text}
placeholder={placeholder}
onChange={handleChange}
/>
</div>
);
jest.fn ๋ชฉํน
let called = false;
const setText = jest.fn();
// ์ด๋ ๊ฒ๋ง ํด๋ ๋๋ค๋๋ฐ ์๋๋๊ฑธ?
//๋ฌด์จ์ผ์ด ์ผ์ด๋๋์ง ์ง์ ์ ์ํด๋ ๋จ.
const setText = jest.fn(()=>{
called = true
});
์ด๊ฒ๋ณด๋ค ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ํ๋ค.
setText๊ฐ ๋ถ๋ ธ๋์ง, ์ํ๋ ๊ฐ์ผ๋ก ๋ฐ๊พธ๊ฒ ํ๋์ง๊น์ง ํ
์คํธํ๊ณ ์ถ์ด
setText๊ฐ call๋๋์ง ๋ฐ๋ก ํ์ธํ ์ ์๋ค.
//TestField.test.tsx
import { fireEvent, render, screen } from '@testing-library/react';
import TextField from '../src/components/TextField';
test('TextField', () => {
// given
const label = 'Name';
const text = 'Tester';
const setText = jest.fn();
// when
render(
<TextField
label={label}
placeholder="name is harry"
text={text}
setText={setText}
/>,
);
//์ด input, fireEvent ๋ถ๋ถ์ ์ฃผ์ํ๋ฉด toBeCalledWith ๋ถ๋ถ์ด ํต๊ณผ๋์ง ์๋๋ค. fireEvent์ ๋ํด ์ ํํ ๋ ์์๋ณด์.
const input = screen.getByLabelText('Name');
fireEvent.change(input, {
target: { value: 'New Name' },
});
expect(setText).toBeCalled(); // ์คํ์ฌ๋ถ
expect(setText).toBeCalledWith('New Name') // New Name ๋์ค๋์ง
});
//TextField.tsx
.
.
.
const handleChange = (e:React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
// setText(value);
};
return (
<div>
<label htmlFor={`${id}`}>
{label}
.
.
.
context ํ์ฉ
import { fireEvent, render, screen } from '@testing-library/react';
import TextField from '../src/components/TextField';
const context = describe;
describe('TextField', () => {
// given
const label = 'Name';
const text = 'Tester';
const setText = jest.fn();
it('renders elements', () => {
// when
render(
<TextField
label={label}
placeholder="name is harry"
text={text}
setText={setText}
/>,
);
// then ํ๋ฉด์ด ๋์์ผ ํจ
screen.getByLabelText(label);
screen.getAllByDisplayValue(text);
screen.getAllByPlaceholderText(/name/);
});
// --------
// context: ์
๋ ฅํ์๋
context('when user enters name', () => {
it('calls "setText" handler', () => {
// given
render(
<TextField
label={label}
placeholder="name is harry"
text={text}
setText={setText}
/>,
);
// when
const input = screen.getByLabelText('Name');
fireEvent.change(input, {
target: { value: 'New Name' },
});
// then
expect(setText).toBeCalledWith('New Name');
});
});
});
์ค๋ณต๋๋ render ๋ถ๋ถ์ ๋นผ์ค๋ค
import { fireEvent, render, screen } from '@testing-library/react';
import TextField from '../src/components/TextField';
const context = describe;
describe('TextField', () => {
// given
const label = 'Name';
const text = 'Tester';
const setText = jest.fn();
function renderTextField() {
render(
<TextField
label={label}
placeholder="name is harry"
text={text}
setText={setText}
/>,
);
}
it('renders elements', () => {
// when
renderTextField();
// then
screen.getByLabelText(label);
screen.getAllByDisplayValue(text);
screen.getAllByPlaceholderText(/name/);
});
// --------
context('when user enters name', () => {
it('calls "setText" handler', () => {
// given
renderTextField();
// when
const input = screen.getByLabelText('Name');
fireEvent.change(input, {
target: { value: 'New Name' },
});
// then
expect(setText).toBeCalledWith('New Name');
});
});
});
mock clear
์ฌ๊ธฐ์ it('renders elements')์
context๋ฅผ ์ด์ฉํ ๋ถ๋ถ ๋ ๊ตฐ๋ฐ์์ ๊ฐ์ ๋ฐ๋ก
setText ๋ชฉ์ ์ฌ์ฉํ๋๋ฐ
์ด๋์๋ ์ฝ ํ๊ธฐ๋ง ํ๋ฉด ์ฝ ๋๊ฑฐ.. ๊ทธ๋ฌ๋ฉด ์๋ผ์
๋งค ํ
์คํธ๋ง๋ค ์ด๊ธฐํ ํด์ค์ผ ํ๋ค.
.
.
.
describe('TextField', () => {
// given
const label = 'Name';
const text = 'Tester';
const setText = jest.fn();
beforeEach(() => { //๋์ค์ ๋ ํ๋ฉด ๋จ.
setText.mockClear();
// jest.clearAllMocks();
});
function renderTextField() {
render(
<TextField
label={label}
placeholder="name is harry"
text={text}
setText={setText}
/>,
.
.
.
์๋์ฒ๋ผ ์ ๋ฆฌํ ์ ์๋ค.
context ๋ด๋ถ์์ ๋๋ฌ๋ด๊ณ ์ถ์ผ๋ฉด ๊ทธ๋ฅ ๋๋ฌ๋ด๋ ์ข๋ค.
import { fireEvent, render, screen } from '@testing-library/react';
import TextField from '../src/components/TextField';
const context = describe;
describe('TextField', () => {
// given
const label = 'Name';
const text = 'Tester';
const setText = jest.fn();
beforeEach(() => {
// setText.mockClear();
jest.clearAllMocks();
});
function renderTextField() {
render(
<TextField
label={label}
placeholder="name is harry"
text={text}
setText={setText}
/>,
);
}
function inputText(value:string) {
fireEvent.change(screen.getByLabelText('Name'), {
target: { value },
});
}
it('renders elements', () => {
// when
renderTextField();
// then ํ๋ฉด์ด ๋์์ผ ํจ
screen.getByLabelText(label);
screen.getAllByDisplayValue(text);
screen.getAllByPlaceholderText(/name/);
});
// --------
// context: ์
๋ ฅํ์๋
context('when user enters name', () => {
beforeEach(() => { //given์ด it ์์ ์๋๊ฒ ์ซ์ด์ ๋บ
// given
renderTextField();
});
it('calls "setText" handler', () => {
// when
inputText('New Name'); //renderTextField ์ฒ๋ผ ๋นผ์คฌ๋ค.
// then
expect(setText).toBeCalledWith('New Name');
});
});
});
๋ฐ๋ณต๋๋ ์ฝ๋๋ฅผ Extract Function
ํ๊ณ ,
fireEvent
๋ฑ์ ํตํด ์ธํฐ๋์
๋ง ๊ฒ์ฆํ๋ค.
์ธ๋ถ ์์กด์ฑ์ด ํฐ ์ฝ๋๋ฅผ ์์ฑํ๋ค๋ฉด ํด๋น ๋ถ๋ถ๋ง ๊ฐ์ง๋ก ๊ตฌํํ ์ ์๋ค
// App.test.tsx
import { render, screen } from '@testing-library/react';
import App from './App';
jest.mock('./hooks/useFetchRestaurants.ts', () => () => [
{
id: '1',
category: '์ค์',
name: '๋ฉ๊ฐ๋ฐ์ ',
menu: [
{ id: '1', name: '์ง์ฅ๋ฉด', price: 8000 },
{ id: '2', name: '์งฌ๋ฝ', price: 8000 },
{ id: '3', name: '์ฐจ๋์งฌ๋ฝ', price: 9000 },
{ id: '4', name: 'ํ์์ก', price: 14000 },
],
},
]);
test('App', () => {
render(<App />);
screen.getByText('๋ฉ๊ฐ๋ฐ์ ');
});
express ์๋ฒ๋ฅผ ์คํ์ํค์ง ์๊ณ mock์ ํ์ฉํด ํ
์คํธํ ์ ์๋ค.
์๋์ฒ๋ผ ๋ฐ๋ก ๋นผ์ ๋ชฉ๊ด๋ฆฌ๋ฅผ ํ ์ ์๋ค.
// fixtures/products.ts
const products = [
{
id: '1',
category: '์ค์',
name: '๋ฉ๊ฐ๋ฐ์ ',
menu: [
{ id: '1', name: '์ง์ฅ๋ฉด', price: 8000 },
{ id: '2', name: '์งฌ๋ฝ', price: 8000 },
{ id: '3', name: '์ฐจ๋์งฌ๋ฝ', price: 9000 },
{ id: '4', name: 'ํ์์ก', price: 14000 },
],
},
{
id: '2',
category: 'ํ์',
name: '๋ฉ๋ฆฌ๊น๋ฐฅ',
menu: [
{ id: '5', name: '๊น๋ฐฅ', price: 3500 },
{ id: '6', name: '์ฐธ์น๊น๋ฐฅ', price: 4500 },
{ id: '7', name: '์ ์ก๊น๋ฐฅ', price: 5000 },
{ id: '8', name: 'ํ์ ์ค๋ฆฌ๊น๋ฐฅ', price: 5500 },
],
},
{
id: '3',
category: '์ผ์',
name: 'ํน๋ฑ๊ณ ๋์นด๋ ',
menu: [
{ id: '9', name: '๊ธฐ๋ณธ์นด๋ ', price: 9000 },
{ id: '10', name: '๊ฐ๋ผ์๊ฒ์นด๋ ', price: 14000 },
{ id: '11', name: '์์์ง์นด๋ ', price: 13000 },
{ id: '12', name: '๋๊น์ค์นด๋ ', price: 14000 },
{ id: '13', name: '๋ญ๊ฐ์ด์ด์นด๋ ', price: 13000 },
],
},
];
export default products;
// fixtures/index.ts
import products from './products';
export default {
products,
};
//App.test.tsx
import { render, screen } from '@testing-library/react';
import fixtures from '../fixtures';
import App from './App';
jest.mock('./hooks/useFetchRestaurants.ts', () => () => fixtures.products);
test('App', () => {
render(<App />);
screen.getByText('๋ฉ๊ฐ๋ฐ์ ');
});
hooks Mock์ ์์ ๋ฐ๋ก ๋บ ์ ์๋ค
// App.test.tsx
import { render, screen } from '@testing-library/react';
import fixtures from '../fixtures';
import App from './App';
jest.mock('./hooks/useFetchRestaurants.ts');
// ํจ์๋ก ์ ๋ฌํ๋ ๋ชฉ์ ๋ฐ๋ก ๋บ.
test('App', () => {
render(<App />);
screen.getByText('๋ฉ๊ฐ๋ฐ์ ');
});
// hooks/__mocks__/useFetchRestaurants.ts
import fixtures from '../../../fixtures';
const useFetchProducts = jest.fn(() => fixtures.products);
export default useFetchProducts;
jest.fn์์ด () => fixtures.products ๋ง ์จ์ค๋ ๊ฐ๋ฅ.
๊ทธ๋ ์ง๋ง ์จ์ฃผ๋ฉด ๊ฐ์ง๋ผ๋ ์ฌ์ค์ด ๋ ๋ช
ํํ๊ณ
ํธ์ถ์ด ๋๋์ง ํ์ธํ๋ ๋ฑ์ test๋ฅผ ๋ ํ์ฉํ ์ ์๊ธฐ ๋๋ฌธ์ ๋ถ์ด๋ ํธ์ ์ถ์ฒ.
์ ์ ์ฑ์ด ์ปค์ง๋ฉด์ ํ๋์ฉ ๊ฐ์ง ๊ตฌํ์ผ๋ก ํ๊ธฐ ์ด๋ ค์ธ ๋๊ฐ ์๋ค.
์ด๋ด๋ MSW๋ฑ์ ๋์์ ๊ณ ๋ คํด๋ณผ ์ ์๋ค. ๋ค์ ์ฃผ์ ๋ก ๋์ด๊ฐ ๋ณด์.