Yangs1s 님의 상세페이지[5팀 양성진] Chapter 3-1. 프론트엔드 테스트 코드

난이도에 맞는 템플릿을 선택해서 작성해주세요!


7주차 과제 체크포인트

기본 과제

Medium

7주차 과제 체크포인트

기본과제

Medium

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

질문

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

const enqueueSnackbarFn = vi.fn();

vi.mock('notistack', async () => {
  const actual = await vi.importActual('notistack');
  return {
    ...actual,
    useSnackbar: () => ({
      enqueueSnackbar: enqueueSnackbarFn,
    }),
  };
});

vitest는 가짜 함수를 생성할수 있도록 vi.fn()을 제공합니다. fn은 테스트환경에서는 실제 함수를 작동 시킬수 없어서, 가짜 함수로 토스트가 호출이 되나 안되나 테스틀 하기 위해서 사용합니다. 'notistack'은 실제 토스트 라이브러리이고, mock은 실제 토스트 라이브러리 대신 가짜 함수를 사용하도록 교체합니다.

 expect(enqueueSnackbarFn).toHaveBeenCalledWith('일정 로딩 완료!', { variant: 'info' })

테스트 코드에서 일정 로딩/저장/삭제 시 올바른 토스트 메시지가 나타나는지 확인하고, 이게 실제 UI 토스트를 띄우지 않고도 알림 로직이 제대로 작동하는지 테스트하는 역할을 합니다.

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

실제 엘리먼트를 테스트코드서 렌더링을 해야하는데 똑같은 작동을 해야하고 SnackBarProvider을 묶어줘야 notistack을 이용할수 있기때문이라고 생각합니다. 실제 테스트환경에서도 실제환경과 동일한 조건을 줘야한다고 생각하고, Provider의존성 때문에 enqueuSnackbar 이런 라이브러리를 이용할수 있기에 에러가 남 그래서 묶어주는 동작은 의미가 있고 실제 환경과 비슷하게 만들어야 테스트도 가능하다고 생각합니다,

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <SnackbarProvider>
        <App />
      </SnackbarProvider>
    </ThemeProvider>
  </React.StrictMode>
);

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

export const server = setupServer(...handlers);

setupServer는 msw에서 제공하는 메서드로, Node.js 프로세스에서 요청을 가로채는 것을 구성하는 함수입니다. 이름에 "서버"라는 단어가 있지만, 실제로는 서버를 구축 하지 않고 전적으로 프로세스의 스레드에서 작동합니다.

이걸 바탕으로 server을 임포트 해오고 use 함수를 이용하는데 테스트별로 맞는 응답을 설정해주고, 서버없이 테스트 케이스를 가능케 하고, 에측가능한 테스트 환경을 구성해주는 역할을 합니다.

Q. setupTests.ts > 왜 이 시간을 설정해주는 걸까요? 모든 테스트에 시간을 다 일일히 설정하기 귀찮으니까 그렇고, 모든 테스트가 동일한 시간 기준으로 돌아가야 예측 가능한 테스트가 되기 때문입니다. setupTest.ts에서 전역적으로 테스트코들의 시간을 한번에 설정할수 있기에 설정해주는거라고 생각합니다.

심화 과제

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

과제 셀프회고

기술적 성장

실무에서도 사용을 해본적도 없었고, 애초에 다 처음이라 매우 어색하고 좀 어려웠습니다. 많이 사용하는 함수도 처음보는것들이 너무 많았고, 벽이 느껴졌습니다. 그래도 매치함수를 익혀보고, 공부를 하다보니까 생각보다 엄청 어렵다라는 느낌은 아니였던거 같습니다, 그래도 소정의 목적인 테스트코드에 익숙해지는 달성한거 같아서 나름 만족한 주차였습니다.

새롭게 학습한 개념

Vitest Matcher 함수 정리

객체 비교

toEqual: 객체의 실제 값(깊은 비교)을 기준으로 검증 toMatchObject: 큰 객체에서 일부 속성만 검증하고 싶을 때

배열 검증

toHaveLength: 배열의 길이를 검증할 때 toContain: 특정 원소가 배열에 존재하는지 검증 (원시값용) toContainEqual: 배열 원소가 객체일 때 깊은 비교로 검증

정규식

toMatch: 해당 정규식에 맞는지 검증

undefined, null 반환값 확인

toBeDefined / not.toBeUndefined: 정의된 값인지 검증 toBeNull: null 값인지 검증

truthy와 falsy

모든 타입의 값이 truthy 또는 falsy로 간주됨 테스트 작성 시 true/false를 직접 비교하기 번거로울 때 사용 toBeTruthy: truthy 값인지 검증 toBeFalsy: falsy 값인지 검증

타입 검증

toBeTypeOf: 원시 타입 검증 toBeInstanceOf: 객체 인스턴스 타입 검증

예외처리 검증

toThrowError() / toThrow(): 함수가 에러를 던지는지 검증

  • expect() 함수에 넘기는 검증 대상을 함수로 감싸야 함
  • 오류 발생 여부뿐만 아니라 오류 메시지나 타입까지 검증 가능
  • 정규식도 지원

특정요소의 존재

  • 특정 요소가 현재 HTML 문서 내에 존재하는지 확인하는 데 사용됩니다.
// 객체 비교
expect(user).toEqual({ name: 'John', age: 30 });
expect(user).toMatchObject({ name: 'John' }); // age는 무시

