본문 바로가기
JavaScript/React

Jest와 React Test Library(RTL)

by curious week 2025. 3. 25.

Jest는 테스트 실행기 + 검증 도구

 

Jest

By ensuring your tests have unique global state, Jest can reliably run tests in parallel. To make things quick, Jest runs previously failed tests first and re-organizes runs based on how long test files take.

jestjs.io

React Testing Library (RTL)는 React 컴포넌트 렌더링 & DOM 쿼리 도구

 

Testing Library | Testing Library

Simple and complete testing utilities that encourage good testing practices

testing-library.com


Jest, React Testing Library (RTL)를 어디에 어떻게 사용하는지, 각 상황에 맞는 예제 코드


유틸 함수

✔ 대상: Jest
❌ 대상 아님: RTL

예제 대상: utils/price.ts

// price.ts
export function getDiscountPrice(price: number, discountRate: number): number {
  return price - price * discountRate;
}

Jest 테스트

// price.test.ts
import { getDiscountPrice } from './price';

test('할인 가격 계산', () => {
  expect(getDiscountPrice(10000, 0.1)).toBe(9000);
});

API 호출 함수 (service)

✔ 대상: Jest
❌ 대상 아님: RTL

예제 대상: auth.service.ts

export const loginApi = async ({ email, password }: { email: string; password: string }) => {
  const res = await fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify({ email, password }),
  });
  return res.json();
};

Jest 테스트 (Mock 사용)

// auth.service.test.ts
import { loginApi } from './auth.service';

global.fetch = vi.fn(() =>
  Promise.resolve({ json: () => Promise.resolve({ accessToken: 'abc' }) })
) as jest.Mock;

test('로그인 API 호출', async () => {
  const res = await loginApi({ email: 'a@b.com', password: '1234' });
  expect(res.accessToken).toBe('abc');
});

Custom Hook

✔ 대상: Jest (단순 로직)
🔄 대상: RTL (컴포넌트 안에서 동작 확인)

예제 대상: useCounter.ts

import { useState } from 'react';

export const useCounter = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount((c) => c + 1);
  return { count, increment };
};

Jest + RTL 테스트 (hook 전용)

// useCounter.test.tsx
import { renderHook, act } from '@testing-library/react';

import { useCounter } from './useCounter';

test('카운터 증가', () => {
  const { result } = renderHook(() => useCounter());
  act(() => {
    result.current.increment();
  });
  expect(result.current.count).toBe(1);
});

컴포넌트 렌더링

❌ 대상 아님: Jest
✔ 대상: RTL

예제 대상: Greeting.tsx

const Greeting = () => <h1>Hello, user!</h1>;
export default Greeting;

RTL 테스트

// Greeting.test.tsx
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

test('인사 메시지가 렌더링됨', () => {
  render(<Greeting />);
  expect(screen.getByText(/hello, user/i)).toBeInTheDocument();
});

사용자 입력/클릭

❌ 대상 아님: Jest
✔ 대상: RTL

예제 대상: Counter.tsx

export const Counter = () => {
  const [count, setCount] = useState(0);
  return (
    <>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </>
  );
};

RTL 테스트

import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './Counter';

test('버튼 클릭 시 카운트 증가', () => {
  render(<Counter />);
  fireEvent.click(screen.getByText(/증가/));
  expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});

전체 로그인/회원가입 흐름

🔄 Jest: 토큰 저장 등 단위 단위 처리
✔ RTL: 사용자가 실제로 로그인하는 흐름

예제 대상: Login.tsx

export const Login = ({ onLogin }: { onLogin: (token: string) => void }) => {
  const [email, setEmail] = useState('');
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        onLogin('mockToken');
      }}>
      <input
        type="email"
        placeholder="email"
        onChange={(e) => setEmail(e.target.value)}
        value={email}
      />
      <button type="submit">로그인</button>
    </form>
  );
};

RTL 테스트 (로그인 시 버튼 클릭 → 콜백 호출 확인)

import { render, screen, fireEvent } from '@testing-library/react';
import { Login } from './Login';

