Skip to content

[7팀 강병준] Chapter 3-1. 프런트엔드 테스트 코드 #65

Open
BangDori wants to merge 31 commits intohanghae-plus:mediumfrom
BangDori:medium
Open

[7팀 강병준] Chapter 3-1. 프런트엔드 테스트 코드 #65
BangDori wants to merge 31 commits intohanghae-plus:mediumfrom
BangDori:medium

Conversation

@BangDori
Copy link

@BangDori BangDori commented Aug 21, 2025

Medium

7주차 과제 체크포인트

기본과제

Medium

  • 총 11개의 파일, 115개의 단위 테스트를 무사히 작성하고 통과시킨다.

질문

Q. medium.useEventOperations.spec.tsx > 아래 toastFn과 mock과 이 fn은 무엇을 해줄까요?

toastFn과 mock은 외부 라이브러리인 notistack의 토스트 알림 기능을 테스트하기 위한 핵심 요소입니다.

enqueueSnackbarFn은 Vitest의 모킹 함수로, 실제 토스트 알림을 표시하는 enqueueSnackbar 함수를 대체합니다. 이를 통해 테스트 환경에서 실제 UI 토스트를 렌더링하지 않고도 함수가 올바른 메시지와 옵션으로 호출되었는지 검증할 수 있습니다.

vi.mock('notistack')은 notistack 라이브러리 전체를 모킹하되, useSnackbar 훅만 가짜 구현으로 교체합니다. 이때 실제 라이브러리의 다른 기능들은 ...actual을 통해 그대로 유지하고, useSnackbar만 테스트용 함수를 반환하도록 오버라이드합니다.

이러한 모킹 전략을 통해 useEventOperations 훅의 각종 작업(이벤트 생성, 수정, 삭제) 후 적절한 성공/실패 메시지가 토스트로 표시되는지 단위 테스트에서 검증할 수 있으며, 외부 의존성 없이 빠르고 안정적인 테스트 실행이 가능합니다.

각 테스트는 해당 작업이 완료된 후 expect(enqueueSnackbarFn).toHaveBeenCalledWith(...)를 통해 올바른 메시지가 전달되었는지 확인합니다.

Q. medium.integration.spec.tsx > 여기서 Provider로 묶어주는 동작은 의미있을까요? 있다면 어떤 의미일까요?

Provider를 상위에 Wrapping 해줌으로써 통합 테스트의 측면에서 조금 더 넓은 범위의 상황을 고려할 수 있게 됩니다.

const setup = (element: ReactElement) => {
  const theme = createTheme();
  const user = userEvent.setup();

  return {
    ...render(
      <ThemeProvider theme={theme}>
        <CssBaseline />
        <SnackbarProvider>{element}</SnackbarProvider>
      </ThemeProvider>
    ),
    user,
  };
};

이렇게 작성되어 있는 setup을 사용함으로써 테스트에서 (1) 스타일링 관련 이슈, (2) 테마 의존적인 동작 등을 할 수 있어 실제 사용자 경험에 가까운 테스트를 수행할 수 있습니다.

Q. handlersUtils > 아래 여러가지 use 함수는 어떤 역할을 할까요? 어떻게 사용될 수 있을까요?

handlersUtils에 정의된 함수들은 MSW(Mock Service Worker)를 사용한 API 모킹 핸들러들로, 테스트 환경에서 실제 백엔드 API 없이도 프론트엔드 기능을 테스트할 수 있게 해주는 핵심적인 도구입니다.

use를 사용하게 되면 실제 api 요청에 대해 서비스 워커가 이를 가로채어 응답을 반환하게 됩니다. 방법 자체는 간단하고, 특정 api를 호출해야하는 경우 해당 api에 대한 응답을 모킹하고자 할 때 활용될 수 있습니다.

Q. setupTests.ts > 왜 이 시간을 설정해주는 걸까요?

