들어가며
안녕하세요 초원입니다. 👩🏻💻
2주가 밀렸네요 .. 테스트 코드 💦
이번 글은 무엇을 공부했고, 요구사항을 어떻게 구현했는지 차근차근 정리해보려고 해요.
미리보기
상황
추가 기능을 구현하던 중 반복일정이 캘린더 뷰에 (일정 * 주) 개수만큼 표시가 되는 문제 발생
해결
expandRecurringEvent함수에 filteredEvents에서 중복을 제거한 이벤트(uniqueEvents)를 전달
관련 코드
const uniqueEvents = Array.from(
new Set(filteredEvents.map((e) => e.id))
).reduce<Event[]>((acc, id) => {
const event = filteredEvents.find((e) => e.id === id);
if (event) acc.push(event);
return acc;
}, []);
관련 이미지
느낀점
문제 해결 과정에서 테스트코드의 필요성을 깨닫게 되었습니다.
지난 주차 리팩토링을 진행할 때 안전하면서도 공격적이게 진행할 수 있다는 점에서 유용하다고 생각했었는데요.
필수적인 기능을 수정하면서도 단위테스트, 통합테스트 중 일부가 깨지는 것을 곧바로 확인하니,
만약 테스트코드가 없었다면 다른 문제를 또 해결하고 있었겠구나 .. 하는 생각이 들었습니다.
발제 때 코딩이 완료되는 시점을 정확히 파악할 수 있다는 말이 모호했는데,
바로 이러한 경험처럼 테스트코드를 통해 사이드이펙트를 방지할 수 있다는 말이 아닐까요? 🐣🐣
그럼 시작!
MSW (Mock Service Worker)
과제를 수행하기 위해 가장 먼저 사용한 도구는 MSW입니다.
클라이언트가 HTTP 요청을 전송하면 Service Worker가 요청을 가로챈 후 Mocking된 응답 값을 반환함으로써
서버와의 통신을 모방하는 오픈소스 라이브러리입니다.
다음 단계에 따라 세팅!
# 필요한 패키지 설치
$ npm install msw @testing-library/react @testing-library/jest-dom @testing-library/user-event vitest jsdom --save-dev
# MSW 서비스 워커 파일 생성
$ npx msw init public/ --save
필요한 패키지를 설치하고 워커파일을 생성하면 아래와 같이 Public 폴더에 mockServiceWorker.js 파일이 생성됩니다.
그 다음으로는 핸들러 함수를 만들어줬어요!
const handlers = [
// GET 요청 처리
http.get('/api/events', () => {
return HttpResponse.json(events);
}),
// POST 요청 처리 (이벤트 추가)
http.post('/api/events', async ({ request }) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newEvent = (await request.json()) as any;
newEvent.id = events.length + 1; // 간단한 ID 생성
events.push(newEvent);
return HttpResponse.json(newEvent, { status: 201 });
}),
// PUT 요청 처리 (이벤트 수정)
http.put('/api/events/:id', async ({ params, request }) => {
const { id } = params;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updatedEvent = (await request.json()) as any;
const index = events.findIndex((event) => event.id === Number(id));
if (index !== -1) {
events[index] = { ...events[index], ...updatedEvent };
return HttpResponse.json(events[index]);
}
return new HttpResponse(null, { status: 404 });
}),
// DELETE 요청 처리 (이벤트 삭제)
http.delete('/api/events/:id', ({ params }) => {
const { id } = params;
const index = events.findIndex((event) => event.id === Number(id));
if (index !== -1) {
events.splice(index, 1);
return new HttpResponse(null, { status: 204 });
}
return new HttpResponse(null, { status: 404 });
}),
];
아래는 package.json과 main파일 설정입니다.
"scripts": {
"server": "node server.js",
"server:watch": "node --watch server.js",
"start": "vite",
"dev": "concurrently \"pnpm run server:watch\" \"pnpm run start\"",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"build": "tsc -b && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import { ChakraProvider } from '@chakra-ui/react';
async function prepare() {
try {
const { setupWorker } = await import('msw/browser');
const { mockApiHandlers } = await import('./mockApiHandlers');
const worker = setupWorker(...mockApiHandlers);
await worker.start();
} catch (error) {
console.error('Failed to start the worker:', error);
}
}
prepare().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ChakraProvider>
<App />
</ChakraProvider>
</React.StrictMode>
);
});
프론트엔드만 실행하고 싶다면 pnpm run start를 사용하면 됩니다.
이 경우 MSW가 활성화되어 API 요청을 모의 처리합니다.
프론트엔드와 백엔드를 동시에 실행하고 싶다면 pnpm run dev를 사용하면 됩니다.
이 경우에도 MSW는 활성화되지만, 실제 백엔드 서버도 함께 실행됩니다.
처음엔 이 세팅이 필수적인줄 알았는데 추후 8주차 과제를 진행하며 선택적으로 진행해도 된다는 것을 알게 되었어요!
과연 회사에서 msw를 적용해볼 수 있을지 ..!
8주차 과제
7주차는 기본 요구사항에 대한 테스트코드를 작성했습니다.
7주차 과제 제출 - 김초원 by kimfield98 · Pull Request #486 · hanghae-plus/front_2nd
체크포인트 PR 올리기 전에 확인사항 head barnch와 sync를 맞춰주세요. $ git pull https://github.com/hanghae-plus/front_2nd.git main base branch를 hanghae-plus:main이 아니라 hanghae-plus:<본안아이디> 로 수정해주세요 확
github.com
8주차는 테스트 코드를 먼저 작성한 뒤 구현하는 순서로 TDD를 적용하는 과제를 받았어요!
위 1,2번 요구사항에 대한 구현을 예시로 설명드릴게요-
setup 함수 만들기
// integration.test.tsx
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
const setup = (element: ReactElement) => {
const user = userEvent.setup();
return { ...render(element), user };
};
통합테스트 파일에 만들어 준 setup 함수입니다.
render 함수를 사용하여 주어진 React 엘리먼트를 렌더링하고, user 객체와 함께 반환하는데요,
이때 userEvent는 사용자 이벤트(클릭, 입력 등)를 시뮬레이션하기 위해 사용됩니다.
const { user } = setup(<App />);
await user.click(screen.getByTestId('submit-button'));
위 코드처럼 각 테스트에서 간단히 사용할 수 있어요.
환경 구성하기
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import { ChakraProvider } from '@chakra-ui/react'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ChakraProvider>
<App />
</ChakraProvider>
</React.StrictMode>,
)
이때 main 함수를 보면 Provider로 감싸져 있는 것을 확인할 수 있는데,
테스트 환경 역시 동일하게 맞춰줘야 추후 Provider의 영향을 받는 요소를 문제 없이 테스트 할 수가 있습니다!
return { ...render(<ChakraProvider>{element}</ChakraProvider>), user };
저의 경우에는 setup 함수에서 ChakraProvider로 감싼 컴포넌트를 렌더링하는 방식으로 사용을 했어요.
Testing | TanStack Query React Docs
Does this replace [Redux, MobX, etc]? react
tanstack.com
오프 코치님이 함께 참고하면 좋다고 알려주신 문서 첨부할게요 👏🏻👏🏻
server 세팅하기
const MOCK_EVENT: Event = {
id: 1,
title: '새로운 일정',
...
};
const events: Event[] = [{ ...MOCK_EVENT }];
// integration.test.tsx
import { setupServer } from 'msw/node';
const server = setupServer(...createMockServer(events));
// createMockServer.ts
import { http, HttpResponse } from 'msw';
export default function createMockServer(events: Event[]) {
const handlers = [
// GET 요청 처리
// POST 요청 처리 (이벤트 추가)
// PUT 요청 처리 (이벤트 수정)
// DELETE 요청 처리 (이벤트 삭제)
];
return handlers;
}
테스트 환경에서 msw를 사용하여 응답을 처리하기 위해 setupServer를 import했고,
핸들러들을 인자로 받는 createMockServer에 목 데이터로 만든 events를 넣어 핸들러들을 반환하도록 했습니다.
이를 통해, setupServer가 반환된 핸들러들을 사용하여 테스트를 실행할 수 있게 됩니다!
vitest API
// 각 테스트가 실행되기 전에 항상 실행
beforeEach(() => {
vi.useFakeTimers({
toFake: ['setInterval', 'Date'],
});
vi.setSystemTime(new Date(2024, 7, 1)); // -----> 8월로 세팅
});
// 모든 테스트가 시작되기 전에 한 번만 실행
beforeAll(() => server.listen());
// 모든 테스트가 끝난 후에 한 번만 실행
afterAll(() => server.close());
// 각 테스트가 끝난 후에 항상 실행
afterEach(() => {
events.length = 0;
events.push({ ...MOCK_EVENT });
vi.useRealTimers();
});
각 테스트가 일관되게 실행되고, 또 테스트 간의 간섭을 방지할 수 있도록 구성한 코드입니다.
server.listen()
• msw를 사용하여 설정된 서버를 시작합니다.
• 이 서버는 API 요청을 가로채고, 미리 정의된 핸들러를 사용하여 응답합니다. 이를 통해 네트워크 요청을 테스트할 수 있습니다.
server.close()
• 테스트가 모두 끝난 후에 서버를 종료합니다.
• 이를 통해 테스트가 끝난 후에 리소스가 적절하게 정리되고, 다른 테스트나 실제 환경에 영향을 미치지 않도록 합니다.
events.length = 0
• events 배열을 초기화합니다.
• 이를 통해 각 테스트가 끝난 후에 배열이 초기 상태로 돌아가 다음 테스트에 영향을 미치지 않도록 합니다.
events.push({ ...MOCK_EVENT })
• 초기 상태의 이벤트를 events 배열에 추가합니다.
• 이를 통해 테스트가 시작되기 전에 항상 동일한 초기 상태를 보장합니다.
vi.useRealTimers()
• 가짜 타이머를 사용한 설정을 실제 타이머로 되돌립니다.
• 이를 통해 테스트가 끝난 후에 타이머가 원래 상태로 복원됩니다.
자, 환경 구성은 끝이 났습니다.
이제 테스트 코드를 작성해볼까요? 🫨 🫨
테스트코드 작성하기
화면 왼쪽에 보이는 필수 폼을 채워넣고 일정 추가 버튼을 누르면, 화면 오른쪽에 해당 내용이 뜨는지 확인하는 테스트코드입니다.
describe('반복 유형 선택 기능', () => {
test('일정 생성 시 반복 유형을 선택할 수 있다', async () => {
const { user } = setup(<App />);
// 새 일정 추가 버튼 클릭
await user.click(screen.getAllByText('일정 추가')[0]);
// 일정 정보 입력
await user.type(screen.getByLabelText('제목'), '초원 반복');
await user.type(screen.getByLabelText('날짜'), '2024-07-05');
await user.type(screen.getByLabelText('시작 시간'), '14:00');
await user.type(screen.getByLabelText('종료 시간'), '15:00');
await user.type(screen.getByLabelText('설명'), '반복 테스트');
await user.type(screen.getByLabelText('위치'), '내 방');
await user.selectOptions(screen.getByLabelText('카테고리'), '개인');
await user.selectOptions(screen.getByLabelText('반복 유형'), '매월');
await user.clear(screen.getByLabelText('반복 간격'));
await user.type(screen.getByLabelText('반복 간격'), '1');
// 저장 버튼 클릭
await user.click(screen.getByTestId('event-submit-button'));
// 새로 추가된 일정이 목록에 표시되는지 확인
const eventList = screen.getByTestId('event-list');
expect(eventList).toHaveTextContent('초원 반복');
expect(eventList).toHaveTextContent('2024-07-05');
expect(eventList).toHaveTextContent('14:00 - 15:00');
expect(eventList).toHaveTextContent('반복 테스트');
expect(eventList).toHaveTextContent('내 방');
expect(eventList).toHaveTextContent('개인');
expect(eventList).toHaveTextContent('반복: 1월마다');
});
});
React Testing Library의 쿼리 메서드
1. screen.getAllByText('일정 추가')
• “일정 추가” 텍스트를 포함하는 모든 버튼을 찾습니다.
• 배열로 반환되므로 몇 번째 요소를 다룰 것인지 선택해야 합니다.
• await user.click(screen.getAllByText('일정 추가')[0]); // 첫 번째 일정 추가 클릭
2. screen.getByLabelText('제목')
• “제목” 라벨 텍스트를 가진 입력 필드를 찾습니다.
3. screen.getByLabelText('날짜')
• 각 입력 필드를 라벨 텍스트를 통해 선택하고, 적절한 값을 입력합니다.
4. screen.getByTestId('event-submit-button'):
• data-testid 속성을 사용하여 저장 버튼을 찾고 클릭합니다.
@testing-library/user-event 메서드
1. user.type
• 입력 필드에 텍스트를 입력합니다.
2. user.selectOptions
• 드롭다운 메뉴에서 옵션을 선택합니다.
3. user.clear
• 입력 필드의 내용을 지웁니다.
4. user.click
• 버튼이나 링크를 클릭합니다.
조금 익숙해졌으니 지난 테스트코드 주차는 잘 흡수한 것 같고, (이제서야)
본격적으로 이번 주차 과제인 TDD를 위해 구현 전에 테스트코드를 먼저 작성해봅시다 ..! 🔥
TDD 요구사항
반복일정 표시 - 캘린더 뷰에서 반복 일정을 시각적으로 구분하여 표시한다.
테스트코드 작성하기
test('캘린더 뷰에서 반복 일정을 시각적으로 구분하여 표시한다', async () => {
// 반복 일정 추가
events.length = 0;
events.push({
id: 1,
title: '반복 테스트 일정',
date: '2024-07-15',
startTime: '09:00',
endTime: '10:00',
description: '이 일정은 반복됩니다.',
location: '여기',
category: '개인',
repeat: { type: 'monthly', interval: 1 },
notificationTime: 10,
});
const { user } = setup(<App />);
// 월별 뷰로 변경
await user.selectOptions(screen.getByLabelText('view'), 'month');
// 7월 뷰에서 반복 일정 확인
let monthView = screen.getByTestId('month-view');
let repeatEvent = within(monthView).getByText('반복 테스트 일정');
// 7월에 일정이 있는지 확인
expect(repeatEvent).toBeInTheDocument();
// 8월 뷰로 변경
await user.click(screen.getByRole('button', { name: /next/i }));
// 8월 뷰에서 반복 일정 확인
monthView = screen.getByTestId('month-view');
repeatEvent = within(monthView).getByText('반복 테스트 일정');
// 8월에 일정이 있는지 확인
expect(repeatEvent).toBeInTheDocument();
// 9월 뷰로 변경
await user.click(screen.getByRole('button', { name: /next/i }));
// 9월 뷰에서 반복 일정 확인
monthView = screen.getByTestId('month-view');
repeatEvent = within(monthView).getByText('반복 테스트 일정');
// 9월에 일정이 있는지 확인
expect(repeatEvent).toBeInTheDocument();
});
(작성 중 ...)
8주차 과제 제출 - 김초원 by kimfield98 · Pull Request #520 · hanghae-plus/front_2nd
테스트 전략 1. 내가 생각하는 각 테스트전략의 장점 단위 테스트 (Unit Test) • 개별 함수나 컴포넌트의 동작을 독립적으로 검증할 수 있어 구체적인 사항에 대한 버그를 발견할 수 있다. • 테스
github.com
7주차 후기
[중간 일기]
조회, 생성까지 테스트 잘 통과되다가 갑자기 전체 테스트가 실패하는 것임 ..? 테스트코드를 처음 작성해보는 나는 뭐가 문제인지도 모르고 눈물이 찔끔 날 뻔 했는데 알고보니 오늘이 딱 8월 1일이 되어서 화면에 7월 데이터가 하나도 렌더링 되지 않는 거였음 ;;;;;;;;;;; 어이없을 무 과제 야무지게 하라고 날짜까지 도와주네(?) 시스템 시간 고정해서 해결했다. 휴. 😣😣 -- 하루 남았는데 난 과연 어디까지 할 수 있을까 밤샌다 진짜 아니 밤새기 전에 베이직 다 끝내고 심화 좀만 건들다가 잔다 화이팅 😭😭 -- 뭔가 어찌저찌 했고 제출을 했다 금요일에 리팩토링 쌈@뽕하개 함 진짜 다 나눴다 아.. 블로그 글올리고 멘토링하고 … 어.. 아니다 테스트 코드 한 주 더 해야되니까 블로그는 담주에 쓸게여 😥😥 네 .. 일단 이런 정신 머리였습니다.. Basic 테스트 코드 간신히 짰는데, Advanced 리팩토링이 날 기다리고 있었고 .. 코드 엄청 많았고 .. 컴포넌트랑 훅에 대한 테스트코드 어떻게 짜는지 모르겠고 .... Fail ....................
하지만 그렇다고 포기할 제가 아니죠 ? 처음할 때보단 쉽겠지 !!! 🤨🤨
8주차 후기
일단 테스트코드 작성에 익숙해지자.
익숙해졌다면 필요성에 대해 고민해보자. 3-4년만 지나도 테스트코드에 대한 문의가 들어올텐데 그때 고민하면 늦을 듯
함께 발전해 나갈 수 있는 방향을 미리 고민하고 그때 논리적으로 설득할 수 있다면,
장기적으로 연습해나간다면 아주 굿일겁니다!
오프 코치님이 세션 때 말씀해주신 내용을 호다닥 받아적었는데,
아주 감명깊게 마음에 콕 박혔어요.
나중에 제 후임이 '제 사수님은 그냥 테스트코드 싫어하는 것 같은데요?' 소리 안나오게
경험해보고 나의 선호가 아닌 우리의 필요에 대한 대답을 펼칠 수 있는 개발자가 되어보려고 합니다 🙏🏻
(오프 코치님 멘토링도 세션도 완전 재밌어요 !!!!!!!)
(그치만 이번 주도 준일 코치님께 멘토링을 받아따)
(pm2, ssr .. 낯선 키워드들이었지만 역시 설명 잘해주셔서 느낌이라도 알고 가서 넘 좋아요)
(nextjs custom server도 대박 문자열로 페이지 자체를 캐싱해둔다니)
제가 지금 뭘 적고 있는지 모르겠지만, 이 정신에 최선이네요 ..
내일이면 벌써 9주차 발제에요 으앙 끝나지마 아니 당장 끝내 아냐 가지마
사랑하는 4조랑 회식하는 날이지~~~~~ 신난다~~~~~
끝 🤗
'항해플러스 프론트엔드 과정' 카테고리의 다른 글
[항해플러스 프론트엔드 2기] 과제만 매주 제출하자고 다짐했던 사람의 10주 회고_정말 힘들었나요_네 (2) | 2024.08.31 |
---|---|
[항해플러스 프론트엔드 2기] 6주차 후기 - CI/CD (0) | 2024.07.27 |
[항해플러스 프론트엔드 2기] 중간점검, 솔직 후기! 과연 정말 실력이 늘었는지? (0) | 2024.07.22 |
[항해플러스 프론트엔드 2기] 5주차 후기 - 디자인 패턴 (0) | 2024.07.22 |
[항해플러스 프론트엔드 2기] 4주차 후기 - 클린코드 (0) | 2024.07.16 |