// 배열 검증
expect(arr).toHaveLength(3);
expect(['apple', 'banana']).toContain('apple');
expect([{id: 1}, {id: 2}]).toContainEqual({id: 1});

// 값이 truthy인지 확인
expect(value).toBeTruthy();
// 값이 falsy인지 확인
expect(value).toBeFalsy();

// 예외처리
expect(() => throwError()).toThrow();
expect(() => throwError()).toThrowError('Error message');


// 특정 요소 검증
expect(textInput).toBeInTheDocument();

코드 품질

케이스들을 만들때, 케이스를 생각하는게 가장 어렵고 고민이 많았습니다. 케이스가 어떤 목적으로 만드는지 명확하게 생각을 못하면서 끼어맞추는 느낌도 강하게 드는 케이스를 만들었던거 같습니다. 그리고 mui 요소를 어떻게 테스트케이스에서 가져올지 감이 안잡혔고, 방법이 명확하지 않아서 계속해서 애를 먹었던거 같아요. 그래서 컴퍼넌트에 aria-label을 추가 했습니다.

     <Select
          id="notification"
          size="small"
          aria-label="카테고리 선택"
          value={notificationTime}
          onChange={(e) => setNotificationTime(Number(e.target.value))}
        >
          {notificationOptions.map((option) => (
            <MenuItem key={option.value} value={option.value}>
              {option.label}
            </MenuItem>
          ))}
        </Select>

학습 효과 분석

테스트 코드를 처음 작성하고, 심화를 다 하지는 못했지만, 컴포넌트를 분리하고 리팩토링후 컴퍼넌트 별로, 전역상태대로, 테스트를 작성해야한다는걸 처음 알았네요. 테스트코드를 실무에서는 접해볼 기회가 없었습니다. 그래서 중요성 또한 크게 알지 못했습니다. 근데 이게 없으면 안정감이라는게 사라지게 되더라고요. 그래서 뭔가 없으면 미쳐 놓칠수 있는 부분도 많겠구나 싶었습니다. 그래서 더 중요성을 만들면서 알게되었던거 같아요. 다음주차까지도 테스트코드작성이니 그 안에 많은걸 좀 보고싶습니다. 이후에도 추가적으로 스스로 만들어보는 연습도 해보면서 감을 더 익혀야할거 같아요.

과제 피드백

리뷰 받고 싶은 내용

  1. 테스트를 작성하게 되면 어떤 내용은 테스트를 해야할지, 아니면 안해도 되는지에 대한 뭔가 그 경계선을 어떻게 잡으면 좋을까요? 저는 사용자가 직접 자주 접하는 기능을 위주로 테스트코드를 만드는게 가장 맞는거 같은데 꼭 그렇지는 않은가요.

2.이번에 만들다보니까 결과를 알고 만드는거라 케이스를 억지로 짜맞춰서 만드는거 같은데 케이스를 보통 어떻게 해야 좋은 케이스를 테스트코드에 줄수 있을까요? 좋은 테스트 코드를 만드는 전략이라는게 생각하시는게 있나요?

과제 피드백

안녕하세요 성진님! 7주차 과제 잘 진행해주셨네요 ㅎㅎ 심화과제의 경우 컴포넌트에 대한 테스트만 추가되어서 아쉽지만 불합격으로 남겨놓겠습니다 ㅠㅠ

테스트를 작성하게 되면 어떤 내용은 테스트를 해야할지, 아니면 안해도 되는지에 대한 뭔가 그 경계선을 어떻게 잡으면 좋을까요? 저는 사용자가 직접 자주 접하는 기능을 위주로 테스트코드를 만드는게 가장 맞는거 같은데 꼭 그렇지는 않은가요.

말씀하신 기준으로 적용해도 좋고, 저의 경우 의존하는 파일 혹은 함수가 많은 경우 (가령 유틸함수 같은..?) 에는 꼭 테스트가 필요하다고 생각해요! 변경사항에 대한 여파를 최대한 안전하게 관리하는거죠 ㅎㅎ

경계에 대해 찾아가는 방법은.. 답정너 같긴 한데 결국 테스트를 많이 작성해보면서 알아가는게 좋다고 생각해요 ㅋㅋ 일단 테스트를 많이 작성해보세요!

이번에 만들다보니까 결과를 알고 만드는거라 케이스를 억지로 짜맞춰서 만드는거 같은데 케이스를 보통 어떻게 해야 좋은 케이스를 테스트코드에 줄수 있을까요? 좋은 테스트 코드를 만드는 전략이라는게 생각하시는게 있나요?

결과를 알고 만드는게 테스트이지 않을까요? 완성된 결과물에 대해 테스트 케이스를 정의하고 만들어가는 게 기본적인 테스트 방식이라고 생각해요 ㅎㅎ 아마 성진님께서 생각하는건 TDD 같은데 처음부터 TDD에 집중할필요는 없답니다.

테스트에 대해 익숙해질 때 TDD도 잘 할 수 있다고 생각해요..! 좋은 테스트 코드를 만들기 위해선 위에도 이야기 했지만, 일단 테스트 자체를 많이 만들어보시면 좋아요. 그러다보면 자연스럽게 "이런 테스트는 읽기가 어렵네?" 라는 지점도 있을 것이고, "이런 테스트는 독립적이면 좋겠네?" "이런 테스트는 너무 느리네?" 라고 알아가는 부분도 있으리라 생각해요.

이에 대해 하나하나 다 설명하기보단 성진님께서 작성해보고 느껴보는게 중요하다고 생각합니다.