๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ’ป ํ”„๋ก ํŠธ์—”๋“œ

Vitest & MSW๋ฅผ ์‚ฌ์šฉํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ ์šฉ

by megan07 2023. 12. 29.

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. ํ‚ค์›Œ๋“œ๊ฐ€ 1๋ฒˆ ํด๋ฆญ๋˜๋ฉด ํ‚ค์›Œ๋“œ ์•กํ‹ฐ๋ธŒ
  2. ํ‚ค์›Œ๋“œ๊ฐ€ 2๋ฒˆ ํด๋ฆญ๋˜๋ฉด ํ‚ค์›ŒํŠธ ์„ ํƒ ์ทจ์†Œ
  3. ํ‚ค์›Œ๋“œ๋Š” ์ตœ๋Œ€ 3๊ฐœ๊นŒ์ง€ ํด๋ฆญ ๊ฐ€๋Šฅ
  4. ํ”Œ๋ฆฌ ๋งŒ๋“ค๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์„ ํƒ๋œ ํ‚ค์›Œ๋“œ๋“ค์ด ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์— ๋‹ด๊ฒจ์ ธ์„œ ๋„ค๋น„๊ฒŒ์ดํŒ…

 

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 ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ

ํ…Œ์ŠคํŒ… ํ•  ๋ถ€๋ถ„

  1. ๊ทธ๋ฃน ํƒœ๊ทธ๋ฅผ ๋„ฃ์œผ๋ฉด ๊ทธ๋ฃน์— ํ•ด๋‹นํ•˜๋Š” ๋…ธ๋ž˜๋งŒ ์ œ๊ณต
  2. ๋ฐฉ๊ตฌ์„์ฝ˜์„œํŠธ ํƒœ๊ทธ๋ฅผ ๋„ฃ์œผ๋ฉด ๋ฐฉ๊ตฌ์„์ฝ˜์„œํŠธ๊ฐ€ ์žˆ๋Š” ๋…ธ๋ž˜๋งŒ ์ œ๊ณต
  3. ์ผ๋ฐ˜ ํƒœ๊ทธ๋“ค์„ ๋„ฃ์œผ๋ฉด ํ•ด๋‹น ํƒœ๊ทธ๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์กด์žฌํ•˜๋Š” ๋…ธ๋ž˜๋“ค์„ ์ œ๊ณต

์šฐ์„  ํ…Œ์ŠคํŠธ์— ์‚ฌ์šฉํ•˜๋Š” 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',
};