React Testing Library
React Testing Library
์ปดํฌ๋ํธ๋ฅผ ๋ด๋ถ ๊ตฌํ์ฌํญ์ ์์กดํ์ง ์๊ณ ์ฌ์ฉ์ ๊ด์ ์์ ํ ์คํธ๋ฅผ ์์ฑํ ์ ์๊ฒ ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
npm i -D @testing-library/react
์ฅ์
์ปดํฌ๋ํธ ๋ด๋ถ์์ ์ด๋ค ์ฝ๋๋ค์ ๊ฐ์ง๊ณ ์๋์ง ์ด๋ค CSS ํด๋์ค๋ฅผ ์ฌ์ฉํ๋์ง ๋ด๋ถ ๊ตฌํ์ฌํญ์ ์์กดํ์ง ์๊ณ ํ๋ฉด์์ UI๊ฐ ์ด๋ค ํ ์คํธ๋ฅผ ๊ฐ์ง๊ณ ์๊ณ ๋ณด์ด๋์ง ์๋ณด์ด๋์ง ๋ณด๋ค ์ธ๋ถ์ ์ธ ์ฌ์ฉ์ ๊ด์ ์์ ํ ์คํธ๋ฅผ ์์ฑํ ์ ์๊ฒ ๋์์ค๋ค.
API
Render
document.body ํ์๋ก ์ปดํฌ๋ํธ๋ฅผ ๋ ๋์ํจ๋ค.
import {render, screen} from '@testing-library/react'
describe('Component', () => {
it('render correctly', () => {
render(<Component />);
// ๊น์๋ฌด๊ฐ๋ฅผ ํฌํจํ๋ ํ
์คํธ๊ฐ UI ์์ ํ์๋๋์ง ๊ฒ์ฆ
expect(screen.getByText(/๊น์๋ฌด๊ฐ/)).toBeInTheDocument();
})
})
waitFor
๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌ๋๋ ์ํฉ์ ๊ธฐ๋ค๋ฆด ๋ ์ฌ์ฉ๋๋ค.
์ฆ, ์ฌ์ฉ์์ ๊ด์ ์์ ํ๋ฉด์ด ์ ๋๋ก ๋ ๋๋ง๋์๋์ง ํ์ธํ๊ธฐ ์ํด ํน์ ์์๊ฐ ํ๋ฉด์ ๋ณด์ฌ์ง ๋๊น์ง ๊ธฐ๋ค๋ฆฐ๋ค
๊ธฐ๋ณธ์ ์ผ๋ก ์ต๋ 5์ด๊น์ง ๋น๋๊ธฐ ์์
์ ๊ธฐ๋ค๋ฆฌ๊ณ options
๋งค๊ฐ๋ณ์๋ฅผ ์ฌ์ฉํ์ฌ ์ด ์๊ฐ์ ์กฐ์ ํ ์ ์๋ค.
await waitFor(() => ..., { timeout: 8000 });
waitFor() โ ์ผ๋ฐ์ ์ผ๋ก ์์๊ฐ ํ๋ฉด์ ๋ํ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ๋ ๋ฐ ์ฌ์ฉ๋ฉ
waitForElementToBeRemoved() โ ์์๊ฐ ํ๋ฉด์์ ์ฌ๋ผ์ง ๋ ๊น์ง ๊ธฐ๋ค๋ฆฌ๋ ๋ฐ ์ฌ์ฉ๋จ
import {render, screen, waitFor, waitForElementToBeRemoved} from '@testing-library/react';
const context = describe;
describe('Component', () => {
it('render correctly', async () => {
render(<Component />);
// ๋ก๋ฉ ํ
์คํธ๊ฐ ์ฌ๋ผ์ง ๋ ๊น์ง ๊ธฐ๋ค๋ฆผ
await waitForElementToBeRemoved(screen.queryByText('Loading...'));
// ๊น์๋ฌด๊ฐ๋ฅผ ํฌํจํ๋ ํ
์คํธ๊ฐ UI ์์ ํ์๋ ๋๊น์ง ๊ธฐ๋ค๋ฆผ
await wailtFor(() => expect(screen.getByText(/๊น์๋ฌด๊ฐ/)).toBeInTheDocument());
// findBy๋ก ์ฌ์ฉํ๋ฉด waitFor ์์ด๋ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.
await screen.findByText(/๊น์๋ฌด๊ฐ/)
})
})
Queries
ํ
์คํ
๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ ๊ณตํ๋ screen
๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋ฉด, ๋ ๋๋ง๋ dom ๋ด๋ถ์์ ํน์ ์์๋ฅผ ์ฝ๊ฒ ์ฐพ์ ์ ์๋ค.
screen
๊ฐ์ฒด๋ getBy
, queryBy
, getAllBy
, queryAllBy
, findBy
, findAllBy
๋ฑ์ ํจ์๋ฅผ ์ ๊ณตํ์ฌ ํ
์คํธ, ์ญํ ๋ฑ์ ๊ธฐ์ค์ผ๋ก ์์๋ฅผ ์ ํํ ์ ์๋ค.
getBy โ ์ฐพ๋ ์์๊ฐ ์๋ ๊ฒฝ์ฐ ์๋ฌ๋ฅผ ๋์ง๋ค.
findBy โ
getBy
๋น๋๊ธฐ ๋ฒ์ queryBy โ ์ฐพ๋ ์์๊ฐ ์๋ ๊ฒฝ์ฐ null์ ๋ฐํํ๋ค.
์์ฃผ์ฐ๋ ์ฟผ๋ฆฌ
getByText โ ์ง์ ํ ํ ์คํธ๋ฅผ ํฌํจํ๋ ์์ ์ ํ
getByRole โ ์ง์ ํ ์ญํ (role)์ ๊ฐ์ง ์์ ์ ํ
getByLabelText โ ์ง์ ํ
aria-label
๊ฐ์ง ์์ ์ ํgetByDisplayValue โ ์ง์ ํ ๊ฐ์ ๊ฐ์ง ์์ ์ ํ (input)
User Event
ํ ์คํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ ๊ณตํ๋ ์ด๋ฒคํธ ์ฒ๋ฆฌ API
์ค์ ์ฌ์ฉ์๋ค์ ๋์์ ์๋ฎฌ๋ ์ด์ ํ์ฌ ํ ์คํธ๋ฅผ ํ ์ ์๋๋ก ๋์์ค๋ค.
fireEvent
์ด๋ฒคํธ๋ฅผ ์ง์ ๋ฐ์์ํค๋ ํจ์
์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํค๋ ์ฝ๋๋ฅผ ์ง์ ์์ฑํ๋ค.
import { render, screen, fireEvent } from '@testing-library/react';
const context = describe;
describe('Input Component', () => {
const text = 'woong';
const setText = jest.fn();
afterEach(() =>{
setText.mockClear();
})
function renderTextField() {
rneder(
<TextField
label="Name"
placeholder="Input your name"
text={text}
setText={setText}
/>
)
}
context('when user types text', () => {
it('calls the change handler', () => {
renderTextField();
fireEvent.change(screen.getByLabelText('Name'), {
target: {
value: 'New Name',
},
});
expect(setText).toBeCalledWith('New Name');
})
})
})
userEvent
์ค์ ์ฌ์ฉ์๊ฐ ์ํํ๋ ๊ฒ๊ณผ ์ ์ฌํ ๋ฐฉ์์ผ๋ก ์ด๋ฒคํธ๋ฅผ ๋ฐ์ ์ํค๋ ํจ์
import userEvent from '@testing-library/user-event';
์ฌ์ฉ์๊ฐ ์ง์ ์ธํฐ๋ ์ ํ๋ฏ์ด ํจ๊ณผ๋ฅผ ์ค๋ค.
๋ณด๋ค ์ง๊ด์ ์ด๊ณ ๋ณต์กํ ๋์๋ ๊ฐํธํ๊ฒ ์์ฑํ ์ ์๋ค.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
const context = describe;
describe('Input Component', () => {
const text = 'woong';
const setText = jest.fn();
afterEach(() =>{
setText.mockClear();
})
function renderTextField() {
rneder(
<TextField
label="Name"
placeholder="Input your name"
text={text}
setText={setText}
/>
)
}
context('when user types text', () => {
it('calls the change handler', () => {
renderTextField();
userEvent.type(screen.getByLabelText('Name'), 'New Name');
expect(setText).toBeCalledWith('New Name');
})
})
})
Mocking
์ค์ ์ฝ๋์์ ์ฐ์ด๋ ์ธ๋ถ ์์กด์ฑ ๋ชจ๋์ ๋์ฒดํ ๋ ์ฐ์ด๋ ๊ฒ์ผ๋ก ํ ์คํธ์ ๊ฒฉ๋ฆฌ์ฑ(isolation)์ ํ๋ณดํ ์ ์๋ค.
"ํ ์คํธ๋ฅผ ๋ ๋ฆฝ์ํค๊ธฐ ์ํด ์์กด์ฑ์ ๊ฐ๋ฐ์๊ฐ ์ปจํธ๋กคํ๊ณ ๊ฒ์ฌํ ์ ์๋ ์ค๋ธ์ ํธ๋ก ๋ณํํ๋ ํ ํฌ๋"
Mock
๊ตฌํ์ฌํญ์ด ์๊ณ ๋ด๊ฐ ์ํ๋ ๋ถ๋ถ๋ง ๊ฐ์ง๋ก ํ๋ด๋ด๋ ์ฉ๋๋ก ์ฌ์ฉ
jest.fn()
ํ ์คํธ ์ฝ๋์์ ํจ์๋ฅผ ๋ชฉ(mock) ํจ์๋ก ๋์ฒดํ ๋ ์ฌ์ฉ
์ค์ ํจ์์ฒ๋ผ ๋์ํ๋ฉฐ, ํธ์ถ๋ ๋ด์ฉ์ ๊ธฐ๋กํ๊ณ ๋ฐํ๊ฐ์ ์ง์ ํ ์ ์๋ค.
//#1. ์ ์ธ๊ณผ ๋์์ ๋ฐํ๊ฐ ์ง์
const mockFn = jest.fn(() => 'mock');
const mockFn = jest.fn();
//#2. ๊ฐ ํ
์คํธ๋ง๋ค ๋ค๋ฅด๊ฒ ๋ฐํ๊ฐ์ ์ง์ ํด์ฃผ๊ณ ์ถ์๋ ์ฌ์ฉ
mockFn.mockImplementation(() => 'mock');
jest.mock()
ํ ์คํธ ์ฝ๋์์ ๋ชจ๋์ ๋ชฉํน(mocking)ํ๊ณ ๊ทธ ์์ ํจ์์ ๊ฐ์ฒด๋ฅผ ๋์ฒดํ ๋ ์ฌ์ฉ
๋ชจ๋์ด exportsํ๋ ๋ชจ๋ ๊ฒ๋ค์ Mockingํ๋ค.
import UserService from '../user_service';
import UserClient from '../user_client';
jest.mock('../user_client', () => jest.fn().mockImplementation(() => ({
login: jest.fn(async (id: string, password: string) => true),
})));
const context = describe;
describe('user service', () => {
let userService: UserService;
let client: UserClient;
beforeEach(() => {
client = new UserClient();
userService = new UserService(client);
});
context('when a successful login', () => {
it('isLogedIn should be true', async () => {
await userService.login('tv', 'dd');
expect(userService.isLogedIn).toBeTruthy();
expect(client.login).toHaveBeenCalled();
expect(client.login).toHaveBeenCalledWith('tv', 'dd');
});
});
});
jest.mocked() ํ์ฉ ver
๋ชฉํนํ ๋ชจ๋์ ํน์ ๋ถ๋ถ๋ง ๋ชฉํนํ ๋ ์ฌ์ฉ
import UserService from '../user_service';
import UserClient from '../user_client';
jest.mock('../user_client');
const context = describe;
describe('user service', () => {
const login = jest.fn(async (id: string, password: string) => true);
let userService: UserService;
jest.mocked(UserClient).mockImplementation(() => ({login}));
beforeEach(() => {
userService = new UserService(new UserClient());
});
context('when a successful login', () => {
it('isLogedIn should be true', async () => {
await userService.login('tv', 'dd');
expect(userService.isLogedIn).toBeTruthy();
expect(login).toHaveBeenCalled();
expect(login).toHaveBeenCalledWith('tv', 'dd');
});
});
});
Mock Clear
๊ฐ ํ ์คํธ๋ ๋ ๋ฆฝ์ ์ผ๋ก ์คํ๋๊ณ ๋ค๋ฅธ ํ ์คํธ์ ์ํฅ์ ๋ผ์น์ง ์์์ผ ํ๋ค.
{
clearMocks: true,
resetMocks: true,
restoreMocks: true,
resetModules: true // ์ํฉ์ ๋ฐ๋ผ ๋ค๋ฆ
๋๋ค.
}
jest.clearAllMocks()
๋ชจ๋ ๋ด์ ๋ชจ๋ mock ํจ์์ ํธ์ถ ๋ด์ญ๊ณผ ํธ์ถ ํ์๋ฅผ ์ด๊ธฐํ
jest.resetAllMocks()
๋ชจ๋ ๋ด์ ๋ชจ๋ mock ํจ์์ ํธ์ถ ๋ด์ญ, ํธ์ถ ํ์ + ๊ตฌํ์ ์ด๊ธฐํ
jest.restoreAllMock()
mock ํจ์๋ฅผ ์ค์ ๊ตฌํ์ผ๋ก ๋ณต์
jest.resetModules()
์์ ํ ๋ถ๋ฆฌ๋ ํ๊ฒฝ์์ ์คํ๋๋๋ก ๋ชจ๋ ๋ชจ๋์ ์ด๊ธฐํ
Stub
๊ธฐ์กด์ ์ฐ์ด๋ ์ฐ ์ธํฐํ์ด์ค๋ฅผ ๋ค ์ถฉ์กฑํ๋ ์ค์ ๋ก ๊ตฌํ๋ ์ฝ๋์ด๋ฉฐ ๋์ฒด ๊ฐ๋ฅํ ๊ฒ
// ์ฐ ์ธํฐํ์ด์ค
export default class ProductClient {
async fetchItems(): Promise<Item[]> {
return fetch('http://example.com/login/id+password').then(async response =>
response.json(),
);
}
}
// Stub
export default class StubProductClient {
async fetchItems(): Promise<Item[]> {
return [
{key: '๐ช๐ป', available: true},
{key: '๐', available: false},
];
}
}
// Test Code
import ProductService from '../product_service';
import StubProductClient from './stub_product_service';
describe('Product Service', () => {
let service: ProductService;
beforeEach(() => { // ์ฐ ์ธํฐํ์ด์ค ๋์ Stub๋ฅผ ์ฃผ์
ํด์ ์ฌ
service = new ProductService(new StubProductClient());
});
it('should filter out only available items', async () => {
const items = await service.fetchAvailableItems();
expect(items.length).toBe(1);
expect(items).toEqual([{
key: '๐ช๐ป',
available: true,
}]);
});
});
Test fixture
ํ ์คํธ๋ฅผ ์ํํ๋๋ฐ ํ์ํ ํ๊ฒฝ์ ์ค์ ํ๋ ์ฝ๋ ๋ธ๋ก
์ฆ, ํ ์คํธ์์ ํน์ ๊ฐ์ฒด, ๋ชจ๋, ํจ์ ๋ฑ์ ์ฌ์ฌ์ฉํ๊ธฐ ์ํด ํ์ํ ๋ฐ์ดํฐ๋ ์ค์ ๋ฑ์ ์ ์ํ๋ ์ฝ๋์ด๋ค.
ํ
์คํธ๋ ์ผ๊ด๋ ๊ฒฐ๊ณผ๋ฅผ ์ป๊ธฐ ์ํด ๋งค๋ฒ ๊ฐ์ ํ๊ฒฝ์์ ์คํ๋์ด์ผ ํ๋ฉฐ, ์ด๋ฅผ ์ํด Test fxitures
๋ฅผ ์ฌ์ฉํ์ฌ ํ๊ฒฝ์ ์ค์ ํด์ผ ํ๋ค.
Last updated