jtwjs Dev Wiki
  • DEV_ROAD
    • 💪🏻 생존하기
    • Week 1
      • 개발 환경 세팅
      • 타입스크립트
      • 리엑트
      • Testing Library
      • Parcel & ESLint
    • Week 2
      • JSX
      • Virtual DOM
    • Week 3
      • React Component
      • React State
    • Week 4
      • Express
      • Fetch API & CORS
      • React Hook
      • useRef & Custom Hook
    • Week 5
      • TDD
      • React Testing Library
      • MSW
      • Playwrite
      • Snapshot
    • Week 6
      • Separtion of Concerns
      • Principle
      • DI, (Dependency Injection)
      • Reflect-metadata
      • TSyringe
      • External Store
      • Follow Redux
      • usestore-ts
      • useSyncExternalStore
    • Week 7
      • Routing
      • Routes
      • Router
      • Navigation
    • Week 8
      • Design System
      • Style Basics
      • CSS-in-JS
      • Styled-Components
      • Global Style & Theme
    • Week 9
      • 개발하기 전 준비
      • 상품 목록 페이지
      • 상품 상세 페이지
      • 장바구니 페이지
    • Week 10
      • 로그인
      • 로그아웃
      • 회원가입
      • 주문 목록 & 주문 상세
    • Week 11
      • 배송 정보 입력
      • 포트원 결제 요청
      • 배송 및 결제 정보 전달
    • Week 12
      • 관리자 웹사이트개발시작
  • DEV_NOTE
    • TypeScript
      • 기본적 문법
        • Enum
        • 다형성
          • Untitled
        • 구조적 타이핑
        • 제너릭 타입
        • 컨디셔널 타입
        • 함수 메서드 타이핑
        • infer로 타입스크립트의 추론 직접 활용
        • 재귀 타입
        • 템플릿 리터럴 타입
        • 추가적인 타입 검사 satisfies 연산자
        • 타입스크립트 건망증
        • 원시 자료형에도 브랜딩 기법 사용 가능
        • 타입 좁히기
        • 유용한 타입 만들기
        • 데코레이터 함수
        • 앰비언트 선언도 선언 병합이 된다.
        • 앰비언트 선언도 선언병합이 된다.
    • Testing
      • Unit Testing
      • 단위 테스트의 두 분파
      • 좋은 단위 테스트를 구성하는 4대 요소
      • 테스트 대역과 식별할 수 있는 동작
      • 단위 테스트 스타일
      • 가치 있는 단위 테스트를 위한 리팩토링
      • 통합 테스트
      • Cross Browsing Testing
      • 기능 테스트 종류
      • React Testing Pattern
      • 프론트엔드 테스트 입문
        • 테스트 범위
        • 단위 테스트 검증
        • Mock
        • UI 컴포넌트 테스트
        • 테스트 커버리지
        • 웹 통합 테스트
        • MSW
        • 스토리북
        • 시각적 회귀 테스트
        • E2E 테스트
        • Github Actions 설정
        • 깃허브 액션에서 E2E
      • 시프트 레프트
        • 테스트 기본중의 기본
        • 단위 테스트
        • 코드 복잡도
        • 리팩터링
        • 코드 리뷰
        • 통합 테스트 패턴
        • 시스템 테스트의 자동화
        • 탐색적 테스트
      • Test Tip
      • vitest
      • playwright
      • Test Data Generator
      • MSW
    • Algorithm
      • coding test
      • Data Structure
    • Next.js
      • Data Fetching
      • Hydration
      • Next 13
      • Optimization
      • Next 15
    • Tailwind
      • Tailwind CSS
      • Theme
      • Directives
      • Tool
      • Design System
    • Storybook
      • Storybook
      • CSF3
      • CDD
      • Headless Component
    • Funtional Programming
      • 함수형 프로그래밍
      • 참조 투명성
      • 부수효과
      • 함수 합성
      • 제너릭 타입 활용하기
      • 암묵적 입출력
      • 액션과 계산, 데이터
      • 계층형 설계
      • 호출 그래프
      • 함수형 설계
      • 불변성
      • 일급 함수
      • 함수형 도구
    • Git
      • Github Actions
      • Conflict
      • Branch 전략
    • Contents Format
      • Audio
    • 3D Graphic
      • 3D keyword
      • Three.js
      • Geometry
      • Material
      • Light
      • Camera
      • Decal
      • Rotation
      • Text
      • Shadow
      • Fog
      • Post Processing
      • Animation
      • Math
        • Vector Space
        • 벡터의 연산
        • 회전 계산
      • 3D 컨텐츠가 만들어지는 과정
      • R3F
      • Env
      • Scene
      • Transform
      • R3F
      • Interaction & Raycast
      • Rendering Algorithnm
      • Blender
      • Blender
    • Accessibility
      • 접근성이란
    • Interactive Web
      • Parallax
      • Canvas
      • requestAnimationFrame
      • Effect
      • HSL
      • React.js + Canvas
      • Matter.js
    • AWS
      • DevOps
      • Amplify
      • S3
      • 클라우드 컴퓨팅
        • 온프레미스와 클라우드
        • 클라우드 도입효과
        • 클라우드 컴퓨팅의 범위
        • 컴퓨팅 옵션
          • EC2 - Virtual Machin
          • ECS, EKS - Container
          • Lambda - Serverless
        • 네트워크 가상화
        • 스토리지
        • 데이터베이스
        • 데이터 수집
        • 머신 러닝 영역
        • IoT 영역
        • 블록체인 영역
      • 클라우드 아키텍처 설계
    • Network
      • Web Server & WAS
    • System Design
      • System Design
      • Component
      • 의존성을 배제한 개발
      • Error Handling
      • Architecture
        • 모노로틱 아키텍처
        • Clean Architecture
        • Layered Architecture
        • 이벤트 기반 아키텍처
      • 상황을 파악하는 메타인지
      • 중복 문제 해결하기
      • Monorepo Arhitecture
        • 모노레포 운영과 트러블슈팅
        • Module Federation
      • 코드 병목지점
      • API 대응
      • 공통 코드
      • Infra 구축
      • 모듈 기반의 개발 방식
      • Design System
        • 최소 수준의 아키텍처 설정
        • 더 효율적인 디자인시스템 만들기
        • 디자인 시스템과 UI 라이브러리 목적
        • 디자인 토큰
      • 효율적인 업무
        • 업무 프로세스 병목 파악
      • Clean Code
      • Design Pattern
        • CQRS Pattern
        • Strangler Fig Pattern
        • 데코레이터 패턴
        • 커맨드 패턴
        • 전략 패턴
        • 옵저버 패턴
      • A/B 테스팅
      • 대규모 리엑트 웹앱 개발
        • 복잡성 관리
        • 모듈성
        • 성능
        • 디자인 시스템
        • 데이터 패칭
        • 상태 관리
        • 국제화
        • 코드 조직화하기
        • 개인화 A/B 테스팅
        • 확장 가능한 웹 아키텍처
        • 테스팅
        • 툴링
        • 기술적 마이그레이션
        • 타입스크립트
        • 라우팅
        • 사용자 중심 API 디자인
        • 리액트 미래
    • Performance
      • React DevTools
      • Component 최적화
      • Page Load
      • API
    • MFA
      • MSA
      • MFA 도입하기
      • Monorepo
        • Monorepo Tool
        • Yarn Berry Workspace
        • Turborepo
      • MFA Composition
      • SPA 통합
      • Design System
      • Package Manager
        • Yarn
        • pnpm
      • Transpiler & Bundler
        • Babel
        • Rollup
        • esbuild
        • swc
        • Webpack
        • Vite
      • 분해와 통합을 위한 여러 기술 비교
    • State Management
      • Zustand
    • React v18
      • Automatic batching
      • Suspense
      • Transition
    • SEO
      • Search Engine Optimization
      • Open Graph Element
      • Metadata
    • FE Develop
      • Scrubbing
      • Clipboard
    • Refactoring
      • 리팩토링 깊게 들여다보기
      • 긴 코드 조각내기
      • 타입 코드 처리하기
      • 유사한 코드 융합하기
      • 데이터 보호
      • 코드 추가 및 제거
    • OAuth 2.0
    • Analytics
      • Mixpanel
    • ETC
      • VSCode
    • React Hook In Action
      • useContext & Provider
      • 커스텀 훅
      • 코드 분할하기 with Suspense, lazy
      • Suspense와 이미지 적재하기
      • useTransition, uesDeferredValue
      • SuspenseList