test('로그인 흐름', () => {
  const onLogin = vi.fn();
  render(<Login onLogin={onLogin} />);
  fireEvent.change(screen.getByPlaceholderText(/email/i), {
    target: { value: 'test@a.com' },
  });
  fireEvent.click(screen.getByText(/로그인/));
  expect(onLogin).toHaveBeenCalledWith('mockToken');
});

  1. 중간 난이도 예제: 로그인 컴포넌트에서 버튼 클릭 → API 요청 → 상태 변경 확인
  2. 고난이도 예제: 비동기 React Query 기반 커스텀 훅 + UI 렌더링 + 상태 갱신까지 포함

예제 1: 로그인 컴포넌트 (Jest + RTL 조합) — 중간 난이도

구조

components/
  └─ Login.tsx
services/
  └─ auth.service.ts
__tests__/
  └─ Login.test.tsx

auth.service.ts

export const loginApi = async (email: string, password: string) => {
  return Promise.resolve({ accessToken: 'abc-token' });
};

Login.tsx

import { useState } from 'react';
import { loginApi } from '../services/auth.service';

export const Login = ({ onLogin }: { onLogin: (token: string) => void }) => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = async () => {
    const res = await loginApi(email, password);
    onLogin(res.accessToken);
  };

  return (
    <div>
      <input
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        placeholder="Password"
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button onClick={handleLogin}>Login</button>
    </div>
  );
};

Login.test.tsx (Jest + RTL)

import { render, screen, fireEvent } from '@testing-library/react';
import { Login } from '../components/Login';
import * as auth from '../services/auth.service';

test('로그인 API 호출 후 토큰 수신', async () => {
  const mockLogin = vi.spyOn(auth, 'loginApi').mockResolvedValue({
    accessToken: 'test-token',
  });
  const onLogin = vi.fn();

  render(<Login onLogin={onLogin} />);

  fireEvent.change(screen.getByPlaceholderText(/email/i), {
    target: { value: 'test@email.com' },
  });
  fireEvent.change(screen.getByPlaceholderText(/password/i), {
    target: { value: '1234' },
  });
  fireEvent.click(screen.getByText(/login/i));

  // wait for async behavior
  await screen.findByText(/login/i);

  expect(mockLogin).toHaveBeenCalledWith('test@email.com', '1234');
  expect(onLogin).toHaveBeenCalledWith('test-token');
});

예제 2: React Query + Zustand + UI 렌더링 — 고난이도

시나리오

  • 서버에서 유저 정보 가져오기
  • Zustand에 저장
  • 유저 이름 렌더링
  • 버튼 클릭으로 상태 초기화

구조

features/
  └─ user/
     ├─ user.store.ts
     ├─ useUserQuery.ts
     └─ UserProfile.tsx
__tests__/
  └─ UserProfile.test.tsx

user.store.ts

import { create } from 'zustand';

type User = { name: string };
type State = {
  user: User | null;
  setUser: (u: User) => void;
  clear: () => void;
};

export const useUserStore = create<State>((set) => ({
  user: null,
  setUser: (u) => set({ user: u }),
  clear: () => set({ user: null }),
}));

useUserQuery.ts

import { useQuery } from '@tanstack/react-query';

export const useUserQuery = () =>
  useQuery({
    queryKey: ['user'],
    queryFn: async () => {
      const res = await fetch('/api/user');
      return res.json(); // { name: "Heeseong" }
    },
  });

UserProfile.tsx

import { useUserQuery } from './useUserQuery';
import { useUserStore } from './user.store';

export const UserProfile = () => {
  const { data } = useUserQuery();
  const user = useUserStore((s) => s.user);
  const setUser = useUserStore((s) => s.setUser);
  const clear = useUserStore((s) => s.clear);

  if (!user && data) setUser(data);

  return (
    <div>
      <h1>{user?.name || 'Unknown'}</h1>
      <button onClick={clear}>Logout</button>
    </div>
  );
};

UserProfile.test.tsx

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useUserStore } from '@/features/user/user.store';
import { UserProfile } from '@/features/user/UserProfile';

const queryClient = new QueryClient();

global.fetch = vi.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ name: 'Heeseong' }),
  })
) as any;

beforeEach(() => {
  useUserStore.getState().clear();
});

