Dev Notes
Web Vitals
Web Vitals
Dev Notes
Docs프론트엔드 테스트 전략 가이드
Web Vitals
Web Vitals
GitHub
Testing120분

프론트엔드 테스트 전략 가이드

테스트 가능한 구조 설계부터 Testing Library 활용까지, 효과적인 프론트엔드 테스트 전략

2025년 10월 28일
testingtesting-libraryjestvitestreact-testingunit-test

적용 환경: React 18+, Testing Library, Jest 또는 Vitest

"테스트 커버리지 80%를 달성하라"는 말을 들으면 무작정 모든 컴포넌트/로직에 테스트를 붙이고 싶어집니다. 하지만 이 접근은 대개 실패합니다. 거대한 페이지 컴포넌트를 테스트하느라 시간을 쏟고, 정작 버그가 발생하는 유틸리티 함수는 테스트 없이 방치됩니다. 변경이 잦은 UI는 테스트가 금방 깨지고, 테스트 유지보수가 부담이 되어 결국 테스트를 포기하게 됩니다.

프론트엔드 테스트에서 중요한 것은 "얼마나 많이"가 아니라 "무엇을" 테스트하느냐입니다. 버그가 발생할 가능성이 높고, 발생했을 때 영향이 큰 코드에 테스트를 집중해야 효과적입니다.

정보

테스트 전략의 핵심은 비용 대비 효과입니다. 테스트 작성과 유지보수에 드는 비용보다 테스트가 방지하는 버그의 가치가 커야 합니다. 모든 코드를 동일하게 테스트하지 말고, 버그 발생 확률이 높고 영향이 큰 곳에 집중하세요.

테스트 가능한 구조

테스트 전략을 논하기 전에, 코드 구조가 테스트를 허용하는지 점검해야 합니다. 하나의 컴포넌트에 UI, 상태 관리, API 호출, 비즈니스 로직이 모두 섞여 있으면 테스트가 어렵습니다. 1000줄짜리 ProductPage.jsx를 테스트하려면 API를 모킹하고, 상태를 설정하고, 수많은 렌더링 조건을 검증해야 합니다.

관심사 분리가 테스트 용이성의 핵심입니다. 각 계층은 독립적으로 테스트할 수 있어야 합니다.

테스트하기 어려운 구조
components
ProductPage.jsx

위 구조에서 ProductPage.jsx는 1000줄이 넘고 모든 로직이 한 파일에 포함되어 있습니다.

테스트하기 쉬운 구조
features/product
ProductPage.tsx
components
ProductInfo.tsx
AddToCartButton.tsx
hooks
useProduct.ts
useCart.ts
utils
pricing.ts
validation.ts

이렇게 분리하면 pricing.ts의 가격 계산 로직은 React 없이 순수하게 테스트할 수 있고, useCart.ts는 renderHook으로 상태 변화만 검증하면 됩니다. ProductPage.tsx는 이들을 조립하는 역할만 담당하므로, 간단한 통합 테스트로 충분히 커버할 수 있습니다.

테스트 우선순위 피라미드

프론트엔드 코드가 모두 동일한 가치를 갖는 것은 아닙니다. 가격 계산 함수의 버그는 매출에 직접 영향을 미치지만, 버튼의 호버 색상이 잘못되어도 사업이 멈추지는 않습니다. 이처럼 코드마다 중요도가 다르기 때문에, 테스트 노력을 어디에 집중할지 결정하는 것이 효과적인 테스트 전략의 핵심입니다.

E2E 테스트(느림)
← 최소한만
통합 테스트(Feature)
← 주요 흐름만
컴포넌트 테스트
← 재사용 컴포넌트
커스텀 훅 테스트
← 상태 로직
순수 함수(유틸/헬퍼)
← 가장 많이!
우선순위대상커버리지 목표이유
1순위순수 함수/유틸100%버그 발생률 높음, 테스트 쉬움, ROI 최고
2순위커스텀 훅90%+재사용 로직, 상태 관리 핵심
3순위UI 컴포넌트80%+재사용 컴포넌트 중심
4순위Feature 컴포넌트50%+통합 테스트, 최소한만

순수 함수부터 시작하세요

