Skip to content

Commit

Permalink
test: 회원가입 폼 및 유효성 검사 구현
Browse files Browse the repository at this point in the history
- SignupForm 컴포넌트 추가: 사용자 회원가입을 위한 입력 필드 및 제출 버튼 포함
- InputField 컴포넌트: 유효성 검사 및 오류 메시지 표시 기능 구현
- Zod를 사용한 폼 데이터 유효성 검사 추가
- 서버 오류 처리 및 사용자 피드백을 위한 Toast 메시지 추가
- Vitest를 사용한 테스트 코드 작성: 정상적인 폼 제출, 유효성 검사, 서버 오류 처리 테스트 포함
- 중복 코드 리팩토링: 입력 필드 및 제출 버튼 클릭 동작을 함수로 분리하여 재사용성 향상
  • Loading branch information
deipanema committed Oct 29, 2024
1 parent 1fe0785 commit 87fe545
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 3 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "vitest --passWithNoTests",
"test": "vitest",
"cy:open": "wait-on http://localhost:3000/ && cypress open",
"cy:run": "npm-run-all --parallel dev cy:open"
},
Expand Down
63 changes: 63 additions & 0 deletions src/app/(auth)/components/InputField.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, vi } from "vitest";
import { useForm } from "react-hook-form";
import { fireEvent, render, screen } from "@testing-library/react";

import InputField from "./InputField";

type TestFormValues = {
nickname: string;
};

describe("InputField Component", () => {
const TestInputField = () => {
const {
register,
trigger,
watch,
formState: { errors },
} = useForm<TestFormValues>();

return (
<InputField
id="nickname"
label="Nickname"
type="text"
placeholder="별명"
register={register}
errors={errors}
trigger={trigger}
watch={watch}
/>
);
};

it("InputField 렌더링 합니다.", () => {
render(<TestInputField />);
expect(screen.getByLabelText("Nickname")).toBeInTheDocument();
});

it("입력이 유효하지 않으면 오류 메시지를 표시합니다.", async () => {
const mockTrigger = vi.fn().mockResolvedValue(false);
render(<TestInputField />);

const input = screen.getByLabelText("Nickname") as HTMLInputElement;
fireEvent.blur(input);

await mockTrigger("nickname");
expect(mockTrigger).toHaveBeenCalledWith("nickname");

const errorElement = screen.getByRole("alert");
expect(errorElement).toBeInTheDocument();
});

it("blur() 되었을 때, InputField 유효성을 검사합니다.", async () => {
const mockTrigger = vi.fn().mockResolvedValue(true);
render(<TestInputField />);

const input = screen.getByLabelText("Nickname") as HTMLInputElement;
fireEvent.blur(input);

await mockTrigger("nickname");
expect(mockTrigger).toHaveBeenCalledWith("nickname");
});
});
4 changes: 3 additions & 1 deletion src/app/(auth)/components/InputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ export default function InputField<T extends FieldValues>({
aria-required="true"
onBlur={handleBlur} // 입력 필드에서 포커스가 벗어날 때 handleBlur를 호출합니다.
/>
<small className="mb-5 text-sm text-red-50">{errorText}</small>
<small role="alert" className="mb-5 text-sm text-red-50">
{errorText}
</small>
</>
);
}
157 changes: 157 additions & 0 deletions src/app/(auth)/components/SignupForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { render, screen, waitFor } from "@testing-library/react";
import { vi, Mock } from "vitest";
import userEvent from "@testing-library/user-event";

import { useSignup } from "@/hook/useSignup";

import SignupForm from "./SignupForm";

// Mock modules
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
}),
}));

vi.mock("@/hook/useSignup", () => ({
useSignup: vi.fn(),
}));

vi.mock("react-toastify", () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}));

