WEGO ) 테스트 코드로 더 안정적인 프로젝트 만들기: Jest를 활용한 실전 테스트 적용

프로젝트를 진행하면서 가장 중요하게 생각했던 것 중 하나는 코드의 안정성이었습니다. 특히 여행 관련 서비스를 개발하면서, 사용자 경험에 직접적인 영향을 미치는 UI 컴포넌트들의 안정성은 매우 중요했습니다.

▪︎ 테스트 코드의 필요성

▫︎ 복잡한 UI 상태 관리

여행 서비스의 특성상 다양한 상태를 가진 컴포넌트들이 많았습니다. 예를 들어 리뷰 카드의 경우 아래와 같은 복잡한 상태들을 수동으로 테스트하는 것은 비효율적이며, 실수하기 쉬웠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
describe('리뷰 카드 컴포넌트', () => {
const props = {
reviewId: 1,
profileImage: '/user.jpg',
reviewImage: '/test.png',
title: '테스트 리뷰',
content: '이것은 테스트 리뷰 내용입니다.',
starRating: 4.5,
travelLocation: '서울',
createdAt: '2023-10-10',
likesFlag: false,
};

it('리뷰 카드가 올바르게 렌더링되어야 합니다 (리뷰 페이지)', () => {
renderWithQueryClient(<ReviewCard {...props} nickname="테스터" />);

expect(screen.getByText('테스트 리뷰')).toBeInTheDocument();
expect(
screen.getByText('이것은 테스트 리뷰 내용입니다.'),
).toBeInTheDocument();
expect(screen.getByText('테스터')).toBeInTheDocument();
expect(screen.getByText('4.5')).toBeInTheDocument();
});

it('리뷰 카드가 올바르게 렌더링되어야 합니다 (마이페이지)', () => {
renderWithQueryClient(<ReviewCard {...props} />);

expect(screen.getByText('테스트 리뷰')).toBeInTheDocument();
expect(
screen.getByText('이것은 테스트 리뷰 내용입니다.'),
).toBeInTheDocument();
expect(screen.getByText('4.5')).toBeInTheDocument();
expect(screen.getByText('서울')).toBeInTheDocument();
});

it('이미지가 올바르게 표시되어야 합니다', () => {
renderWithQueryClient(<ReviewCard {...props} />);

const image = screen.getByAltText('테스트 리뷰');
expect(image).toBeInTheDocument();
});

▫︎ 비동기 데이터 처리의 안정성

API 호출과 같은 비동기 작업이 많은 서비스 특성상, 데이터 로딩, 에러 상태 등 다양한 상황에 대한 테스트가 필요했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
it("로딩 중일 때 스켈레톤 UI를 표시한다", () => {
(useReview as jest.Mock).mockReturnValue({
isLoading: true,
});

const { container } = renderWithQueryClient(<ReviewContents />);
// 스켈레톤 UI의 특정 클래스명을 기준으로 요소를 찾습니다.
const skeletonElements = container.querySelectorAll(".skeleton-style");
expect(skeletonElements.length).toBeGreaterThan(0);
});

it("리뷰 데이터가 있을 때 리뷰 카드가 렌더링된다", () => {
(useReview as jest.Mock).mockReturnValue({
data: {
pages: [
{
data: {
content: [
{
id: 1,
nickname: "사용자1",
profileImage: "https://example.com/profile1.jpg",
reviewImage: "https://example.com/review1.jpg",
title: "리뷰 제목 1",
content: "리뷰 내용 1",
starRating: 5,
travelLocation: "서울",
createdAt: "2023-10-01",
isLiked: true,
},
],
},
},
],
},
isLoading: false,
isError: false,
hasNextPage: false,
fetchNextPage: jest.fn(),
});

const { getByText } = renderWithQueryClient(<ReviewContents />);
expect(getByText("리뷰 제목 1")).toBeInTheDocument();
expect(getByText("리뷰 내용 1")).toBeInTheDocument();
});

it("리뷰 데이터가 없을 때 아무것도 렌더링하지 않는다", () => {
(useReview as jest.Mock).mockReturnValue({
data: {
pages: [
{
data: {
content: [],
},
},
],
},
isLoading: false,
isError: false,
hasNextPage: false,
fetchNextPage: jest.fn(),
});

const { queryByText } = renderWithQueryClient(<ReviewContents />);
expect(queryByText(/리뷰 제목 1/i)).not.toBeInTheDocument();
});

▪︎ 테스트 코드 작성 전략

▫︎ 테스트 시나리오 구성

테스트 코드는 크게 세 가지 관점에서 작성했습니다.

1. 기본 렌더링 테스트

1
2
3
4
5
6
7
8
9
10
11
describe("ReviewComment", () => {
it("리뷰 작성칸을 렌더링합니다", () => {
render(<ReviewComment />);

expect(screen.getByText("여행에 대한 후기를 남겨주세요!")).toBeInTheDocument();
expect(screen.getByLabelText("최대 20자 입력 가능 textarea")).toBeInTheDocument();
expect(screen.getByPlaceholderText("여행 제목을 입력해 주세요.")).toBeInTheDocument();
expect(screen.getByLabelText("최대 100자 입력 가능 textarea")).toBeInTheDocument();
expect(screen.getByPlaceholderText("여행에 대한 다양한 후기를 공유해 주세요!")).toBeInTheDocument();
});
});

2. 사용자 인터렉션 테스트

1
2
3
4
5
6
7
it("페이지 버튼 클릭 시 paginate 함수가 호출된다", () => {
const paginateMock = jest.fn();
const { getByText } = render(<Pagination totalPages={4} currentPage={1} paginate={paginateMock} />);

fireEvent.click(getByText("2"));
expect(paginateMock).toHaveBeenCalledWith(2);
});

3. 엣지 케이스 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
it("리뷰 데이터가 없을 때 빈 상태 메시지를 렌더링한다", () => {
(useMyReview as jest.Mock).mockReturnValue({
data: {
data: {
content: [],
total: 0,
},
},
});

renderWithProvider(<Written />);

expect(screen.getByText("아직 작성한 리뷰가 없어요!")).toBeInTheDocument();
});

▫︎ 비동기 테스트 처리

1. 로딩 상태 처리

1
2
3
4
5
6
7
8
9
10
it("로딩 중일 때 스켈레톤 UI를 표시한다", () => {
(useReview as jest.Mock).mockReturnValue({
isLoading: true,
});

const { container } = renderWithQueryClient(<ReviewContents />);
// 스켈레톤 UI의 특정 클래스명을 기준으로 요소를 찾습니다.
const skeletonElements = container.querySelectorAll(".skeleton-style");
expect(skeletonElements.length).toBeGreaterThan(0);
});

2. 데이터 페칭 결과 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
it("여행 데이터가 있을 때 여행 카드가 렌더링된다", () => {
(usePastTravel as jest.Mock).mockReturnValue({
data: {
data: {
content: [
{
travelId: 1,
travelName: "과거 여행 1",
maxTravelMateCount: 5,
currentTravelMateCount: 2,
isDomestic: true,
location: "서울",
image: "https://example.com/image.jpg",
startAt: "2023-10-01",
endAt: "2023-10-10",
},
],
total: 1,
},
},
});

const { getByText } = renderWithQueryClient(<PastTravel />);
expect(getByText(/과거 여행 1/i)).toBeInTheDocument();
});

3. 에러 상태 처리

추가로 각 컴포넌트에서 발생할 수 있는 에러 상황을 고려하여 테스트케이스를 작성했습니다.

▫︎ 모킹 전략 활용

외부 의존성이 있는 컴포넌트의 경우, 적절한 모킹을 통해 테스트의 안정성을 확보했습니다.

1
2
3
4
5
6
7
8
jest.mock("next/navigation", () => ({
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
// 필요한 다른 router 메서드들...
}),
}));