테스트 문화가 없는 프로젝트라면 utils 폴더의 순수 함수부터 시작하는 것이 좋습니다. 외부 의존성이 없어 테스트 작성이 쉬우면서도, 비즈니스 로직의 핵심이라 가치가 높기 때문입니다. 이런 작은 성공 경험이 쌓이면서 자연스럽게 팀의 테스트 문화가 형성됩니다.

1순위: 순수 함수/유틸 테스트

비즈니스 로직의 핵심을 담고 있는 영역입니다. 조건 분기가 많아 버그가 발생하기 쉬운 반면, 외부 의존성이 없어 테스트 작성은 가장 간단합니다.

테스트 대상

  • 가격/할인 계산
  • 날짜 포맷팅
  • 유효성 검증
  • 데이터 변환/정규화
  • 배열/객체 조작 헬퍼

테스트 전략

순수 함수는 입력이 같으면 항상 같은 출력을 반환하므로, 테스트 역시 단순해집니다. 입력을 주고 출력을 검증하는 것이 전부입니다.

TypeScript
// utils/pricing.ts
export function calculateDiscount(price: number, discountRate: number): number {
  if (discountRate < 0 || discountRate > 100) {
    throw new RangeError('할인율은 0-100 사이여야 합니다');
  }
  return Math.floor(price * (discountRate / 100));
}
 
export interface PriceInput {
  price: number;
  discount?: number;
  coupon?: number;
}
 
export function calculateFinalPrice({ price, discount = 0, coupon = 0 }: PriceInput): number {
  const discountAmount = calculateDiscount(price, discount);
  return Math.max(0, price - discountAmount - coupon);
}
TypeScript
// utils/pricing.test.ts
import { calculateDiscount, calculateFinalPrice } from './pricing';
 
describe('calculateDiscount', () => {
  // 정상 케이스
  test('10% 할인을 정확히 계산한다', () => {
    expect(calculateDiscount(10000, 10)).toBe(1000);
  });
 
  // 경계값
  test('할인율 0%는 0을 반환한다', () => {
    expect(calculateDiscount(10000, 0)).toBe(0);
  });
 
  test('할인율 100%는 전액을 반환한다', () => {
    expect(calculateDiscount(10000, 100)).toBe(10000);
  });
 
  // 엣지 케이스
  test('소수점은 버림 처리한다', () => {
    expect(calculateDiscount(9999, 10)).toBe(999);
  });
 
  // 에러 케이스
  test('할인율이 범위를 벗어나면 에러를 던진다', () => {
    expect(() => calculateDiscount(10000, -10)).toThrow(RangeError);
    expect(() => calculateDiscount(10000, 101)).toThrow(RangeError);
  });
});
 
describe('calculateFinalPrice', () => {
  test('할인과 쿠폰을 모두 적용한다', () => {
    expect(calculateFinalPrice({
      price: 10000,
      discount: 10,
      coupon: 1000,
    })).toBe(8000);
  });
 
  test('최종 가격은 0 미만이 되지 않는다', () => {
    expect(calculateFinalPrice({
      price: 5000,
      discount: 50,
      coupon: 5000,
    })).toBe(0);
  });
});

테스트 케이스 분류

가장 일반적인 사용 시나리오로, 예를 들어 10000원 상품에 10% 할인을 적용하면 1000원이 할인되는 경우입니다. 이 케이스가 통과하면 기본 로직이 정상 동작한다고 볼 수 있습니다.

TypeScript
test('10% 할인을 정확히 계산한다', () => {
  expect(calculateDiscount(10000, 10)).toBe(1000);
});

2순위: 커스텀 훅 테스트

상태 관리와 부수 효과(side effects)를 포함한 재사용 로직입니다. @testing-library/react의 renderHook을 사용합니다.

테스트 대상

  • 데이터 페칭 훅 (useQuery, useFetch)
  • 상태 관리 훅 (useCart, useAuth)
  • 폼 관리 훅 (useForm)
  • UI 상태 훅 (useModal, useToast)

동기 훅 테스트

TypeScript
// hooks/useCart.ts
import { useState, useCallback } from 'react';
 
interface Product {
  id: string;
  name: string;
  price: number;
}
 
interface CartItem extends Product {
  quantity: number;
}
 