describe("SignupForm", () => {
const mockMutate = vi.fn();

beforeEach(() => {
vi.clearAllMocks();
(useSignup as Mock).mockReturnValue({
mutate: mockMutate,
isLoading: false,
});
});

describe("렌더링 테스트", () => {
it("모든 입력 필드와 제출 버튼이 렌더링되어야 한다", () => {
render(<SignupForm />);

expect(screen.getByLabelText("닉네임")).toBeInTheDocument();
expect(screen.getByLabelText("아이디")).toBeInTheDocument();
expect(screen.getByLabelText("비밀번호")).toBeInTheDocument();
expect(screen.getByLabelText("비밀번호 확인")).toBeInTheDocument();
expect(screen.getByText("회원가입하기")).toBeInTheDocument();
});

it("페이지 로드시 닉네임 입력 필드에 포커스가 되어야 한다", () => {
render(<SignupForm />);

expect(screen.getByLabelText("닉네임")).toHaveFocus();
});
});

describe("유효성 검사", () => {
it("닉네임이 2자 미만일 경우 에러 메시지를 표시해야 한다", async () => {
render(<SignupForm />);
const nicknameInput = screen.getByLabelText("닉네임");

await userEvent.type(nicknameInput, "a");
await userEvent.tab();

expect(await screen.findByText("닉네임은 최소 2자 이상이어야 합니다.")).toBeInTheDocument();
});

it("이메일 형식이 잘못된 경우 에러 메시지를 표시해야 한다", async () => {
render(<SignupForm />);
const emailInput = screen.getByLabelText("아이디");

await userEvent.type(emailInput, "invalid-email");
await userEvent.tab();

expect(await screen.findByText("유효한 이메일 주소를 입력해 주세요.")).toBeInTheDocument();
});

it("비밀번호가 8자 미만일 경우 에러 메시지를 표시해야 한다", async () => {
render(<SignupForm />);
const passwordInput = screen.getByLabelText("비밀번호");

await userEvent.type(passwordInput, "1234567");
await userEvent.tab();

expect(await screen.findByText("비밀번호는 최소 8자 이상이어야 합니다.")).toBeInTheDocument();
});

it("비밀번호와 비밀번호 확인이 일치하지 않을 경우 에러 메시지를 표시해야 한다", async () => {
render(<SignupForm />);
const passwordInput = screen.getByLabelText("비밀번호");
const passwordConfirmInput = screen.getByLabelText("비밀번호 확인");

await userEvent.type(passwordInput, "password123");
await userEvent.type(passwordConfirmInput, "password456");
await userEvent.tab();

expect(await screen.findByText("비밀번호가 일치하지 않습니다.")).toBeInTheDocument();
});
});

describe("폼 제출", () => {
const validFormData = {
nickname: "유저",
email: "[email protected]",
password: "password123",
passwordConfirm: "password123",
};

it("유효한 데이터로 폼 제출시 회원가입 mutation이 호출되어야 한다", async () => {
render(<SignupForm />);

await userEvent.type(screen.getByLabelText("닉네임"), validFormData.nickname);
await userEvent.type(screen.getByLabelText("아이디"), validFormData.email);
await userEvent.type(screen.getByLabelText("비밀번호"), validFormData.password);
await userEvent.type(screen.getByLabelText("비밀번호 확인"), validFormData.passwordConfirm);

const submitButton = screen.getByText("회원가입하기");
await userEvent.click(submitButton);

await waitFor(() => {
expect(mockMutate).toHaveBeenCalledWith(
expect.objectContaining({
nickname: validFormData.nickname,
email: validFormData.email,
password: validFormData.password,
passwordConfirm: validFormData.passwordConfirm,
}),
expect.any(Object),
);
});
});

it("이미 사용 중인 이메일로 가입 시도시 에러 메시지를 표시해야 한다", async () => {
const mockError = {
isAxiosError: true,
response: {
status: 409,
},
};

mockMutate.mockImplementation((_, options) => {
options.onError(mockError);
});

render(<SignupForm />);

await userEvent.type(screen.getByLabelText("닉네임"), validFormData.nickname);
await userEvent.type(screen.getByLabelText("아이디"), validFormData.email);
await userEvent.type(screen.getByLabelText("비밀번호"), validFormData.password);
await userEvent.type(screen.getByLabelText("비밀번호 확인"), validFormData.passwordConfirm);

const submitButton = screen.getByText("회원가입하기");
await userEvent.click(submitButton);

expect(await screen.findByText("이미 사용 중인 이메일입니다.")).toBeInTheDocument();
});
});
});
1 change: 0 additions & 1 deletion src/hook/useSignup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { ErrorType } from "@/api/goalAPI";

export const useSignup = () => {
const router = useRouter();
console.log("1");

return useMutation({
mutationFn: signup,
Expand Down

0 comments on commit 87fe545

Please sign in to comment.