
type)과, 잘못된 입력에 대해 경고하는 userEvent 객체가 아주 중요한 역할을 합니다.LoginForm)를 만든다고 가정해 봅시다.요구사항:
- 이메일과 비밀번호 입력창, 그리고 로그인 버튼이 있어야 한다.
- 사용자가 입력창에 글자를 치면, 그 값이 화면에 반영되어야 한다.
- 빈 값으로 로그인 버튼을 누르면 "이메일을 입력하세요" 또는 "비밀번호를 입력하세요" 에러 메시지가 떠야 한다.
- 입력이 올바르면 부모 컴포넌트가 전달해 준
onSubmit함수가 실행되어야 한다.
src/components/LoginForm.test.tsx 파일을 생성합니다.
이번 테스트에서는 사용자가 키보드를 한 글자씩 누르는 동작을 흉내 내기 위해 userEvent.type()을 사용합니다.
또한, 폼 제출이 성공했는지 확인하기 위해 vi.fn()을 활용합니다.import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { vi, describe, test, expect } from 'vitest';
// 아직 컴포넌트가 없으므로 import 에러가 날 수 있습니다.
import LoginForm from './LoginForm';
describe('LoginForm 컴포넌트 동작 테스트', () => {
test('이메일과 비밀번호를 입력하고 제출하면 onSubmit이 호출된다', async () => {
const user = userEvent.setup();
// 폼 제출 성공 여부를 확인하기 위한 가짜 함수(Mock Function)
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
const emailInput = screen.getByLabelText('이메일');
const passwordInput = screen.getByLabelText('비밀번호');
const submitBtn = screen.getByRole('button', { name: '로그인' });
console.log('[Step 1] 입력 전 상태 확인');
screen.debug(emailInput); // 입력창 상태만 따로 출력해 봅니다.
console.log('[Step 2] 텍스트 입력 시뮬레이션');
// 사용자가 타자를 치는 동작 (비동기)
await user.type(emailInput, 'test@example.com');
await user.type(passwordInput, 'password123');
// 입력값이 제대로 들어갔는지 검증 (value 속성 확인)
expect(emailInput).toHaveValue('test@example.com');
expect(passwordInput).toHaveValue('password123');
console.log('[Step 3] 로그인 버튼 클릭');
await user.click(submitBtn);
// handleSubmit 함수가 정확한 인자(입력한 값)와 함께 1번 호출되었는지 검증
expect(handleSubmit).toHaveBeenCalledTimes(1);
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
test('빈 값으로 제출하면 에러 메시지가 표시되고 onSubmit은 호출되지 않는다', async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
const submitBtn = screen.getByRole('button', { name: '로그인' });
console.log('[Step 4] 빈 값으로 제출 시도');
await user.click(submitBtn);
console.log('[Step 5] 에러 메시지 렌더링 확인');
screen.debug();
// 에러 메시지가 화면에 나타났는지 확인
expect(screen.getByText('이메일을 입력하세요')).toBeInTheDocument();
expect(screen.getByText('비밀번호를 입력하세요')).toBeInTheDocument();
// 에러가 났으므로 제출 함수는 호출되지 않아야 함
expect(handleSubmit).not.toHaveBeenCalled();
});
});npm test src/components/LoginForm.test.tsxLoginForm 컴포넌트가 없기 때문입니다.
이제 이 상세한 테스트 시나리오를 만족시키는 컴포넌트를 구현해 봅시다.src/components/LoginForm.tsx를 생성합니다.
입력값을 관리하기 위해 useState를 사용하고, 제출 시 검증 로직을 추가합니다.import { useState } from 'react';
interface LoginFormProps {
onSubmit: (data: { email: string; password: string }) => void;
}
export default function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// 에러 상태 관리 (초기값은 빈 객체)
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); // 폼의 기본 동작(새로고침) 방지
// 유효성 검사 로직
const newErrors: { email?: string; password?: string } = {};
if (!email) newErrors.email = '이메일을 입력하세요';
if (!password) newErrors.password = '비밀번호를 입력하세요';
// 에러가 하나라도 있으면 제출 중단
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
// 검사 통과 시 에러 초기화 및 데이터 전송
setErrors({});
onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<div>
{/* 라벨과 인풋을 연결하기 위해 htmlFor와 id를 일치시킵니다 */}
<label htmlFor="email">이메일</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{/* 에러가 있으면 빨간색 문구 표시 */}
{errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
</div>
<div>
<label htmlFor="password">비밀번호</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{errors.password && <span style={{ color: 'red' }}>{errors.password}</span>}
</div>
<button type="submit">로그인</button>
</form>
);
}npm test src/components/LoginForm.test.tsx
[Step 1] 입력 전 상태 확인
<input id="email" type="email" value="" />
[Step 2] 텍스트 입력 시뮬레이션
[Step 3] 로그인 버튼 클릭
[Step 4] 빈 값으로 제출 시도
[Step 5] 에러 메시지 렌더링 확인
<body>
...
<span style="color: red;">이메일을 입력하세요</span>
<span style="color: red;">비밀번호를 입력하세요</span>
...
</body>
PASS src/components/LoginForm.test.tsxinput의 value가 비어있습니다.user.type이 실행되면서 useState가 업데이트되고, input의 값도 test@example.com으로 변했습니다.errors 상태가 업데이트되면서 화면에 빨간색 에러 메시지가 렌더링 되었습니다.fireEvent.change를 많이 썼지만, 지금은 userEvent.type을 권장합니다.t -> e -> s -> t) 입력 이벤트를 발생시킵니다.
따라서 KeyDown, KeyUp 같은 이벤트도 모두 테스트할 수 있어 훨씬 정교합니다.git clone https://github.com/PROMLEE/my-tdd-app.git
cd my-tdd-app
git checkout part10userEvent.type을 사용하여 실제 사용자가 타이핑하는 것과 똑같은 환경을 만들었습니다.getByLabelText를 사용하기 위해 label 태그와 input 태그를 htmlFor-id로 연결했습니다. 이렇게 하면 시각 장애인을 위한 스크린 리더도 잘 동작하고, 테스트도 쉬워집니다.다음 시간에는11편. 테스트 관점에서 본 컴포넌트 구조를 통해, 테스트를 짜다 보니 발견하게 되는 "나쁜 컴포넌트 구조"와 이를 개선하는 방법을 알아보겠습니다.