export function useCart() {
  const [items, setItems] = useState<CartItem[]>([]);
 
  const addItem = useCallback((product: Product, quantity = 1) => {
    setItems((prev) => {
      const existing = prev.find((item) => item.id === product.id);
      if (existing) {
        return prev.map((item) =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + quantity }
            : item
        );
      }
      return [...prev, { ...product, quantity }];
    });
  }, []);
 
  const totalPrice = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
 
  return { items, addItem, totalPrice };
}
TypeScript
// hooks/useCart.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCart } from './useCart';
 
describe('useCart', () => {
  test('초기 상태는 빈 장바구니다', () => {
    const { result } = renderHook(() => useCart());
 
    expect(result.current.items).toEqual([]);
    expect(result.current.totalPrice).toBe(0);
  });
 
  test('상품을 추가할 수 있다', () => {
    const { result } = renderHook(() => useCart());
 
    act(() => {
      result.current.addItem({ id: '1', name: '상품', price: 10000 });
    });
 
    expect(result.current.items).toHaveLength(1);
    expect(result.current.totalPrice).toBe(10000);
  });
 
  test('같은 상품을 추가하면 수량이 증가한다', () => {
    const { result } = renderHook(() => useCart());
    const product = { id: '1', name: '상품', price: 10000 };
 
    act(() => {
      result.current.addItem(product);
      result.current.addItem(product);
    });
 
    expect(result.current.items).toHaveLength(1);
    expect(result.current.items[0].quantity).toBe(2);
  });
});

act() 사용 시 주의

상태를 변경하는 함수 호출은 반드시 act()로 감싸야 합니다. 그렇지 않으면 "Warning: An update to Component inside a test was not wrapped in act(...)" 경고가 발생하는데, 이는 React가 테스트 환경에서 상태 업데이트의 완료를 보장받지 못했기 때문입니다. act()로 감싸면 상태 업데이트가 완료될 때까지 기다린 후 다음 assertion을 실행합니다.

비동기 훅 테스트

TypeScript
// hooks/useProduct.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { useProduct } from './useProduct';
 
describe('useProduct', () => {
  test('데이터를 성공적으로 불러온다', async () => {
    const mockProduct = { id: '1', name: '테스트 상품' };
 
    // Jest
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: async () => mockProduct,
    });
    // Vitest의 경우: vi.stubGlobal('fetch', vi.fn().mockResolvedValue(...))
 
    const { result } = renderHook(() => useProduct('1'));
 
    // 초기 상태
    expect(result.current.loading).toBe(true);
 
    // 데이터 로드 완료 대기
    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });
 
    expect(result.current.product).toEqual(mockProduct);
    expect(result.current.error).toBe(null);
  });
 
  test('에러를 처리한다', async () => {
    global.fetch = jest.fn().mockRejectedValue(new Error('네트워크 오류'));
 
    const { result } = renderHook(() => useProduct('1'));
 
    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });
 
    expect(result.current.error).toBe('네트워크 오류');
  });
});

3순위: UI 컴포넌트 테스트

재사용되는 작은 컴포넌트의 렌더링과 인터랙션을 테스트합니다.

테스트 대상

  • 공통 컴포넌트 (Button, Input, Card, Modal)
  • 폼 컴포넌트
  • 리스트 아이템 컴포넌트

테스트 관점

관점테스트 내용
렌더링props에 따라 올바르게 렌더링되는가
인터랙션클릭, 입력 등 이벤트가 올바르게 동작하는가
상태 변화disabled, loading 등 상태에 따른 변화
접근성role, aria 속성이 올바른가

Button 컴포넌트 테스트

TypeScript
// components/Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
 