Powered by GitBook
On this page
  • 프로바이더
  • Next.js 라우터 렌더링 통합 테스트
  • test.each 활용
  • React Hook Form 테스트
  • 폼 유효성 검사 테스트
  • 인터렉션 테스트
  • 이미지 업로드 통합 테스트
  1. DEV_NOTE
  2. Testing
  3. 프론트엔드 테스트 입문

웹 통합 테스트

상위 컴포넌트에서 하위 컴포넌트에 위임한 처리까지 테스트하면 상위 컴포넌트의 책임이 불분명해진다.

상위 컴포넌트는 연동하는 부분에만 집중해 테스트를 작성하면 테스트 목적 컴포넌트간 책임도 분명해진다.

프로바이더

  • 프로바이더를 사용하는 전역 UI를 대상으로 실시하는 테스트의 중점은 다음과 같다.

    • Provider의 상태에 따라 렌더링 여부가 변경됨

    • Provider의 갱신 함수로 상태를 갱신 가능

  • Context를 테스트하는 두 가지 방법

1. 테스트용 컴포넌트를 만들어 인터렉션 실행하기

  • useToastAction 이라는 커스텀 훅을 사용하면 최하위 컴포넌트에서도 <Toast> 렌더링 가능

  • 이를 테스트하고자 테스트용으로만 사용할 컴포넌트를 만들어 실제와 비슷한 상황을 재현해보자.

  • showToast 를 실행할 수만 있으면 되기 때문에 버튼을 클릭하면 showToast가 실행되도록 구현 ㄱ

