React Testing Library
ํ์ต ํค์๋
React Testing Library
given - when - then ํจํด
Mocking
Test fixture
React Testing Library
Facebook์์ ๊ณต์์ ์ผ๋ก ์ฌ์ฉ์ ๊ถ์ฅ (โ React๋ก ์๋น์ค๋ฅผ ๋ง๋ ๋ค๋ฉด!)
Behavior Driven Test(ํ์ ์ฃผ๋ ํ ์คํธ) ๋ฐฉ๋ฒ๋ก ์ด ๋๋ ๋๋ฉด์ ํจ๊ป ์ฃผ๋ชฉ ๋ฐ๊ธฐ ์์ํ ํ ์คํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
React ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉ์ ์ ์ฅ์ ๊ฐ๊น๊ฒ ํ ์คํธํ ์ ์๋ ๋๊ตฌ
DOM์ ํ์ฉํด ์ฐ๋ฆฌ๊ฐ UI๋ฅผ ์ง์ ํ ์คํธํ ์ ์๋๋ก ํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
๐ ํ์ ์ฃผ๋ ํ ์คํธ(ํ์ ์ฃผ๋ ํ ์คํธ)๋ ๊ธฐ์กด์ ๊ดํ์ด๋ Implementation Driven Test(๊ตฌํ ์ฃผ๋ ํ ์คํธ)์ ๋จ์ ์ ๋ณด์ํ๊ธฐ ์ํ ๋ฐฉ๋ฒ๋ก ์ด๋ฉฐ ์ฌ์ฉ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ด์ฉํ๋ ๊ด์ ์์ ์ฌ์ฉ์์ ์ค์ ๊ฒฝํ ์์ฃผ๋ก ํ ์คํธ๋ฅผ ์์ฑํ๋ค.
<h2 class="title">์ ๋ชฉ</h2>
๊ตฌํ ์ฃผ๋ ํ ์คํธ ๊ด์
h2
๋ผ๋ ํ๊ทธ๊ฐ ์ฐ์๊ณ ,title
์ด๋ผ๋ ํด๋์ค๊ฐ ์ฌ์ฉ๋์๋์ง ์ฌ๋ถ๋ฅผ ํ ์คํธ
ํ์ ์ฃผ๋ ํ ์คํธ ๊ด์
๋ธ๋ผ์ฐ์ ํ๋ฉด์
์ ๋ชฉ
์ด๋ผ๋ ํ ์คํธ๊ฐ ๋ณด์ผ ๋ฟ. ๋ฐ๋ผ์, ์ฌ์ฉ์์๊ฒ ์ด๋ค ์ปจํ ์ธ ๊ฐ ํ์ฌ ๋ณด์ด๊ณ , ์ฌ์ฉ์๊ฐ ์ด๋ค ์ด๋ฒคํธ๋ฅผ ๋ฐ์์์ผฐ์ ๋, ๊ทธ์ ๋ฐ๋ผ ํ๋ฉด์ ๋ณํ๊ฐ ์ผ์ด๋๋์ง๋ฅผ ํ ์คํธ
โ๐ป React Testing Library ์ ๋ฆฌ
โ ์ฌ์ฉ์ ๊ด์ ์์ ๋ธ๋ผ์ฐ์ (ํ๋ฉด์) ์ผ์ด๋๋ ํ์์ ๋ํด ํ ์คํธ ํ ์ ์๊ฒ ํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
given - when - then ํจํด
[์ค๋น] - [์คํ] - [๊ฒ์ฆ] ๋จ๊ณ๋ก ํ ์คํธ ์ฝ๋๋ฅผ ๋๋ ์ ์์ฑํ๋ค.
Given [์ค๋น] : ์๋๋ฆฌ์ค ์งํ์ ํ์ํ ๊ฐ์ ์ค์ , ํ ์คํธ์ ์ํ๋ฅผ ์ค์
When [์คํ] : ์๋๋ฆฌ์ค ์งํ ํ์์กฐ๊ฑด ๋ช ์, ํ ์คํธํ๊ณ ์ ํ๋ ํ๋
Then [๊ฒ์ฆ] : ์๋๋ฆฌ์ค๋ฅผ ์๋ฃํ์ ๋ ๋ณด์ฅํด์ผ ํ๋ ๊ฒฐ๊ณผ๋ฅผ ๋ช ์, ์์๋๋ ๋ณํ ์ค๋ช
์์ ๋ฅผ ํตํด given - when - then ํจํด์ผ๋ก ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑ
// TextField.tsx
import React, { 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 = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setText(value);
};
return (
<div>
<label htmlFor={id.current}>
{label}
</label>
<input
id={id.current}
type="text"
placeholder={placeholder}
value={text}
onChange={handleChange}
/>
</div>
);
}
// TextField.test.tsx
import { fireEvent, render, screen } from '@testing-library/react';
import TextField from './TextField';
const context = describe;
describe('TextField', () => {
// 1. Given
// labe, text, setTest ํจ์,๋ ๋๋ง๋์ด์ผ ํ๋ ์ปดํฌ๋ํธ ํจ์ ์ค๋น
const label = '๊ฒ์';
const text = 'Search Text';
const setText = jest.fn(); // โ mocking
beforeEach(() => {
setText.mockClear();
// ๋๋ jest.clearAllMocks();
});
function renderTextField() {
render(
<TextField
label={label}
placeholder="Input your name"
text={text}
setText={setText}
/>
);
}
it('renders an input control', () => {
// 2. When
// TextField ์ปดํฌ๋ํธ๊ฐ ํ๋ฉด์ ๋ ๋๋ง ๋ ๋
renderTextField();
// 3.Then
// ํ๋ฉด Dom์์ label, value, placeholder ์์ฑ์ ๊ฐ์ง๊ณ ์๋ ์์๋ฅผ ์ฐพ๋๋ค.
screen.getByLabelText(label);
screen.getByDisplayValue(text);
screen.getByPlaceholderText('์๋น์ด๋ฆ');
});
});
Mocking
ํ ์คํธํ๊ณ ์ ํ๋ ์ฝ๋๊ฐ ์์กดํ๋ function์ด๋ class์ ๋ํด ๋ชจ์กฐํ์ ๋ง๋ค์ด '์ผ๋จ' ๋์๊ฐ๊ฒ ํ๋ ๊ฒ
โ ๋จ์ ํ ์คํธ๋ฅผ ์์ฑํ ๋, ํด๋น ์ฝ๋๊ฐ ์์กดํ๋ ๋ถ๋ถ์ ๊ฐ์ง(mock)๋ก ๋์ฒดํ๋ ๊ธฐ๋ฒ
๐ค ์ ๊ฐ์ง๋ก ๋์ฒดํ๋๊ฐ?
ํ ์คํธ ํ๊ณ ์ถ์ ๊ธฐ๋ฅ์ด ๋ค๋ฅธ ๊ธฐ๋ฅ๋ค๊ณผ ์ฎ์ด์์ ๊ฒฝ์ฐ(์์กด) ์ ํํ ํ ์คํธ๊ฐ ํ๋ค๊ธฐ ๋๋ฌธ์
๐ค ๊ทธ๋ผ Mocking์ด ํ์ํ ๊ฒฝ์ฐ๋?
์ธ๋ถ ์์กด์ฑ์ด ํฐ ์ฝ๋(API ์์ฒญ ๋ฑ)๋ฅผ ์์ฑํ ๊ฒฝ์ฐ, ํด๋น ๋ถ๋ถ๋ง ๊ฐ์ง๋ก ๊ตฌํ
๋งค๋ฒ ์๋ฒ๋ฅผ ๋์ฐ๊ธฐ ์ด๋ ต๊ณ , ์ค์๋ฒ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ด๋ ค์ด ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด์ ํ ์คํธ์์๋ง Mocking์ ํตํด ์๋ฒ๋ฅผ ๊ตฌํ ๋ณดํต ๋ฐฑ์๋์ ์ํตํ๋ ๋ถ๋ถ์ Mocking ํด์ ํ ์คํธํด์ผ ํ๋๋ฐ, ์ด ๋ถ๋ถ์ ํ๋์ฉ ๊ฐ์ง ๊ตฌํ์ผ๋ก ๋ฐ๊พธ๋ค ๋ณด๋ฉด ์ด๋ ค์ธ ๋๊ฐ ์๋ค. ์ด๋ด ๋ MSW ๋ฑ ๋ค๋ฅธ ๋์์ ๊ณ ๋ คํด์ผ ํ๋ค.
Think in React ์์ ๋ฅผ ํตํด API ์์ฒญ ์ฝ๋ ๋ชจํน
// App.tsx
import FilterableProductTable from './components/FilterableProductTable';
import useFetchProducts from './hooks/useFetchProducts';
export default function App() {
// useFetchProducts : Custom hook์ผ๋ก API ์๋ต๋ฐ์ ๊ฐ์ฒด products๋ก ์ ๋ฌ
const products = useFetchProducts();
return (
<>
<h1>์ํ</h1>
<FilterableProductTable products={products} />
</>
);
}
// App.test.tsx
import {render, screen} from '@testing-library/react';
import App from './App';
// const products === mocking์ ํตํด data ์ธ์
jest.mock('./hooks/useFetchProducts', () => () => [
{
category: 'Fruits', price: '$1', stocked: true, name: 'Apple',
},
]);
test('App', () => {
render(<App/>);
screen.getByText('Apple'); // ํ๋ฉด์์ Apple์ด๋ผ๋ ํ
์คํธ๋ฅผ ๊ฐ์ง๊ณ ์๋ ํ
์คํธ
});
Test fixture
๐ Fixture๋ '๊ณ ์ ๋์ด ์๋ ๋ฌผ์ฒด'๋ฅผ ์๋ฏธํ๋ค.
๐ค Junit ํ์์ ๋งํ๋ ํ
์คํธ ํฝ์ค์ฒ๋?
ํ ์คํธ ์คํ์ ์ํด ๋ฒ ์ด์ค๋ผ์ธ์ผ๋ก์ ์ฌ์ฉ๋๋ ๊ฐ์ฒด๋ค์ ๊ณ ์ ๋ ์ํ
โ๐ป ๋ด๊ฐ ์๊ฐํ๋ ํ
์คํธ ํฝ์ค์ฒ๋
mockingํ ๋ด์ฉ๋ค์ ํ๊ณณ์ ๋ชจ์๋๊ณ ๋ค๋ฅธ๊ณณ์์ ์ฌ์ฌ์ฉ ํ๊ธฐ ์ํ ์ฉ๋๋ก ๋ง๋๋ ํด๋
๐ ํด๋ ๊ตฌ์กฐ
์ง์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ
โโโ src
โ โโโ App.test.tsx
โ โโโ App.tsx
โโโ fixtures
โ โโโ index.ts
โ โโโ products.ts
// App.test.tsx
import {render, screen} from '@testing-library/react';
import App from './App';
import fixtures from '../fixtures';
jest.mock('./hooks/useFetchProducts', () => () => fixtures.products);
test('App', () => {
render(<App/>);
screen.getByText('Apple');
});
// fixtures/products.ts
const products = [
{
category: 'Fruits', price: '$1', stocked: true, name: 'Apple',
},
];
export default products;
// fixtures/index.ts
import products from './products';
export default {
products,
};
๋ณต์กํด์ง๋ฉด ์ด ๋ฐฉ๋ฒ์ ์ฌ์ฉ โ mocks
ํด๋๋ฅผ ๋ถ๋ฆฌ
mocks
ํด๋๋ฅผ ๋ถ๋ฆฌโ โโโ hooks
โ โ โโโ __mocks__
โ โ โ โโโ useFetchProducts.ts
โ โ โโโ useFetchProducts.ts
// App.test.tsx
import {render, screen} from '@testing-library/react';
import App from './App';
// jest.mock('./hooks/useFetchProducts', () => () => fixtures.products);
jest.mock('./hooks/useFetchProducts');
test('App', () => {
render(<App/>);
screen.getByText('Apple');
});
// hooks/__mocks__/useFetchProducts.ts
import fixtures from '../../../fixtures';
// const useFetchProducts = () => fixtures.products; // ์ด๋ ๊ฒ ์จ๋ ๋์ง๋ง
const useFetchProducts = jest.fn(() => fixtures.products); // ๋ชจํน์ ๋๋ฌ๋ด๊ธฐ ์ํด ๊ถ์ฅ๋๋ ๋ฐฉ๋ฒ
export default useFetchProducts;
๐ ์ฐธ๊ณ
Last updated