describe('Button', () => {
  test('children이 렌더링된다', () => {
    render(<Button>클릭</Button>);
    expect(screen.getByRole('button', { name: '클릭' })).toBeInTheDocument();
  });
 
  test('클릭 시 onClick이 호출된다', async () => {
    const user = userEvent.setup();
    const handleClick = jest.fn();
 
    render(<Button onClick={handleClick}>클릭</Button>);
    await user.click(screen.getByRole('button'));
 
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
 
  test('disabled 상태에서는 클릭되지 않는다', async () => {
    const user = userEvent.setup();
    const handleClick = jest.fn();
 
    render(<Button onClick={handleClick} disabled>클릭</Button>);
    const button = screen.getByRole('button');
 
    expect(button).toBeDisabled();
    await user.click(button);
    expect(handleClick).not.toHaveBeenCalled();
  });
});

userEvent vs fireEvent

userEvent는 실제 사용자 동작을 시뮬레이션하여, 클릭 한 번에도 hover → mousedown → mouseup → click 순서로 이벤트가 발생합니다. 반면 fireEvent는 지정한 단일 이벤트만 발생시키므로 실제 브라우저 동작과 차이가 있습니다. 실제 사용자 경험에 가까운 테스트를 위해 userEvent를 권장하며, v14 이상에서는 userEvent.setup()으로 초기화한 후 사용합니다.

Input 컴포넌트 테스트

TypeScript
// components/Input.test.tsx
describe('Input', () => {
  test('label과 input이 연결되어 있다', () => {
    render(<Input id="email" label="이메일" />);
    expect(screen.getByLabelText('이메일')).toBeInTheDocument();
  });
 
  test('에러 메시지가 표시된다', () => {
    render(<Input id="email" error="필수 항목입니다" />);
 
    expect(screen.getByRole('alert')).toHaveTextContent('필수 항목입니다');
    expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'true');
  });
 
  test('사용자 입력이 onChange를 호출한다', async () => {
    const user = userEvent.setup();
    const handleChange = jest.fn();
 
    render(<Input id="email" onChange={handleChange} />);
    await user.type(screen.getByRole('textbox'), 'test');
 
    expect(handleChange).toHaveBeenCalled();
  });
});

폼 컴포넌트 테스트

폼 컴포넌트에서는 유효성 검증 로직이 핵심이므로, 다양한 입력 시나리오에서 올바른 에러 메시지가 표시되는지 검증해야 합니다.

TypeScript
// components/LoginForm.test.tsx
describe('LoginForm', () => {
  test('빈 이메일로 제출하면 에러가 표시된다', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={jest.fn()} />);
 
    await user.click(screen.getByRole('button', { name: '로그인' }));
 
    expect(screen.getByText('이메일을 입력해주세요')).toBeInTheDocument();
  });
 
  test('유효한 값으로 제출하면 onSubmit이 호출된다', async () => {
    const user = userEvent.setup();
    const handleSubmit = jest.fn();
    render(<LoginForm onSubmit={handleSubmit} />);
 
    await user.type(screen.getByLabelText('이메일'), 'test@example.com');
    await user.type(screen.getByLabelText('비밀번호'), 'password123');
    await user.click(screen.getByRole('button', { name: '로그인' }));
 
    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123',
    });
  });
});

4순위: Feature 컴포넌트 통합 테스트

큰 Feature 컴포넌트는 최소한의 통합 테스트만 수행하는 것이 좋습니다. 하위 컴포넌트와 훅은 이미 개별 테스트로 검증했으므로, Feature 레벨에서는 이들이 올바르게 조립되었는지만 확인하면 됩니다.

테스트 범위

  • 주요 섹션이 렌더링되는가
  • 로딩/에러/빈 상태가 올바르게 표시되는가
  • 하위 컴포넌트에 올바른 props가 전달되는가

모킹을 활용한 테스트

TypeScript
// features/product/ProductPage.test.tsx
import { render, screen } from '@testing-library/react';
import { ProductPage } from './ProductPage';
import { useProduct } from './hooks/useProduct';
import { ProductInfo } from './components/ProductInfo';
import { ReviewList } from './components/ReviewList';
 
jest.mock('./hooks/useProduct');
jest.mock('./components/ProductInfo');
jest.mock('./components/ReviewList');
 
const mockUseProduct = useProduct as jest.MockedFunction<typeof useProduct>;
 
describe('ProductPage', () => {
  beforeEach(() => {
    (ProductInfo as jest.Mock).mockImplementation(({ product }) => (
      <div>ProductInfo: {product.name}</div>
    ));
    (ReviewList as jest.Mock).mockImplementation(() => <div>ReviewList</div>);
  });
 
  test('로딩 상태를 표시한다', () => {
    mockUseProduct.mockReturnValue({ loading: true, product: null, error: null });
    render(<ProductPage productId="1" />);
    expect(screen.getByText('로딩중...')).toBeInTheDocument();
  });
 
  test('에러 상태를 표시한다', () => {
    mockUseProduct.mockReturnValue({
      loading: false,
      product: null,
      error: '불러올 수 없습니다',
    });
    render(<ProductPage productId="1" />);
    expect(screen.getByRole('alert')).toBeInTheDocument();
  });
 
  test('주요 섹션이 렌더링된다', () => {
    mockUseProduct.mockReturnValue({
      loading: false,
      product: { id: '1', name: '테스트 상품' },
      error: null,
    });
    render(<ProductPage productId="1" />);
 
    expect(screen.getByText('ProductInfo: 테스트 상품')).toBeInTheDocument();
    expect(screen.getByText('ReviewList')).toBeInTheDocument();
  });
});

Feature 테스트는 최소한으로

Feature 컴포넌트는 변경이 잦아서, 세부적인 테스트를 많이 작성하면 리팩토링할 때마다 테스트도 함께 수정해야 하는 부담이 생깁니다. 따라서 "로딩/에러/정상 상태가 올바르게 표시되는가" 정도의 스모크 테스트만 유지하는 것이 효율적입니다.

테스트 작성 판단 기준

반드시 테스트해야 할 것

대상이유
비즈니스 로직버그 발생 시 직접적인 비즈니스 영향
조건문이 많은 함수분기별 동작 검증 필요
재사용 유틸/훅/컴포넌트여러 곳에서 사용되어 영향 범위 큼
외부 API 호출 로직네트워크 실패, 에러 처리 검증
폼 유효성 검증사용자 입력 검증의 정확성
접근성 관련 속성스크린 리더 등 보조 기술 지원

선택적으로 테스트

대상판단 기준
단순 UI 컴포넌트스타일링 위주라면 스킵 가능
자주 변경되는 UI테스트 유지보수 비용 고려
프로토타입 단계 코드안정화된 후 추가

테스트하지 않아도 되는 것

대상이유
외부 라이브러리이미 테스트됨
단순 props 전달 래퍼로직이 없음
CSS/스타일시각적 회귀 테스트는 별도 도구 사용

Testing Library 쿼리 가이드

Testing Library의 쿼리는 사용자가 실제로 요소를 찾는 방식과 유사한 순서로 우선순위가 정해져 있습니다.

TypeScript
// 1순위: getByRole (접근성 트리 기반, 가장 권장)
screen.getByRole('button', { name: '제출' });
screen.getByRole('textbox', { name: '이메일' });
screen.getByRole('heading', { level: 1 });
 
// 2순위: getByLabelText (폼 요소)
screen.getByLabelText('비밀번호');
 
// 3순위: getByPlaceholderText
screen.getByPlaceholderText('검색어를 입력하세요');
 
// 4순위: getByText (일반 텍스트)
screen.getByText('로그인');
screen.getByText(/환영합니다/i);  // 정규식 사용 가능
 
// 5순위: getByTestId (최후의 수단)
screen.getByTestId('custom-element');

getByRole을 우선 사용하세요

getByRole은 사용자가 요소를 인식하는 방식(버튼, 입력창, 제목 등)과 가장 유사하며, 테스트 과정에서 접근성 문제까지 함께 검증할 수 있다는 장점이 있습니다. 만약 getByRole로 요소를 찾을 수 없다면, 해당 요소에 적절한 role이나 접근성 속성이 누락되었을 가능성이 높습니다.

쿼리 변형

접두사용도없을 때
getBy요소가 있다고 확신할 때에러 발생
queryBy요소가 없음을 확인할 때null 반환
findBy비동기로 나타나는 요소Promise 반환
TypeScript
// 요소가 있어야 할 때
expect(screen.getByRole('button')).toBeInTheDocument();
 
// 요소가 없어야 할 때
expect(screen.queryByRole('button')).not.toBeInTheDocument();
 
// 비동기로 나타날 때
await waitFor(() => {
  expect(screen.getByText('로딩 완료')).toBeInTheDocument();
});
// 또는
const element = await screen.findByText('로딩 완료');

자주 사용하는 Matcher

TypeScript
// 존재 여부
expect(element).toBeInTheDocument();
expect(element).not.toBeInTheDocument();
 
// 텍스트
expect(element).toHaveTextContent('안녕하세요');
expect(element).toHaveTextContent(/안녕/);
 
// 폼 요소
expect(input).toHaveValue('test@example.com');
expect(checkbox).toBeChecked();
expect(button).toBeDisabled();
expect(button).toBeEnabled();
 