테스트 환경의 일관성과 예측 가능성을 보장하기 위한 핵심적인 설정..이 아닐까요?

시간은 고정적이지 않고 항상 동적인 상태를 띄고 있기 때문에 특정 시간대에 의존하게 되면 시간이 지남에 따라 테스트의 신뢰성과 결정성이 깨지는 위험이 있습니다. 그렇기 때문에 이를 해소하고자 특정 시간대를 fake로 설정하고 사용하는 것이 아닐까요?

심화 과제

  • App 컴포넌트 적절한 단위의 컴포넌트, 훅, 유틸 함수로 분리했는가?
  • 해당 모듈들에 대한 적절한 테스트를 5개 이상 작성했는가?

과제 셀프회고

기술적 성장

toBe vs toBeTruthy/toBeFalsy

기존에는 boolean 값 검증 시 toBeTruthy()나 toBeFalsy()와 toBe()의 차이를 몰랐지만, 이제는 정확한 타입과 값을 검증하는 toBe(true/false)의 중요성을 이해하게 되었습니다.

  • toBeTruthy(): 1, "hello", [] 등 truthy한 모든 값을 통과시킴
  • toBeFalsy(): 0, "", null, undefined 등 falsy한 모든 값을 통과시킴
  • toBe(true/false): 정확히 boolean 타입의 true/false만 검증
// 개선 전: 의도하지 않은 값도 통과할 수 있음
expect(isValidInput("")).toBeFalsy(); // 빈 문자열도 통과

// 개선 후: 정확한 boolean 값만 검증
expect(isValidInput("")).toBe(false); // 명확한 false 값만 통과

단위 / 통합 / E2E 차이

  • 단위 테스트: 개별 함수나, 컴포넌트, 훅 등 하나의 단일 모듈에 대한 검증 (이 모듈을 신뢰할 수 있는가?)

빠르고 안정적이며 버그 위치를 파악하는데 매우 용이합니다! 다만 단일 모듈에 대한 검증이 주로 이뤄지기 때문에 단위 테스트가 통과한다고 안정적인 애플리케이션이라는 것을 보장할 수는 없습니다. => 즉 코드 퀄리티와 관련한 것으로 봐도 무방할 듯

  • 통합 테스트: 여러 컴포넌트나 모듈 간의 상호작용을 검증 (어떤 이벤트에 대해 신뢰할 수 있는가?)

검색 -> UI 업데이트까지의 사용자 시나리오에 대한 테스트를 수행할 수 있기 떄문에 여러 모듈을 한 번에 테스트할 수 있음. 하지만 단위 테스트보다 느리고 설정이 생각보다 복잡하다. 컴포넌트 연동 간 적절하지 않은 모킹을 하는 경우 테스트의 신뢰성이 크게 저하할 수 있음.

  • E2E 테스트: 사용자 관점에서의 전체 워크플로우를 검증 (돈이 되는 거. 서비스에서 가장 중요한 거)

로그인부터 일정 생성, 알림 설정, 로그아웃까지 저니맵을 기준으로 제품의 가치 전반을 확인할 수 있는 테스트. 거의 실제 환경과 유사하므로 테스트시 외부 요인(네트워크 환경이 안좋은 등)에 대해서도 잘 반응해서 단위/통합 테스트에서 확인하지 못한 문제를 조기에 확인할 수 있음. 하지만 가장 느리며 환경에 의존적이라는 문제가 있다.

그래서 내가 생각할 때의 기준은,

  • 빠른 피드백 사이클 혹은 코드 퀄리티 라면 단위 테스트
  • 여러 모듈 간 연동성 혹은 사용자 시나리오를 테스트하기 위함이라면 통합 테스트
  • 제품에서 가장 중요한 기능이다라면 E2E 테스트
  • 돈을 벌어다 주는 기능이다? E2E 테스트
  • 해당 기능이 잘못 동작하면 불침번을 서야한다? E2E 테스트