// 테스트용 컴포넌트
const TestComponent = ({ message }: { message: string }) => {
  const { showToast } = useToastAction();
  return <button onClick={() => showToast(message)}>show</button>
}
  • 테스트 render 함수로 최상위 컴포넌트인 <TestProvider> 와 하위 컴포넌트인 <TestComponent> 를 렌더링 한다.

test("showToast를 호출하면 Toast 컴포넌트가 표시된다.", async () => {
  const message ='test';
  render(
    <TestProvider>
      <TestComponent message={message}/>
    </TestProvider>
  )
  expect(screen.queryByRole("alert")).not.toBeInTheDocument();
  await user.click(screen.getByRole("button"));
  expect(screen.getByRole("alert")).toHaveContent(message)
})

2. 초깃값을 주입해서 렌더링된 내용 확인하기

  • <TestProvider> 는 Props 에 defaultState 라는 초깃값을 설정할 수 있도록 구현됨

  • 단순히 렌더링 여부를 확인하고 싶은거면 defaultState 에 초깃값을 주입해 검증하면 됨

test("showToast를 호출하면 Toast 컴포넌트가 표시된다.", async () => {
  const state: ToastState = {
    isShown: true,
    message: "성공했습니다",
    style: "succeed",
  }
  render(
    <TestProvider defaultState={state}>{null}</TestProvider>
  )
  expect(screen.getByRole("alert")).toHaveContent(state.message)
})

Next.js 라우터 렌더링 통합 테스트

라우터 (페이지 이동과 URL을 관리하는 기능)와 관련된 UI 컴포넌트의 통합 테스트

  • Next.js에서 라우터 부분을 테스트하려면 목 객체를 사용해야 한다.

  • next-router-mock : 제스트에서 Next.js 라우터를 테스트할 수 있도록 목 객체를 제공하는 라이브러리

    • <Link> 컴포넌트에서 발생한 라우터 변화

    • useRouter를 활용한 URL 참조 혹은 변경에 대한 통합 테스트를 jsdom에서 실행 가능