▪︎ 테스트 코드 작성 시 중점 사항

▫︎ 테스트 가독성

각 테스트 케이스의 의도가 명확히 드러나도록 작성했습니다.

1
2
3
4
5
6
7
it('기본적으로 "myTravel" 탭이 선택되어야 한다', () => {
renderWithQueryClient(
<MainTab selectedTab="myTravel" setSelectedTab={setSelectedTabMock} setSelectedSubTab={setSelectedSubTabMock} />
);

expect(screen.getByText(/나의 여행/i)).toBeInTheDocument();
});

▫︎ 테스트 유지보수성

반복되는 테스트 로직은 유틸리티 함수로 분리하여 재사용성을 높였습니다.

1
2
3
const renderWithProvider = (ui: React.ReactNode) => {
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
};

▪︎ 마치며

테스트 코드 작성은 단순히 버그를 잡기 위한 도구가 아닌, 더 나은 설계를 위한 도구이자 문서의 역할도 수행했습니다. 특히 비동기 처리가 많은 현대 웹 애플리케이션에서 다양한 상황에 대한 테스트 케이스를 작성함으로써, 더 안정적인 서비스를 제공할 수 있었습니다.

앞으로도 테스트 커버리지를 높이고, 더 효율적인 테스트 전략을 발전시켜 나갈 예정입니다.