Vitest - React hook form 테스트 에러
📌상황
나를 너무나 고뇌하게 만들었던(…) react hook form을 이용한 Signin Component 테스트에 관한 내용을 작성해보고자 한다.😇 (a.k.a. 코드 한 줄이 불러온 에러 파티^^)
먼저 로그인 페이지를 만들기 위해 Signin Component를 만들었다. ID와 Password가 입력 되어야 하는 input 태그는 react hook form을 사용해 만들어 둔 Input Component를 활용했다.
사실 아래 Input Component에서 작성한 코드 한 줄로 인한 에러였지만, 아래 부분에는 해당 코드를 주석처리 해두었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React, { HTMLAttributes } from "react"
import { FieldValues, Path, UseFormRegister } from "react-hook-form"
import { FiSearch } from "react-icons/fi"
import { IoIosClose } from "react-icons/io"
interface InputProps<T extends FieldValues>
extends HTMLAttributes<HTMLDivElement> {
name: Path<T>
label?: string
placeholder?: string
register: ReturnType<UseFormRegister<T>>
error?: string
reset?: () => void
icon?: boolean
}
const Input = <T extends FieldValues>({
name,
label,
placeholder = "",
register,
error,
reset,
icon = false,
...rest
}: InputProps<T>) => (
<div className="h flex w-full flex-col gap-2" {...rest}>
{label && (
<label htmlFor={name} className="ml-1 text-sm font-medium text-white">
{label}
</label>
)}
<div
className={`flex w-full items-center gap-3 rounded-full bg-white px-6 py-[12px]`}
>
{icon && <FiSearch className="text-gray-800" />}
<input
id={name}
placeholder={placeholder}
type="text"
{...register}
// onChange={(e) => {
// console.log(e.target.value)
// }}
className="flex-1 bg-transparent outline-none placeholder:font-medium placeholder:text-gray-500"
/>
{reset && (
<button onClick={reset} type="button" data-testid={`reset-${name}`}>
<IoIosClose
// reset 함수 호출
className="cursor-pointer text-2xl text-gray-800"
/>
</button>
)}
</div>
{error && <p className="ml-1 text-xs text-red-500">{error}</p>}
</div>
)
export default Input
✅테스트를 시도하다.
테스트는 총 4가지를 시도했다. 하지만 모든 테스트에서 오류만 발생할 뿐 다음으로 진행되지 않았다…😇
1
2
3
4
5
6
7
8
9
10
11
12
13
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"
import { vi } from "vitest"
import SigninForm from "@/components/SigninForm"
describe("SigninForm 테스트", () => {
it("폼 제출 시 올바른 데이터를 수집한다", async () => {})
it("입력이 비어있을 때 에러 메시지가 표시된다", async () => {})
it("유효성 검사가 통과되지 않을 경우 에러 메세지가 표시된다", async () => {})
it("resetField 호출 시 입력값이 초기화된다", async () => {})
})
✅문제를 찾아보자
사실 npm run dev를 이용해 확인하면 외관상(?) 으로는 전혀 문제가 없었다. 사용자가 input에 입력을 하고 submit 하면 사용자가 작성한 id와 password 값 모두 정확히 받아오고 있었기에 원인은 더더욱 미궁으로 빠져들었으나..
dom을 확인하는 screen.debug()
가 생각나 input에 값을 입력하고 submit 하기 전을 기준으로 앞뒤 상황을 살펴보았을 때 힌트를 발견할 수 있었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"
import { vi } from "vitest"
import SigninForm from "@/components/SigninForm"
describe("SigninForm 테스트", () => {
it("폼 제출 시 올바른 데이터를 수집한다", async () => {
const onSubmit = vi.fn()
render(<SigninForm onSubmit={onSubmit} />)
const id = screen.getByPlaceholderText("아이디를 입력해주세요")
const password = screen.getByPlaceholderText("비밀번호를 입력해주세요")
const submitButton = screen.getByTestId("signin")
screen.debug()
// 입력값 변경
await act(async () => {
fireEvent.change(id, { target: { value: "id@test.com" } })
fireEvent.change(password, { target: { value: "password1!" } })
})
await act(async () => {
fireEvent.click(submitButton)
})
screen.debug()
})
})
현재 내가 생각하는대로 작동하려면, id와 password 모두 유효성 검사를 통과하기 때문에 에러메시지가 발생해서는 안된다. 하지만 둘 다 ‘필수값입니다’ 라는 에러메세지 가 표시되어 있었고, 이를 통해 input의 값이 제대로 변경되지 않는다는 것을 알 수 있었다. 그래서 onChange에 문제가 생겼나… 라고 짐작하며 Input 컴포넌트를 찬찬히 살펴본 결과…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const Input = <T extends FieldValues>({
name,
label,
placeholder = "",
register,
error,
reset,
icon = false,
...rest
}: InputProps<T>) => (
//생략
<input
id={name}
placeholder={placeholder}
type="text"
{...register}
onChange={(e) => {
// 👈 여기 왜 있니...
console.log(e.target.value)
}}
className="flex-1 bg-transparent outline-none placeholder:font-medium placeholder:text-gray-500"
/>
//생략
)
export default Input
원인을 발견했다…. 🤦♀️ 바로 이 코드 사용자가 input에 입력할 때마다 제대로 값이 받아오는지 확인하기 위해 사용한 코드가 문제를 일으키는 것이었다.
1
2
3
4
onChange={(e) => {
// 👈 (😭)
console.log(e.target.value)
}}
🥺문제의 원인을 파고들어 보면…
react hook form의 register를 살펴보자. react hook form의 register
는 onChange, onBlur, name, ref 이렇게 4가지를 이미 사용하고 있다.
즉 내가 추가로 작성한 onChange가 react hook form측에서 적용되어야 할 onChange를 간섭하고 있었고, 때문에 제대로 input 값을 변경하지 못했던 것이다.😣
1
2
3
4
5
6
7
8
9
10
const { onChange, onBlur, name, ref } = register('firstName');
<input
onChange={onChange}
onBlur={onBlur}
name={name}
ref={ref}
/>
// 위와 동일하다.
<input {...register('firstName')} />
onChange는 디버깅을 위해 작성한 코드 일 뿐 실제 화면에는 전혀 필요없는 코드이기 때문에 해당 코드는 삭제해주었고, 테스트는 통과하는 것처럼 보였지만…! 또다시 에러를 마주하게 되었다 ^^;
그래도 드디어(!) 값이 들어오긴 했다는 에러메세지를 보고 조금 감격하고 다시 에러메시지를 살펴보았다.
✅ 두 번째 에러
제출은 올바르게 되었지만 문제는 이 데이터가 내가 예상한 데이터가 맞는지 대조하는 부분이었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"
import { vi } from "vitest"
import SigninForm from "@/components/SigninForm"
describe("SigninForm 테스트", () => {
it("폼 제출 시 올바른 데이터를 수집한다", async () => {
const onSubmit = vi.fn()
render(<SigninForm onSubmit={onSubmit} />)
const id = screen.getByPlaceholderText("아이디를 입력해주세요")
const password = screen.getByPlaceholderText("비밀번호를 입력해주세요")
const submitButton = screen.getByTestId("signin")
screen.debug() //DOM 상태 확인
// 입력값 변경
await act(async () => {
fireEvent.change(id, { target: { value: "id@test.com" } })
fireEvent.change(password, { target: { value: "password1!" } })
})
await act(async () => {
fireEvent.click(submitButton)
})
//on Submit이 제대로 작동한지 확인
await waitFor(() => {
//👈 에러 발생
expect(onSubmit).toHaveBeenCalledWith({
id: "id@test.com", // id 값 검증
password: "password1!", // password 값 검증
})
})
})
})
에러 메세지를 살펴보면 금방 어떤 것이 문제인지 알 수 있었다.
1st spy call:
Array [
Object {
"id": "id@test.com",
"password": "password1!",
},
+ SyntheticBaseEvent {
+ "_reactName": "onSubmit",
+ "_targetInst": null,
+ "bubbles": true,
(생략)
첫번째 Object{} 값으로 내가 원하는 값이 들어왔지만, 그 이후 SyntheticBaseEvent~ 등이 들어오면서 expect에는 오류가 발생하는 것이었다. 그래서 이 부분은 해당 Object{} 값만 비교하는 것으로 교체하여 테스트를 마무리할 수 있었다.😊
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
describe("SigninForm 테스트", () => {
it("폼 제출 시 올바른 데이터를 수집한다", async () => {
const onSubmit = vi.fn()
render(<SigninForm onSubmit={onSubmit} />)
const id = screen.getByPlaceholderText("아이디를 입력해주세요")
const password = screen.getByPlaceholderText("비밀번호를 입력해주세요")
const submitButton = screen.getByTestId("signin")
screen.debug() //DOM 상태 확인
// 입력값 변경
await act(async () => {
fireEvent.change(id, { target: { value: "id@test.com" } })
fireEvent.change(password, { target: { value: "password1!" } })
})
await act(async () => {
fireEvent.click(submitButton)
})
//on Submit이 제대로 작동한지 확인
await waitFor(() => {
// 첫 번째 인자를 가져와서 확인
const firstCallArgs = onSubmit.mock.calls[0][0]
expect(firstCallArgs).toEqual({
id: "id@test.com",
password: "password1!",
})
})
screen.debug()
})
})
🙋♀️ 마무리
사실 글로는 이 고민의 시간이 짧게 느껴지지만 정말 오랜 시간 헤맸고, 해결 방법은 너무 간단해서 이렇게 쓰기에는 조금 부끄럽지만, 해결하고자 노력했던 시간이 아깝게 느껴지진 않는다. 무엇보다 해결했다는 성취감이 더 크니까! 😎
앞으로 테스트에 익숙해져서 더 좋은 컴포넌트, 더 좋은 코드를 쓰고 싶다는 마음이 가득하다.🤗 파이팅하자!