test("현재 위치는 'My Posts'이다", () => {
  mockRouter.setCurrentUrl("/my/posts"); //현재 url이 /my/posts 라고 가정
})
import mockRouter from 'next-router-mock';

test("현재 위치는 '...'이다", () => {
  mockRouter.setCurrentUrl("/my/posts/create");
  render(<Nav onCloseMenu={() => {}} />);
  const link = screen.getByRole("link", {name: "Create Post" });
  expect(link).toHaveAttribute("aria-current", "page");
})

test.each 활용

  • 동일한 테스트를 매개변수만 변경해 반복하고 싶다면 test.each 사용해보자.

test.each([
  { url: "/my/posts", name: "My Posts" },
  { url: "/my/posts/123", name: "My Posts" },
  { url: "/my/posts/create", name: "Create Posts" },
])("$url의 현재 위치는 $name이다", ({ url, name }) => {
  mockRouter.setCurrentUrl(url);
  render(<Nav onCloseMenu={() => {}} />);
  const link = screen.getByRole("link", {name});
  expect(link).toHaveAttribute("aria-current", "page");
})

입력 통합 테스트

import { render, screen } from '@testing-library/react'
import mockRouter from 'next-router-mock';

function setup(url = "/my/posts?page=1") {
  mockRouter.setCurrentUrl(url);
  render(<Header />)
  const combobox = screen.getByRole("combobox", { name: "공개 여부" });
  return { combobox }
}

test("기본값으로 '모두가 선택됨", async () => {
  const { combobox } = setup();
  expect(combobox).toHaveDisplayValue("모두")
})


test("status?=public 으로 접속하면 '공개'가 선택됨", async () => {
  const { combobox } = setup("/my/posts?status=public");
  expect(combobox).toHaveDisplayValue("공개")
})

인터렉션 테스트

  • 인터렉션 함수를 별도로 만들어 테스트 코드에서 UI 컴포넌트의 입력을 재현하는 코드를 직관적으로 바꿔보자

import { render, screen } from '@testing-library/react'
import mockRouter from 'next-router-mock';

const user = userEvent.setup();

function setup(url = "/my/posts?page=1") {
  mockRouter.setCurrentUrl(url);
  render(<Header />)
  const combobox = screen.getByRole("combobox", { name: "공개 여부" });
  async function selectOption(label: string) {
    await user.selectOptions(combobox, label);
  }
  return { combobox, selectOption }
}