코드 품질

없습니다!

학습 효과 분석

기존에는 공휴일 테스트를 작성할 때, 테스트 실행 결과로만 어떤 공휴일이 포함되는지 확인할 수 없었습니다.

it.each([
  { month: 3, expected: { '2025-03-01': '삼일절' } },
  { month: 5, expected: { '2025-05-05': '어린이날' } },
  // ...
])('$month월에는 공휴일이 있다', ({ month, expected }) => {
  const result = fetchHolidays(new Date(`2025-${month}`));
  expect(result).toEqual(expected);
});

하지만 이런 방식은 테스트만 봐서는 어떤 공휴일이 있는지 직관적으로 알기 어렵다는 한계가 있었습니다. 이를 개선하기 위해 holidayNames 필드를 추가하고, 테스트 이름에서 바로 공휴일 이름을 드러나도록 변경했습니다.

it.each([
  { month: 3, expected: { '2025-03-01': '삼일절' }, holidayNames: '삼일절' },
  { month: 5, expected: { '2025-05-05': '어린이날' }, holidayNames: '어린이날' },
  // ...
])('$month월에는 $holidayNames 공휴일이 있다', ({ month, expected }) => {
  const result = fetchHolidays(new Date(`2025-${month}`));
  expect(result).toEqual(expected);
});

이렇게 변경하면서 테스트 자체가 실행 검증뿐만 아니라, 문서로서도 어떤 달에 어떤 공휴일이 있는지를 한눈에 보여주는 역할을 하게 되었습니다.

이 과정에서 테스트 코드의 description을 잘 작성하는 것도 디버깅 관점에서나 문서화의 관점에서 굉장히 중요하구나! 라는 사실을 배우게 되었습니다.

과제 피드백

이전까지는 테스트를 작성할 때 있어 크게 '이 테스트가 가진 의도는 무엇인지', '이 테스트가 필요한 테스트인지'를 생각하지 못했던 것 같습니다. 하지만 이번 과제에서 의도가 분명하지 않거나 중복된 의도를 가진 테스트들을 찾아나가는 과정이 생각보다 재미있었고 저런 고민들이 왜 필요한지를 배울 수 있었던 것 같습니다.

리뷰 받고 싶은 내용

  1. 테스트 코드의 리팩토링은 언제, 어떻게 진행하는 게 좋을까요?
  2. 테스트 코드 작성 능력을 향상시키기 위해 어떤 학습 방법을 추천하시나요? (방식보다는 어떤 관점을 가지고 접근해야할 지 궁금합니다. ex. 이 테스트의 의도는 무엇일지 생각해보기 등등등...)

falsy / truthy 테스트를 toBe로 명확하게 검증하도록 변경
@adds9810
Copy link

adds9810 commented Aug 22, 2025

병준님의 PR보고 배워갑니다. 특히 toBe vs toBeTruthy/toBeFalsy를 읽고 toBeTruthy/toBeFalsy로 작성한 부분들이 정확하게 검증이 안 될 수 있다는 걸 알고 있던 부분들 호다닥 고쳤습니다. 감사합니다.^^

@hty0525
Copy link

hty0525 commented Aug 23, 2025

이번 과제도 열심히 하느라 고생하셨습니다!
각 테스트에서 사용된 것들을 왜 사용하는지, 어떻게 사용하는지에 대해 확실히 알아간 것 같아서 너무 좋습니다.
병준님의 기술적 성장으로 생각 이상으로 개발할 때 사소해 보이지만 중요한 것들에 대해서 저도 배워갑니다.
각 테스트의 차이와 언제 쓰면 좋을지도 정리 잘해 주셨습니다.
이번 과제를 왜 하는지에 대해 확실하게 얻은 게 있어 보여 좋습니다!

그리고 질문이 있습니다!

  • 테스트 코드를 클린 코드와 연관 지어 생각해 본다면 어떨까요?
  • 병준님이 생각하는 단위, 통합, E2E 테스트란?