test('유저 이름을 불러오고 로그아웃 시 상태 초기화', async () => {
  render(
    <QueryClientProvider client={queryClient}>
      <UserProfile />
    </QueryClientProvider>
  );

  await waitFor(() => {
    expect(screen.getByText('Heeseong')).toBeInTheDocument();
  });

  fireEvent.click(screen.getByText(/logout/i));
  expect(screen.getByText('Unknown')).toBeInTheDocument();
});

Jest는 언제 사용하는가?

비즈니스 로직 / 유틸 함수 / API / 상태 변경을 테스트할 때

// utils/calc.ts
export const sum = (a: number, b: number) => a + b;
test('sum 함수 테스트', () => {
  expect(sum(2, 3)).toBe(5); // ✅ Jest만으로 충분
});

사용하는 기능

  • test(), describe(), expect()
  • jest.fn(), mockReturnValue()
  • beforeEach(), afterAll()
  • resolves, rejects (비동기 처리)

컴포넌트가 아닌 일반 함수/비동기 로직은 Jest만으로 테스트 가능합니다.


React Testing Library는 언제 사용하는가?

React 컴포넌트의 렌더링, 사용자 상호작용, UI 반응을 테스트할 때

render(<Button label="클릭" onClick={fn} />);
const button = screen.getByText("클릭");
fireEvent.click(button); // ✅ 사용자가 클릭하는 행위를 시뮬레이션

사용하는 기능

  • render() : 컴포넌트를 가상 DOM에 렌더링
  • screen.getByText(), getByRole() 등 쿼리 함수
  • fireEvent, userEvent로 이벤트 발생
  • waitFor, findByText()로 비동기 테스트

컴포넌트를 테스트할 땐 반드시 React Testing Library가 필요
대신 검증(expect)은 Jest를 사용 → 그래서 항상 두 도구를 같이 쓰는 것처럼 보이는 것

함수, 유틸 로직 Jest 단독 sum(), formatDate() 등
API 호출 테스트 (with MSW) Jest + mock 도구 fetchUser() 등
컴포넌트 렌더링 Jest + RTL render(<MyComponent />)
UI에 특정 텍스트가 있는지 Jest + RTL getByText("로그인")
버튼 클릭 이벤트 처리 Jest + RTL fireEvent.click(button)
비동기 렌더링 확인 Jest + RTL + findByText 로딩 후 "완료" 메시지 확인 등
// Jest + RTL 조합
test('버튼 클릭 시 텍스트 변경', () => {
  render(<MyButton />);
  fireEvent.click(screen.getByText('클릭')); // RTL
  expect(screen.getByText('완료')).toBeInTheDocument(); // Jest + RTL
});

1. Jest의 역할

Test Runner .test.js 또는 .spec.js 파일을 찾아 실행
Assertion Library expect() 구문으로 결과를 검증
Mocking 기능 의존성을 가짜로 만들어 테스트 가능하게 함

2. 기본 사용 구조

test('설명', () => {
  // 1. 준비 (Arrange)
  // 2. 실행 (Act)
  // 3. 검증 (Assert)
});

예시:

test('1 + 2는 3이다', () => {
  const result = 1 + 2;
  expect(result).toBe(3);
});

Jest 폴더

__폴더명__(예: __tests__, __mocks__, __snapshots__)처럼 언더스코어(__)로 감싼 폴더명은 특별한 의미가 있는 관례적인 구조

__tests__ ❌ 선택 코드와 테스트를 분리하고 싶을 때
__mocks__ 🔶 조건부 Jest의 자동 mock 기능을 쓸 경우
__snapshots__ ✅ 자동 생성 스냅샷 테스트를 사용할 경우에만

→ 반드시 써야 하는 건 아니고, 구조적 정리가 필요할 때 선택적으로 사용


3. 자주 쓰는 Jest 메서드 & 기능

test 또는 it

test('설명', () => {...});
it('설명', () => {...}); // 동일함

expect()

expect(실제값).matcher(예상값) 형태로 작성