test("공개 여부를 변경하면 status가 변경된다.", async () => {
  const { selectOption } = setup();
  expect(mockRouter).toMatchObject({query: {page: "1"}});
  await selectOption("공개");
  expect(mockRouter).toMatchObject({
    query: { page: "1", status: "public" },
  })
  await selectOption("비공개");
  expect(mockRouter).toMatchObject({
    query: {page: "키", status: "private"
  })
  
  
})

검증 범위를 좁히면 UI 컴포넌트의 책임과 이에 따른 테스트 코드가 더욱 명확해진다.

React Hook Form 테스트

  • 폼은 전송하기 전에 입력된 내용을 참조하기 때문에 폼을 구현할 떄 먼저 '어디에서 입력 내용을 참조할 것인지'를 정해야 한다.

    • 제어 컴포넌트: useState 를 사용해 컴포넌트 단위로 상태를 관리하는 컴포넌트

      • 제어 컴포넌트로 구현된 폼은 관리 중인 상태를 필요한 타이밍에 웹 API로 보낸다.

    • 비제어 컴포넌트: 폼을 전송할 떄 <input> 등의 입력 요소에 브라우저 고유 기능을 사용해 값을 참조하도록 구현함

      • 전송시 직접 값을 참조하기 때문에 useState 상태를 관리하지 않아도 되며, ref 로 DOM의 값을 참조

      • value, onChange 를 따로 지정하지 않는다.

      • 제어 컴포넌트에서 useState로 지정한 초깃값은 defaultValue 로 대체

  • React Hook Form은 비제어 컴포넌트로 고성능 폼을 쉽게 작성할 수 있도록 도와주는 라이브러리

  • 입력 요소를 참조하는 ref, 이벤트 핸들러를 자동으로 생성하고 설정해준다.

const { register, handleSubmit } = useForm({
  defaultvalues: { search: q },
})
  • register 는 전송 시 참조할 입력 내용으로 '등록한다'는 의미

  • register함수를 사용하는것만으로 참조와 전송 준비가 완료됨

<input type="search" {...register("search")} />

폼 유효성 검사 테스트

  • 입력 내용에 따라 어떤 유효성 검사가 실시되는지에 중점을 두고 테스트

  • React Hook Form에는 하위 패키지로 resolver 가 있다.

    • 여기에 입력 내용을 검증할 유효성 검사 스키마 객체를 할당 가능

export const PostForm = (props: Props) => {
  const {
    register,
    setValue,
    handleSubmit,
    control,
    formState: { erros, isSubmitting },
  } = useForm<PostInput>({
    resolver: zodResolver(createMyPostInputSchema), // 입력 내용 유효성 검사 스키마
  });
  
  return (
    <form
      aria-label={props.title}
      className={styles.module}
      onSubmit={handleSubmit(props.onValid, props.onInvalid)}
    >
  )
}
// src/lib/schema/MyPosts.ts
import * as z from 'zod';

export const createMyPostInputSchema = z.object({
  title: z.string().min(1, '한 글자 이상의 문자를 입력해주세요'),
  description: z.string().nullable(),
  body: z.string().nullable(),
  published: z.boolean(),
  imageUrl: z.string({ required_error: "이미지를 선택해주세요" }).nullable(),
})

React Hook Form의 handleSubmit 함수의 인수는 함수를 직접 인라인으로 작성하지 않고 Props 에서 취득한 이벤트 핸들러를 지정 할 수도 있다.

type Props<T extends FieldValues = PostInput> = {
  title: string;
  children?: React.ReactNode,
  onClickSave: (isPublish:boolean) => void,
  onValid: SubmitHandler<T>;
  onInvalid?: SubmitErrorHandler<T>;
}

<form onSubmit={handleSubmit(props.onValid, props.onInvalid)}/>
  • 해당 컴포넌트의 책임

    • 입력폼 제공

    • 입력 내용 유효성 검사

    • 오류가 있으면 오류 표시

    • 유효 내용이 전송되면 onValid 실행

    • 유효하지 않은 내용이 전송되면 onInvalid 실행

인터렉션 테스트

  • 인터렉션 테스트를 위해 설정 함수에 인터랙션 함수를 추가한다. (개별 인터렉션들을 반환)

async function setup() {
  const { container } = render(<Default />);
  const { selectImage } = selectImageFile();
  async function typeTitle(title: string) {
    const textbox = screen.getByRole("textbox", {name: "제목"});
    await user.type(textbox, title);
  }
  async function saveAsPublished() {
    await user.click(screen.getByRole("switch", {name: "공개 여부" }));
    await user.click(screen.getByRole("button", {name: "공개하기" }));
    await screen.findByRole("alertdialog");
  }
  async function saveAsDraft() {
    await user.click(screen.getByRole("button", {name: "비공개 상태로 저장"}));
  }
  async function clickButton(name: "네" | "아니오") {
    await user.click(screen.getByRole("button", { name }));
  }
  
  return {container, typeTitle, saveAsPublished, saveAsDraft, clickButton, selectImage}
}
test("공개를 시도하면 AlertDialog가 표시된다.", async () => {
  const { typeTitle, saveAsPublished, selectImage } = await setup();
  await typeTitle("201");
  await selectImage();
  await saveAsPublished();
  expect(screen.getByText("기사를 공개합니다. 진행하시겠습니까?")).toBeInTheDocument();
})

test("API 통신을 시작하면 '저장 중입니다...'가 표시된다", async () => {
  const { typeTitle, saveAsPublished, clickButton, selectImage } = await setup();
  await typeTitle("hoge");
  await selectImage();
  await saveAsPublished();
  await clickButton("네");
  await waitFor(() => expect(screen.getByRole("alert").toHaveTextContent("공개됐습니다."))
})

화면 이동 테스트

  • 화면 이동은 웹 API 호출이 정상적으로 종료된 후에 발생한다.

  • waitFor 함수로 mockRouter 의 pathname 이 특정 페이지와 일치하는지 검증

test("공개에 성공하면 화면을 이동한다", async () => {
  const { typeTitle, saveAsPublished, clickButton, selectImage } = await setup();
  await typeTitle("201");
  await selectImage();
  await saveAsPublished();
  await clickButton("네");
  await waitFor(() => expect(mockRouter).toMatchObject({ pathname: "/my/posts/201" }))
})

이미지 업로드 통합 테스트

파일 업로드 기능은 E2E 테스트에서도 검증 가능하고 통합 테스트에서도 검증 가능하다.

  • 컴퓨터에 저장된 이미지를 선택하여 업로드 시도

  • 이미지 업로드에 성공하면 프로필 이미지로 적용

  • 이미지 업로드에 실패하면 실패했음을 알림

jest로 재현할 수 없는 처리들은 mock 객체를 이용하자.

  • 컴퓨터의 저장된 이미지를 데이터화

  • Next.js에 구현된 이미지 업로드 API

이미지를 선택하는 mock 함수

  • 테스트 환경인 jsdom은 브라우저 API 제공하지 않으므로 이미지 선택(Browser API)를 사용할 수 없음

    • 더미 이미지 파일 작성하기

    • user.upload를 호출해 이미지 선택 인터렉션 재현하기

export function selectImageFile(
  inputTestId = 'file',
  fileName = 'hello.png',
  content = 'hello'
) {
  const user = userEvent.setup();
  // 더미 이미지 파일 작성
  const filePath = ['C:\\fakepath\\${fileName}`];
  const file = new File([content], fileName, { type: "image/png" });
  // render한 컴포넌트에서 data-testid="file"인 input을 취득
  const fileInput = screen.getByTestId(inputTestId);
  // 이 함수를 실행하면 이미지 선택이 재현됨
  const selectImage = () => user.upload(fileInput, file);
  return { fileInput, filePath, selectImage };
}

이미지 업로드 API를 호출하는 mock 함수

  • 이미지 업로드 API를 호출하게되면 Next.js의 API Routes에 요청이 발생하고 AWS S3에 이미지를 업로드 하는 처리가 실행됨

    • 이와 같은 처리까지 UI 컴포넌트 테스트에서 실행하면 본 목적에서 벗어남

    • 그래서 목 함수를 설정해 정해진 응답이 오도록 설정

//__mock__/jest.ts

import { ErrorStatus, HttpError } from '@/lib/error';
import * as UploadImage from '../fetcher';
import { uploadImageData } from './fixture';

jest.mock('../fetcher');

export function mockUploadImage(status: ErrorStatus) {
  if (status && status > 299) {
    return jest
      .spyOn(UploadImage, "uploadImage")
      .mockRejectedValueOnce(new HttpError(status).serialize());
  }
  return jest
    .spyOn(UploadImage, "uploadImage")
    .mockResolvedValueOnce(uploadImageData);
}
test("이미지 업로드에 성공하면 이미지의 src 속성이 변경됨", async () => {
  mockUploadImage()
  render(<TestComponent />);
  expect(screen.getByRole('img').getAttribute("src")).toBeFalsy();
  const { selectImage } = selectImageFile();
  await selectImage();
  await waitFor(() => 
    expect(screen.getByRole("img").getAttribute("src")).toBeTruthy()
})

test("이미지 업로드에 실패하면 경고창이 표시된다.", async () => {
  mockUploadImage(500); // 실패하는 목함수
  render(<TestComponent />);
  expect(screen.getByRole('img').getAttribute("src")).toBeFalsy();
  const { selectImage } = selectImageFile();
  await selectImage();
  await waitFor(() => 
    expect(screen.getByRole("alert").toHaveTextContent(
      "이미지 업로드에 실패했습니다"
    )
})
Previous테스트 커버리지NextMSW

Last updated 9 months ago