@BangDori
Copy link
Author

BangDori commented Aug 23, 2025

@hty0525

이번 과제에서 깊게 고민해보지 못한 것들에 대한 질문을 주셔서 너무 감사합니다~~!

테스트 코드를 클린 코드와 연관 지어 생각해 본다면 어떨까요?

항해를 진행해오면서 저만의 클린 코드 기준이 생겼는데요, 제가 생각하는 클린 코드는 "의도를 잘 파악할 수 있는 코드"라고 생각해요. 이 말을 조금 풀어보면 다음과 같은 요소들을 포함한다고 생각합니다!

  1. 가독성
  2. 단일 책임 원칙
  3. 의도 표현

기타 등등....

그리고 이걸 테스트 코드와 연관지어 생각해본다면, 이 기준은 테스트 코드에서도 동일하게 적용돼야 한다고 생각해요.

describe('BankAccount', () => {
  it('잔액이 0원일 때 10,000원을 입금하면 잔액이 10,000원이 된다', () => {
    // Given: 초기 잔액이 0원인 계좌
    const account = new BankAccount(0);

    // When: 10,000원을 입금했을 때
    account.deposit(10_000);

    // Then: 잔액은 10,000원이 된다
    expect(account.getBalance()).toBe(10_000);
  });
});

위 테스트 코드는 given-when-then 패턴을 사용해 가독성을 높이고자 했고, 입금이라는 하나의 시나리오에 대해서만 검증하도록 작성했어요. 또 it 구문의 description을 구체적으로 작성하여 테스트의 의도를 명확히 표현하려고 했구요!

이런 관점에서 보면, 테스트 코드 역시 실제 코드와 동일하게 클린 코드 원칙을 지켜야 한다고 생각합니다. 그래야 테스트 자체가 신뢰할 수 있는 문서이자 사양으로서의 역할을 충실히 할 수 있기 때문입니다.

병준님이 생각하는 단위, 통합, E2E 테스트란?

제가 생각하는 각 테스트들은 다음과 같아요.

단위 테스트

최소 단위의 모듈(함수, 공통 컴포넌트 등)을 검증하기 위한 테스트로서 개별 모듈이 정상적으로 동작하는지를 테스트하기 위한 것!

단위 테스트는 단순히 함수의 동작을 검증하기 위한 방법뿐만 아니라 코드의 품질을 올려주기 위한 방법도 될 수 있다고 생각해요. 그 이유는 함수의 동작을 Given-When-Then 패턴으로 테스트를 작성하게 되면, 본인이 만드려는 함수가 무엇인지 조금 더 명확하게 인지할 수 있고 또 이에 맞춰 코드를 작성하게 될 것이니까 가독성이 높아지지 않을까요?! (제 생각입니닷)

그리고 또 공통 모듈에서 정말 엄청난 장점을 가질 수 있다고 생각해요. 예를 들어 사내에서 사용하는 공통 모듈을 라이브러리화해서 패키지로 추출했다고 생각을 해볼게요.

여기서 패키지에 작성되어 있는 각 함수들에 대한 단위 테스트가 작성되어 있다면, 이 라이브러리를 사용하는 개발자들은 각 함수의 동작을 빠르게 파악할 수 있고 또 신뢰할 수 있지 않을까요? 그리고 이러한 내용을 정말 잘 보여주는 대표적인 라이브러리가 es-toolkit이라고 생각들어요.

통합 테스트

여러 개의 모듈(컴포넌트, 서비스, 훅 등)이 서로 상호작용하는 과정을 검증하기 위한 테스트예요. 단일 함수가 올바르게 동작하는지 보는 게 아니라, 이벤트 → 여러 모듈 호출 → 결과라는 흐름이 끊김 없이 잘 이어지는지를 확인하는 거죠.