// 속성
expect(element).toHaveAttribute('type', 'submit');
expect(element).toHaveAttribute('aria-invalid', 'true');
 
// CSS 클래스
expect(element).toHaveClass('active');
expect(element).toHaveClass('btn', 'btn-primary');
 
// 함수 호출
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith({ id: '1' });
expect(mockFn).toHaveBeenLastCalledWith('마지막 인자');

테스트 작성 베스트 프랙티스

사용자 관점에서 테스트

TypeScript
// ❌ 내부 상태를 직접 검사
expect(component.state.isOpen).toBe(true);
expect(wrapper.instance().handleClick).toHaveBeenCalled();

하나의 테스트는 하나의 동작만

TypeScript
// ❌ 여러 동작을 하나의 테스트에
test('장바구니 기능', () => {
  // 추가, 수량 변경, 삭제, 총액 계산 모두 테스트
  // 실패 시 어떤 동작이 문제인지 알기 어려움
});

테스트 제목은 명확하게

TypeScript
// ❌ 무엇을 테스트하는지 불분명
test('버튼 테스트', () => {});
test('useCart', () => {});
test('정상 동작', () => {});

AAA 패턴 따르기

TypeScript
test('상품을 장바구니에 추가하면 총액이 업데이트된다', () => {
  // Arrange (준비)
  const { result } = renderHook(() => useCart());
  const product = { id: '1', name: '상품', price: 10000 };
 
  // Act (실행)
  act(() => {
    result.current.addItem(product);
  });
 
  // Assert (검증)
  expect(result.current.totalPrice).toBe(10000);
});

정리

테스트 전략의 핵심은 코드를 테스트하기 쉬운 구조로 설계하는 것입니다. 1000줄짜리 컴포넌트를 통째로 테스트하려 하기보다, 작은 단위로 분리해서 각각 테스트하는 편이 훨씬 효율적입니다.

핵심 원칙

  1. 우선순위를 정하세요: 순수 함수 → 커스텀 훅 → UI 컴포넌트 → Feature 컴포넌트 순으로 테스트 가치가 높습니다. 비즈니스 로직을 순수 함수로 추출하면 가장 테스트하기 쉬우면서도 가장 가치 있는 테스트가 됩니다.

  2. getByRole을 우선 사용하세요: 사용자가 요소를 찾는 방식과 가장 유사할 뿐 아니라, 테스트 과정에서 접근성 문제까지 함께 검증할 수 있습니다.

  3. 사용자 관점에서 테스트하세요: 구현 세부사항(상태 값, 내부 메서드)이 아닌, 사용자가 실제로 보고 경험하는 결과를 검증해야 합니다.

  4. 100% 커버리지를 목표로 하지 마세요: 모든 코드를 테스트하려 하기보다, 버그가 발생할 가능성이 높은 곳에 의미 있는 테스트를 집중하는 것이 효과적입니다.

참고 자료

  • Testing Library - Guiding Principles
  • Testing Library - Query Priority
  • Kent C. Dodds - Testing Implementation Details
  • Jest 공식 문서
  • Vitest 공식 문서
목차
  • 테스트 가능한 구조
  • 테스트 우선순위 피라미드
  • 1순위: 순수 함수/유틸 테스트
    • 테스트 대상
    • 테스트 전략
    • 테스트 케이스 분류
  • 2순위: 커스텀 훅 테스트
    • 테스트 대상
    • 동기 훅 테스트
    • 비동기 훅 테스트
    • Input 컴포넌트 테스트
    • 폼 컴포넌트 테스트
  • 4순위: Feature 컴포넌트 통합 테스트
    • 테스트 범위
    • 모킹을 활용한 테스트
  • 테스트 작성 판단 기준
    • 반드시 테스트해야 할 것
    • 선택적으로 테스트
    • 테스트하지 않아도 되는 것
  • Testing Library 쿼리 가이드
    • 쿼리 변형
  • 자주 사용하는 Matcher
  • 테스트 작성 베스트 프랙티스
    • 사용자 관점에서 테스트
    • 하나의 테스트는 하나의 동작만
    • 테스트 제목은 명확하게
    • AAA 패턴 따르기
  • 정리
  • 참고 자료