.toBe(value) === 값 비교
.toEqual(obj) 객체 값 비교 (deep comparison)
.toContain(item) 배열 또는 문자열 포함 여부
.toHaveLength(n) 길이 검사
.toBeTruthy() / .toBeFalsy() 참/거짓 여부
.toBeNull() / .toBeUndefined() null 또는 undefined 여부
.toBeGreaterThan(n) 숫자 비교
.toMatch(regex) 문자열 정규표현식 비교
.toHaveBeenCalled() mock 함수 호출 여부
.toHaveBeenCalledTimes(n) 몇 번 호출됐는지
.toHaveBeenCalledWith(arg) 어떤 인자로 호출됐는지

4. 구조화 도우미

describe()

  • 관련 테스트들을 그룹화
describe('덧셈 함수', () => {
  test('1 + 1 = 2', () => { ... });
  test('0 + 0 = 0', () => { ... });
});

beforeEach / afterEach

  • 매 테스트 전후에 공통 동작 실행
beforeEach(() => {
  console.log('매 테스트마다 실행됨');
});

beforeAll / afterAll

  • 전체 테스트 한 번만 실행
beforeAll(() => {
  console.log('테스트 시작 전 한 번 실행');
});

5. Mock 관련 메서드

jest.fn()

  • 가짜 함수(mock 함수)를 생성
const mockFn = jest.fn();
mockFn('hello');
expect(mockFn).toHaveBeenCalledWith('hello');

jest.mock()

  • 외부 모듈 전체를 mock 처리
jest.mock('../api/userAPI'); // userAPI 모듈을 자동으로 mock 처리

mockReturnValue()

const mock = jest.fn().mockReturnValue(42);
expect(mock()).toBe(42);

mockResolvedValue() / mockRejectedValue()

  • 비동기 함수의 반환값도 설정 가능
const fetchUser = jest.fn().mockResolvedValue({ name: 'mars112' });
const failAPI = jest.fn().mockRejectedValue(new Error('실패!'));

6. 비동기 테스트

1) async/await 사용

test('비동기 함수가 값을 반환해야 한다', async () => {
  const fetchData = () => Promise.resolve(42);
  const result = await fetchData();
  expect(result).toBe(42);
});

2) .resolves / .rejects

test('resolves matcher 사용', () => {
  return expect(Promise.resolve('OK')).resolves.toBe('OK');
});

test('rejects matcher 사용', () => {
  return expect(Promise.reject('에러')).rejects.toBe('에러');
});

 

유틸 함수 테스트

// utils/math.ts
export const add = (a: number, b: number) => a + b;
// math.test.ts
import { add } from './math';

test('add는 두 수를 더한다', () => {
  expect(add(2, 3)).toBe(5);
});

컴포넌트 테스트 (React Testing Library 사용)

// Button.tsx
export const Button = ({ onClick }: { onClick: () => void }) => (
  <button onClick={onClick}>클릭</button>
);
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

test('버튼 클릭 시 함수가 호출됨', () => {
  const onClick = jest.fn();
  render(<Button onClick={onClick} />);
  fireEvent.click(screen.getByText('클릭'));
  expect(onClick).toHaveBeenCalledTimes(1);
});

8. Jest 설정 파일 (jest.config.ts)

export default {
  testEnvironment: 'jsdom', // 브라우저 환경
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1', // 절대경로 import 지원
  },
};

실무에 바로 쓸 수 있게 정리

1. setupTests.ts 구성

테스트 환경을 초기화하는 전역 설정 파일입니다.
보통 @testing-library/jest-dom 같은 확장 matcher를 불러오거나, 공통 mock을 여기에 정의해요.

파일 위치

src/
├── setupTests.ts   ← 여기에 설정

예시

// src/setupTests.ts
import '@testing-library/jest-dom'; // toBeInTheDocument 등 확장 matcher 지원

jest.config.ts에 등록 필요

// jest.config.ts
export default {
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
};

2. Mock Server 연동 테스트 (MSW 사용)

**Mock Service Worker (MSW)**를 사용하면 fetch, axios 요청을 가짜 서버로 처리 가능
실제 API가 없어도 테스트 가능하고, 비동기 테스트에서 매우 유용합니다.

설치

npm install msw --save-dev

구조