즉, 통합 테스트는 조금 더 실제 사용자 시나리오에 가까운 흐름을 보장한다고 볼 수 있어요. 예를 들어, "검색어를 입력했을 때 검색 결과가 화면에 노출된다."라는 일련의 과정을 하나로 묶어 검증할 수 있어요.

단위 테스트가 모듈 자체의 품질을 보장했다면, 통합 테스트는 모듈들이 합쳐졌을 때 애플리케이션이 의도대로, 시나리오대로 동작하는지를 보장하는 셈이죠. 물론! trade-off도 있겠죵?

  • 단위 테스트보다 느리다.
  • 잘못된 모킹을 하게 되면 오히려 안하는 것만 못하다.
  • 설정이 조금 복잡하다. (Provider 등)

설정이 조금 복잡하다는 근데, 완전 상황에 따라 다르게 해석될 수 있는데 오히려 Provider나 CSS 설정을 해주기 때문에 예측하지 못한 상황도 검증할 수 있다고 생각해요.

E2E 테스트

E2E(End-to-End) 테스트는 말 그대로 사용자 관점에서 서비스의 전체 흐름(워크플로우)을 끝까지 검증하는 테스트예요. 단위 테스트가 “모듈을 신뢰할 수 있는가?”, 통합 테스트가 “모듈들이 서로 잘 맞물리는가?”를 본다면, E2E는 **“사용자가 실제로 이 제품을 써서 원하는 가치를 얻을 수 있는가?”**에 집중하는 거죠.

예를 들어, 로그인 → 일정 생성 → 알림 설정 → 로그아웃 같은 저니맵 전체를 따라가는 시나리오를 테스트하는 게 대표적이에요. 이 단계에서 문제가 생기면 바로 돈과 직결되는 부분이라, 흔히 "서비스에서 가장 중요한 테스트"라고 불리기도 하죠.

또한 E2E 테스트는 실제 환경과 매우 유사하게 동작하기 때문에, 네트워크 지연, 브라우저 호환성, API 응답 속도 같은 외부 요인에 대해서도 현실적인 피드백을 줍니다. 그래서 단위/통합 테스트에서 발견하지 못한 문제를 확인할 수 있다는 장점이 있어요. 하지만! 가장 느리고 실패했을 때 원인 파악이 어렵다는 단점이 있어요..!

그럼에도 불구하고 E2E 테스트는 "사용자가 우리 서비스를 믿고 사용할 수 있는지"를 나타낼 수 있는 중요한 지표라고 생각을 해요. 그렇기 떄문에 굉~장히 중요하지 않나~~

여담으로, 제가 예전 기술 면접에서 면접관분들한테 "E2E 테스트를 하면 신뢰성을 보장할 수는 있지만 실제 환경과 유사하게 가져가기 위해서는 외부 의존성이 발생하게 되는데 이로 인한 리소스가 너무 많이 들지는 않나요?" 라는 질문을 한 적이 있어요.

당시에는 E2E 테스트에 대한 큰 이해가 없었던 상황이라, '리소스 관리가 너무 힘들지 않나?'라고만 생각했었는데 면접관분의 답변을 듣고나서 굉장히 좁은 식견으로 테스트를 바라보고 있었구나.. 라는 생각을 했습니다.. ㅎㅎㅎㅎ

"외부 의존성이 많이 생겨 리소스가 많이 들기는 하지만 오히려 이런 의존성을 그대로 다 받아내기 때문에 실제 환경과 더 유사하게 동작하게끔 할 수 있고, 문제로 인한 실패도 파악할 수 있어 사용자에게 더 높은 품질의 애플리케이션을 제공할 수 있어서 우리 팀은 E2E 테스트를 작성한다."라고 답변을 해주셨어요. 이런 경험을 통해 E2E 테스트는 단순히 비용이 큰 게 아니라, 품질을 보장하는 중요한 투자라는 걸 배웠어요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants