vite๋ฅผ ์ฌ์ฉํ๋ฉด์ jest๋ฅผ ์ฌ์ฉํ๋ ค๊ณ ํ์ผ๋
msw์์ ์๋ฌ๊ฐ ๋ฐ์ํด์ vitest๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ๊ฒฐ์ ํ์ต๋๋ค.
ํจํค์ง ์ค์น
์ฐ์ ํ์ํ ํจํค์ง๋ค์ ๋ชจ๋ ์ค์นํด์ฃผ์ธ์
์ ๊ฐ์ ๊ฒฝ์ฐ๋ ์๋์ฒ๋ผ ์ค์นํ๋๋ฐ ํน์ ์ ๊ฐ ๋น ํธ๋ฆฐ๊ฒ ์์ ์๋ ์์ด์
๋น ์ง ๋ถ๋ถ์ ํ์ธํด์ ์ถ๊ฐํด์ฃผ์ธ์
yarn add --dev vitest @testing-library/jest-dom @testing-library/react @testing-library/user-event msw jsdom
MSW ์ค์
MSW๋ฅผ ์ฌ์ฉํ์๋ ค๋ ๋ถ๋ค์ ์ฐ์ ๊ณต์ ๋ฌธ์๋ฅผ ํ์ธํด์ฃผ์ธ์
์ ๋ ์๋น์ค์์ปค๋ก ๋ฐ์ดํฐ ๋ชจํนํ๋ ๊ฒ์ ์ฌ์ฉํ์ง ์๊ณ
ํ
์คํธ๋ฅผ ์ํด์ msw๋ฅผ ์ฌ์ฉํ๋ ค๊ณ ํฉ๋๋ค
api ๊ฐ๋ฐ์ด ์๋ฃ๋์ง ์์์ ํ๋ก ํธ ๊ฐ๋ฐ ์ค์ mock๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ์๋ ค๋ ๋ถ๋ค์ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์
MSW Browser
์ ์ฒ๋ผ ํ
์คํธ์์๋ง(Node ํ๊ฒฝ) ์ฌ์ฉํ์ค ๋ถ๋ค์ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์
MSW Node
์ ์ฒด์ ์ผ๋ก ์นด์นด์ค์ํฐ ๊ธฐ์ ๋ธ๋ก๊ทธ:MSW๋ฅผ ์ฐธ๊ณ ํ ํ
์๋ฒ ์
์
๋ถ๋ถ์ ๊ณต์๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด์ ์์ฑํ์ต๋๋ค.
MSW setup-server
๋ฃจํธ ํด๋์์ vitest.setup.ts๋ฅผ ๋ง๋ ๋ค์ ํด๋น ์ค์ ์ ์ถ๊ฐ ํด์ฃผ์ธ์
ํ ์คํธ ์์ ์ , MSW๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํจ์ ๋๋ค
//vitest.setup.ts
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from './src/mocks/server';
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' });
});
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
๋ฃจํธ ํด๋์์ vitest.config.ts๋ฅผ ๋ง๋ ๋ค์ ํด๋น ์ค์ ์ ์ถ๊ฐ ํด์ฃผ์ธ์
//vitest.config.ts
import { mergeConfig } from 'vite';
import { defineConfig } from 'vitest/config';
import viteConfig from './vite.config.ts';
export default mergeConfig(
viteConfig,
defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
},
}),
);
ํ ์คํธ์ฝ๋ ์์ฑํ๊ธฐ
MSW ์ฌ์ฉ ์ํ๊ณ ํ ์คํธ
ํด๋น ํ์ด์ง๋ฅผ ํ ์คํธ ํ๋ ์ฝ๋๋ฅผ ์์ฑํ ์์ ์ ๋๋ค
ํ ์คํธ ํ ๋ถ๋ถ์ ํฌ๊ฒ 4๊ฐ์ง์ ๋๋ค.
- ํค์๋๊ฐ 1๋ฒ ํด๋ฆญ๋๋ฉด ํค์๋ ์กํฐ๋ธ
- ํค์๋๊ฐ 2๋ฒ ํด๋ฆญ๋๋ฉด ํค์ํธ ์ ํ ์ทจ์
- ํค์๋๋ ์ต๋ 3๊ฐ๊น์ง ํด๋ฆญ ๊ฐ๋ฅ
- ํ๋ฆฌ ๋ง๋ค๊ธฐ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ ํ๋ ํค์๋๋ค์ด ์ฟผ๋ฆฌ ์คํธ๋ง์ ๋ด๊ฒจ์ ธ์ ๋ค๋น๊ฒ์ดํ
import Record from '@components/record/Record';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { withRouter } from '@tests/withRouter';
import { Route, useLocation } from 'react-router-dom';
import '@testing-library/jest-dom';
describe('Record', () => {
//1. ํค์๋๊ฐ 1๋ฒ ํด๋ฆญ๋๋ฉด ํค์๋ ์กํฐ๋ธ
it('activate a tag when click once', async () => {
//์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋ง ํ ๋ค์
render(withRouter(<Route path='/' element={<Record />} />));
//tag ํ๋๋ฅผ ์ ํ
const tag = document.querySelector('.tag')!;
//ํด๋ฆญ์ด๋ฒคํธ๋ฅผ ๋ฐ๋ํ ๋ค์
await userEvent.click(tag);
//์ ํ๋ ํ๊ทธ์ active ํด๋์ค๊ฐ ์ถ๊ฐ๋๋์ง ํ์ธ
expect(tag.classList.contains('active')).toBe(true);
});
//2. ํค์๋๊ฐ 2๋ฒ ํด๋ฆญ๋๋ฉด ํค์ํธ ์ ํ ์ทจ์
it('inactive a tag when click twice', async () => {
render(withRouter(<Route path='/' element={<Record />} />));
const tag = document.querySelector('.tag')!;
//ํด๋ฆญ์ด๋ฒคํธ๋ฅผ ๋ ๋ฒ ๋ฐ๋ํ ๋ค์
await userEvent.click(tag);
await userEvent.click(tag);
//์ ํ๋ ํ๊ทธ์ active ํด๋์ค๊ฐ ์๋ ๊ฒ์ ํ์ธ
expect(tag.classList.contains('active')).toBe(false);
});
//3. ํค์๋๋ ์ต๋ 3๊ฐ๊น์ง ํด๋ฆญ ๊ฐ๋ฅ
it('tags can not be selected more then 3', async () => {
render(withRouter(<Route path='/' element={<Record />} />));
//ํ๊ทธ ๋ฆฌ์คํธ๋ฅผ ๋ถ๋ฌ์์
const tags = document.querySelectorAll('.tag');
//์ด 5๊ฐ์ ํ๊ทธ๋ฅผ ํด๋ฆญ
await userEvent.click(tags[0]);
await userEvent.click(tags[1]);
await userEvent.click(tags[2]);
await userEvent.click(tags[3]);
await userEvent.click(tags[4]);
//์กํฐ๋ธ๋ ํ๊ทธ๋ค์ ๋ชจ๋ ๋ถ๋ฌ์จ ๋ค์
const selectedTags = document.querySelectorAll('.tag.active');
//ํ๊ทธ๋ค์ ๊ฐ์๊ฐ 3๊ฐ์์ ํ์ธ
expect(selectedTags.length).toBe(3);
});
//4. ํ๋ฆฌ ๋ง๋ค๊ธฐ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ ํ๋ ํค์๋๋ค์ด ์ฟผ๋ฆฌ ์คํธ๋ง์ ๋ด๊ฒจ์ ธ์ ๋ค๋น๊ฒ์ดํ
it('navigate with selected tags query string', async () => {
//๋ค๋น๊ฒ์ดํธ๋ ์์ ์ปดํฌ๋ํธ๋ฅผ ํ๋ ๋ง๋ญ๋๋ค
//ํด๋น ์ปดํฌ๋ํธ๋ ๋ฐ์ ์ฟผ๋ฆฌ์คํธ๋ง ๊ฐ์ ๋ณด์ฌ์ค๋๋ค
function TmpRecordResult() {
return <div>{JSON.stringify(useLocation().search)}</div>;
}
//์ปดํฌ๋ํธ ๋ ๋๋ง
render(
withRouter(
<>
<Route path='/' element={<Record />} />
<Route path={`/result`} element={<TmpRecordResult />} />
</>,
),
);
//ํ๊ทธ๋ค์ ๋ถ๋ฌ์์
const tags = document.querySelectorAll('.tag');
//ํ๊ทธ 2๊ฐ๋ฅผ ์ ํํ๊ณ
await userEvent.click(tags[0]);
await userEvent.click(tags[1]);
//ํ๋ฆฌ ๋ง๋ค๊ธฐ ๋ฒํผ ํด๋ฆญ
const makePlaylistBtn = screen.getByText('๐ต ํ๋ฆฌ ๋ง๋ค๊ธฐ');
await userEvent.click(makePlaylistBtn);
//๋ค๋น๊ฒ์ดํ
๋ ํ์ด์ง์์ ํ๊ทธ id๋ค์ด ๋ณด์ฌ์ง์ ํ์ธ
expect(screen.getByText(`"?tags=${tags[0].id},${tags[1].id}"`))
.toBeInTheDocument;
});
});
MSW ์ฌ์ฉํ์ฌ ํ ์คํธ
ํ ์คํ ํ ๋ถ๋ถ
- ๊ทธ๋ฃน ํ๊ทธ๋ฅผ ๋ฃ์ผ๋ฉด ๊ทธ๋ฃน์ ํด๋นํ๋ ๋ ธ๋๋ง ์ ๊ณต
- ๋ฐฉ๊ตฌ์์ฝ์ํธ ํ๊ทธ๋ฅผ ๋ฃ์ผ๋ฉด ๋ฐฉ๊ตฌ์์ฝ์ํธ๊ฐ ์๋ ๋ ธ๋๋ง ์ ๊ณต
- ์ผ๋ฐ ํ๊ทธ๋ค์ ๋ฃ์ผ๋ฉด ํด๋น ํ๊ทธ๊ฐ ํ๋๋ผ๋ ์กด์ฌํ๋ ๋ ธ๋๋ค์ ์ ๊ณต
์ฐ์ ํ ์คํธ์ ์ฌ์ฉํ๋ axios ์ธ์คํด์ค์ ํ๋ก๋์ ์์ ์ฌ์ฉํ๋ axios ์ธ์คํด์ค๋ฅผ ๋ถ๋ฆฌํ์ต๋๋ค.
ํ
์คํธ๋ฅผ ํ ๋๋ ๊ฐ์์ baseURL์ ์ง์ ํ axios ์ธ์คํด์ค๋ฅผ ์ฌ์ฉํ๊ณ
ํ๋ก๋์
์์๋ ๋ณ๋์ baseURL์ ๊ฐ์ง์ง ์์ axios ์ธ์คํด์ค๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
import { Playlist } from '@components/recordResult/libs/playlist';
import { PlaylistClient } from '@components/recordResult/api/playlistClient';
import { TagType } from '@components/recordResult/types/record.result.types';
describe('playlist lib', () => {
it('๊ทธ๋ฃน ํ๊ทธ๋ฅผ ๋ฃ์ผ๋ฉด ๊ทธ๋ฃน์ ํด๋นํ๋ ๋
ธ๋๋ง ์ ๊ณต', async () => {
//ํ๋ ์ด๋ฆฌ์คํธ ์ธ์คํด์ค๋ฅผ ๋ง๋ค๊ธฐ ์ํด์
//๊ทธ๋ฃน ํค์๋์
//๋คํธ์ํฌ ๋ก์ง์ด ๋ค์ด์๋ PlaylistClient(isTest:boolean=false) ์ธ์คํด์ค๋ฅผ ์์ฑํ์ฌ ์ฃผ์
ํด์ฃผ์์ต๋๋ค
//์ ์ฒ๋ผ json ํ์ผ์ด ์๋๋ผ apiํธ์ถ์ ํ ๊ฒฝ์ฐ์๋ ๋ฐ๋ก ์ธ์คํด์ค๋ฅผ ์ฃผ์
ํ์ง ์์๋ ๋ ๋ฏํฉ๋๋ค.
const playlist = new Playlist(
new Set(['NCT DREAM']),
new PlaylistClient(true),
);
//๋ฆฌ์คํธ๋ฅผ ๋ง๋ค๊ณ
const list = await playlist.create();
//๋ฆฌ์คํธ์ ์๋ ๋ชจ๋ ๊ณก๋ค์ ์ํฐ์คํธ๊ฐ NCT DREAM์ด ๋ง๋ ์ง ํ์ธํฉ๋๋ค
expect(list.every((song) => song.artist === 'NCT DREAM')).toBe(true);
});
it('๋ฐฉ๊ตฌ์์ฝ์ํธ ํ๊ทธ๋ฅผ ๋ฃ์ผ๋ฉด ํด๋น ํ๊ทธ๋ฅผ ๊ฐ์ง๊ณ ์๋ ๋
ธ๋๋ง ์ ๊ณต', async () => {
const playlist = new Playlist(
new Set(['๋ฐฉ๊ตฌ์์ฝ์ํธ']),
new PlaylistClient(true),
);
const list = await playlist.create();
//๋ฆฌ์คํธ์ ์๋ ๋ชจ๋ ๊ณก๋ค์ด ๋ฐฉ๊ตฌ์์ฝ์ํธ ํ๊ทธ๋ฅผ ๊ฐ์ง๊ณ ์๋์ง ํ์ธํฉ๋๋ค
expect(list.every((song) => song.tags.includes('๋ฐฉ๊ตฌ์์ฝ์ํธ'))).toBe(true);
});
it('์ผ๋ฐ ํ๊ทธ๋ค์ ๋ฃ์ผ๋ฉด ํด๋น ํ๊ทธ๊ฐ ํ๋๋ผ๋ ์กด์ฌํ๋ ๋
ธ๋๋ค์ ์ ๊ณต', async () => {
const tags: TagType[] = ['๋ด๋
ธ๋', '์ฌ๋ฆ๋
ธ๋', '๊ฐ์๋
ธ๋'];
const playlist = new Playlist(new Set(tags), new PlaylistClient(true));
const list = await playlist.create();
//ํ๋์ ๊ณก์ด 1๊ฐ ์ด์์ ํ๊ทธ๋ฅผ ๊ฐ์ง๊ณ ์๊ธฐ ๋๋ฌธ์
//์ ํํ ํ๊ทธ๊ฐ ํ๋๋ผ๋ ํฌํจ๋์ด ์๋์ง ํ์ธํฉ๋๋ค.
expect(
list.every((song) => {
let check = false;
for (let tag of song.tags) {
if (tags.includes(tag)) {
check = true;
break;
}
}
return check;
}),
).toBe(true);
});
});
์ฐธ๊ณ ๋ฅผ ์ํด playlistClient.ts ํ์ผ๋ ์ถ๊ฐํฉ๋๋ค
//playlistClient.ts
import { client, testClient } from '@api/api';
import {
ArtistType,
ConcertSongListType,
SongType,
} from '../types/record.result.types';
import { AxiosInstance } from 'axios';
export class PlaylistClient {
client: AxiosInstance;
constructor(isTest: boolean = false) {
this.client = isTest ? testClient : client;
}
fetchSongs = async (
artist: Exclude<ArtistType, '๋ฐฉ๊ตฌ์์ฝ์ํธ'>,
): Promise<SongType[]> => {
return await this.client
.get(SongFile[artist])
.then((data) => data.data['songList']);
};
fetchConcertSongs = async (): Promise<ConcertSongListType> => {
return this.client.get(SongFile['๋ฐฉ๊ตฌ์์ฝ์ํธ']).then((data) => data.data);
};
// ๋ชจ๋ ํ์ผ ํจ์น (๋ฐฉ๊ตฌ์ ์ฝ์ํธ ์ ์ธ)
fetchAllData = async () => {
const songFilesExceptConcert: Exclude<ArtistType, '๋ฐฉ๊ตฌ์์ฝ์ํธ'>[] = [
'NCT 127',
'NCT U',
'NCT DREAM',
'WayV',
'SOLO',
];
return Promise.all(
songFilesExceptConcert.map(
async (artist) => await this.fetchSongs(artist),
),
).then((list) => list.flat(1));
};
}
export const SongFile: { [group in ArtistType]: string } = {
'NCT 127': '/assets/data/songs/nct127.json',
'NCT U': '/assets/data/songs/nctU.json',
'NCT DREAM': '/assets/data/songs/nctDream.json',
WayV: '/assets/data/songs/wayV.json',
SOLO: '/assets/data/songs/solo.json',
๋ฐฉ๊ตฌ์์ฝ์ํธ: '/assets/data/songs/concert.json',
};
'๐ป ํ๋ก ํธ์๋' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
CSS ์ ๋๋ฉ์ด์ ์ฑ๋ฅ ๊ฐ์ (0) | 2024.01.08 |
---|---|
[React Native] ํฐํธ ์ค์ (0) | 2024.01.05 |
[React Native] ๋ฅ๋งํฌ ์ค์ ํ๊ธฐ (0) | 2024.01.04 |
[React Native] Path alias ์ค์ (0) | 2024.01.03 |
ํ์ด์ํฐ EJS -> React ๋ง์ด๊ทธ๋ ์ด์ ๊ณผ์ (~ing) (1) | 2023.12.29 |