src/
├── mocks/
│   ├── handlers.ts
│   └── server.ts
├── setupTests.ts

handlers.ts

import { rest } from 'msw';

export const handlers = [
  rest.get('/api/user', (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({ name: 'mars112' }));
  }),
];

server.ts

import { setupServer } from 'msw/node';
import { handlers } from './handlers';

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

setupTests.ts

import '@testing-library/jest-dom';
import { server } from './mocks/server';

// 테스트 전체 전/후 훅 등록
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

테스트

import { render, screen } from '@testing-library/react';
import { useEffect, useState } from 'react';

const User = () => {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(data => setUser(data));
  }, []);
  return <div>{user ? user.name : '로딩 중...'}</div>;
};

test('유저 데이터를 렌더링한다', async () => {
  render(<User />);
  const name = await screen.findByText('mars112');
  expect(name).toBeInTheDocument();
});

3. Jest로 TDD

TDD(Test-Driven Development): “테스트 먼저 쓰고 → 코드를 작성해서 통과시킴”


이메일 유효성 검사 함수

Step 1: 테스트부터 작성 (email.test.ts)

import { validateEmail } from './email'; // 아직 없음

test('올바른 이메일은 통과해야 한다', () => {
  expect(validateEmail('abc@example.com')).toBe(true);
});

test('잘못된 이메일은 실패해야 한다', () => {
  expect(validateEmail('hello@wrong')).toBe(false);
  expect(validateEmail('no-at-symbol.com')).toBe(false);
});

테스트 실패 (validateEmail이 아직 없음)


Step 2: 함수 생성 & 테스트 통과시키기 (email.ts)

export const validateEmail = (email: string) => {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};

npm test → 통과됨!

jest test code 작성 순서

  1. 실패하는 테스트 작성
  2. 테스트를 통과할 최소한의 코드 작성
  3. 통과 후 리팩토링
  4. 기능 안정성과 의도 확인 가능

 

1. React Testing Library(RTL)의 역할

렌더링 도구 render()로 컴포넌트를 가상 DOM에 마운트
쿼리 함수 실제 사용자처럼 텍스트/레이블 등을 기준으로 DOM 요소 찾기
이벤트 시뮬레이션 fireEvent, userEvent로 클릭/입력/포커스 등 실행
비동기 처리 지원 findBy, waitFor로 비동기 렌더링 대응 가능

2. 기본 사용 구조

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('설명', () => {
  render(<컴포넌트 />);
  const 요소 = screen.getByText('텍스트');
  userEvent.click(요소);
  expect(요소).toBeInTheDocument();
});

3. 자주 쓰는 메서드 & 쿼리 함수

render()

  • 컴포넌트를 테스트 환경에서 가상 DOM에 마운트
render(<MyComponent />);

screen의 쿼리 함수들

getByText() 텍스트로 요소 찾기 (하나만 있을 때)
getByRole() 역할(버튼, heading 등)으로 찾기
getByLabelText() <label>과 연결된 input 찾기
getByPlaceholderText() placeholder로 찾기
getByTestId() data-testid 속성으로 찾기
queryBy... 요소가 없을 수도 있을 때
findBy... 비동기적으로 기다렸다가 찾기 (Promise)
expect(screen.getByText("제목")).toBeInTheDocument();

이벤트 도구

fireEvent

  • 기본 DOM 이벤트 (예: click, change)
fireEvent.click(screen.getByText('클릭'));

userEvent

  • 실제 사용자 행동에 가까운 고급 시뮬레이션
await userEvent.type(input, 'Hello');
await userEvent.click(button);

userEvent는 실제로 input에 포커스 → 타이핑 → blur 순으로 작동합니다.


4. 비동기 처리

test('로딩 후 데이터 표시', async () => {
  render(<UserProfile />);
  expect(screen.getByText('로딩 중...')).toBeInTheDocument();

  const name = await screen.findByText('mars112');
  expect(name).toBeInTheDocument();
});

추가 유틸: waitFor()

await waitFor(() => {
  expect(screen.getByText('완료')).toBeInTheDocument();
});

 

입력폼 테스트

test('입력값이 반영되는가', async () => {
  render(<input placeholder="이름 입력" />);
  const input = screen.getByPlaceholderText('이름 입력');
  await userEvent.type(input, 'mars112');
  expect(input).toHaveValue('mars112');
});

버튼 클릭 이벤트

test('버튼 클릭 시 텍스트 변경', async () => {
  render(<Button />);
  const btn = screen.getByText('시작');
  await userEvent.click(btn);
  expect(screen.getByText('완료')).toBeInTheDocument();
});

6. 테스트용 속성 - data-testid

CSS class, id 같은 스타일 속성이 아니라, 테스트 전용으로 쓰이는 속성

<div data-testid="user-name">mars112</div>
const name = screen.getByTestId('user-name');
expect(name).toHaveTextContent('mars112');

가능하면 getByText, getByRole 등 사용자 중심 접근 우선 사용 → getByTestId는 최후의 수단


7. 전역 설정 (setupTests.ts)

// setupTests.ts
import '@testing-library/jest-dom'; // 확장 matcher 제공 (toBeInTheDocument 등)

등록 위치 (jest.config.ts)

export default {
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
};

8. 확장 matcher (@testing-library/jest-dom)

expect(element).toBeInTheDocument();
expect(button).toBeDisabled();
expect(input).toHaveValue('mars112');
expect(link).toHaveAttribute('href', '/home');

이런 matcher는 jest-dom을 통해 사용!


1. 비동기 컴포넌트 테스트 실전 예제

컴포넌트: User.tsx

import { useEffect, useState } from 'react';

export const User = () => {
  const [name, setName] = useState('');
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(data => {
        setName(data.name);
        setLoading(false);
      });
  }, []);

  if (loading) return <p>로딩 중...</p>;

  return <h1>안녕하세요, {name}님</h1>;
};

테스트 코드: User.test.tsx (with MSW)

import { render, screen } from '@testing-library/react';
import { User } from './User';
import { rest } from 'msw';
import { setupServer } from 'msw/node';

// Mock server
const server = setupServer(
  rest.get('/api/user', (req, res, ctx) =>
    res(ctx.status(200), ctx.json({ name: 'mars112' }))
  )
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('사용자 데이터를 로딩 후 표시해야 함', async () => {
  render(<User />);
  expect(screen.getByText('로딩 중...')).toBeInTheDocument();

  const name = await screen.findByText('안녕하세요, mars112님');
  expect(name).toBeInTheDocument();
});

2. Form Validation 테스트

컴포넌트: LoginForm.tsx

import { useState } from 'react';

export const LoginForm = () => {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!email.includes('@')) {
      setError('올바른 이메일을 입력하세요');
    } else {
      setError('');
      alert('로그인 성공');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        placeholder="이메일"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type="submit">로그인</button>
      {error && <p>{error}</p>}
    </form>
  );
};

테스트 코드: LoginForm.test.tsx

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';

test('올바르지 않은 이메일 입력 시 오류 메시지 출력', async () => {
  render(<LoginForm />);
  const input = screen.getByPlaceholderText('이메일');
  const button = screen.getByText('로그인');

  await userEvent.type(input, 'wrong-email');
  await userEvent.click(button);

  expect(screen.getByText('올바른 이메일을 입력하세요')).toBeInTheDocument();
});

3. Mocking Axios 요청 테스트

컴포넌트: PostList.tsx

import { useEffect, useState } from 'react';
import axios from 'axios';

export const PostList = () => {
  const [posts, setPosts] = useState<string[]>([]);

  useEffect(() => {
    axios.get('/api/posts').then((res) => setPosts(res.data));
  }, []);

  return (
    <ul>
      {posts.map((title, i) => (
        <li key={i}>{title}</li>
      ))}
    </ul>
  );
};

테스트 코드: PostList.test.tsx

import { render, screen } from '@testing-library/react';
import { PostList } from './PostList';
import axios from 'axios';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

test('axios를 통해 글 목록을 가져와 렌더링', async () => {
  mockedAxios.get.mockResolvedValue({ data: ['글1', '글2'] });

  render(<PostList />);
  const items = await screen.findAllByRole('listitem');
  expect(items).toHaveLength(2);
  expect(items[0]).toHaveTextContent('글1');
});