[ 살펴보기 ] React Testing Library - User Interactions
![[ 살펴보기 ] React Testing Library - User Interactions](https://cdn.hashnode.com/res/hashnode/image/upload/v1729611861727/c0fdf368-72ea-49d2-a772-f47c3cd47d92.jpeg)
지난 포스트에선 다양한 query를 통해 특정 element를 찾는 방법을 살펴보았다. 이번 포스트에선 user가 특정 button을 누르거나 input에 값을 입력하는 등과 같은 action을 어떻게 test하는지 살펴보자.
User의 action을 모방하기 위해 @testing-library/user-event package를 추가로 설치한다. @testing-library/user-event package는 선택한 element가 button일 때 click action을 모방하거나 input element일 때 value를 기입하는 것과 같이 다양한 action을 test할 수 있게 해준다.
npm install -D @testing-library/user-event
그리고 user action test를 위해 jest가 제공하는 mock function을 함께 사용한다. Mock function은 특정 함수의 내부 사항을 구현할 필요 없이 component가 필요로 하는 function type의 prop에 전달할 수 있고 mock function의 invoke 여부 역시 확인할 수 있기에 유용하게 활용할 수 있다.
Click test
User가 button을 click하는 동작에 대한 test code를 작성해보자. 테스트를 위한 component는 다음과 같다.
import React from "react";
type Props = {
label: string;
disabled?: boolean;
onButtonClick: () => void;
};
const Button = ({ label, disabled, onButtonClick }: Props) => {
const handleButtonClick = () => {
if (disabled) return;
onButtonClick();
};
return (
<button
disabled={disabled}
className="p-2 border-2 border-orange-300"
onClick={handleButtonClick}
>
{label}
</button>
);
};
export default Button;
위의 예제 코드에선 disabled property가 true이면 return을 통해 function block을 종료하며 onButtonClick property가 실행되지 않도록 한다. 위의 component 동작을 점검하기 위한 test code는 다음과 같다.
import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import Button from "@/components/buttons/base/Button";
test("Click Button", async () => {
const mockButtonClick = jest.fn();
render(<Button label="test btn" onButtonClick={mockButtonClick} />);
const testEl = screen.getByRole("button");
await userEvent.click(testEl);
expect(mockButtonClick).toHaveBeenCalled();
});
각 test case에서 mockButtonClick이라는 jest mock function을 생성하여 onButtonClick prop에 전달하고 있다.
그리고 render function을 통해 Button component를 render하고 role을 통해 button element를 찾는다. 그렇게 찾은 button element를 @testing-library/user-event package에서 제공하는 userEvent의 click method를 통해 해당 element의 click action을 테스트 할 수 있다.
마지막으로 우리가 onButtonClick에 전달했던 mock function이 정상적으로 실행되었는지 toHaveBeenCalled matcher를 통해 확인한다. 위의 테스트를 실행해보면 다음과 같이 test case가 정상적으로 통과하는 것을 확인할 수 있다.
PASS __test__/Button.test.tsx
√ Click Button (113 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.184 s, estimated 8 s
Ran all test suites.
이제 반대로 onButtonClick prop으로 전달한 mock function이 실행되지 않아야 되는 상황을 가정해보자. 위에서 살펴본 component 예제에서 disabled prop이 true일 때는 onButtonClick으로 전달한 function이 실행되지 않도록 했다. 그럴 때는 expect에서 사용할 수 있는 matcher 중 not을 추가로 사용할 수 있다.
test("Block Button Click", async () => {
const mockButtonClick = jest.fn();
render(<Button label="test btn" disabled onButtonClick={mockButtonClick} />);
const testEl = screen.getByRole("button");
await userEvent.click(testEl);
expect(mockButtonClick).not.toHaveBeenCalled();
});
위의 test case에선 mock function이 실행이 되지 않는 상황이 우리가 예상하는 동작이므로 expect(mockButtonClick).not.toHaveBeenCalled()와 같이 not을 추가해 mock function이 실행되지 않을 때만 test case가 성공했다고 여긴다.
Hover test
특정 element의 hover action이 발생 했을 때의 경우 역시 test code에서 테스트 할 수 있다. 테스트를 위한 component는 다음과 같다.
import React, { useState } from "react";
type Props = {
label: string;
disabled?: boolean;
onButtonClick: (val: string) => void;
};
const Button = ({ label, disabled, onButtonClick }: Props) => {
const [isMouseOn, setIsMouseOn] = useState(false);
const handleButtonClick = () => {
if (disabled) return;
onButtonClick("");
};
const handleMouseOn = () => {
setIsMouseOn(true);
};
const handleMouseOff = () => {
setIsMouseOn(false);
};
return (
<div>
<button
disabled={disabled}
className="p-2 border-2 border-orange-300"
onClick={handleButtonClick}
onMouseOver={handleMouseOn}
onMouseLeave={handleMouseOff}
>
{label}
</button>
{isMouseOn && <span data-testid="test-info">test info</span>}
</div>
);
};
export default Button;
위의 component에선 button에 mouse hover가 발생하면 span element가 화면에 render되고 mouse가 button에서 벗어나면 span이 unmount된다. Component의 동작을 검사하기 위한 test code는 다음과 같다.
test("Button Hover", async () => {
render(<Button label="test22" onButtonClick={mockButtonClick} />);
const testEl = screen.getByRole("button");
await userEvent.hover(testEl);
expect(screen.getByTestId("test-info")).toBeInTheDocument();
await userEvent.unhover(testEl);
expect(screen.queryByTestId("test-info")).not.toBeInTheDocument();
});
우선 Button component를 render하고 getByRole query를 통해 button element를 찾는다. 그리고 hover method를 통해 button element에 hover action을 발생시키고 getByTestId query를 통해 span element가 정상적으로 render되었는지 확인한다.
Input test
User가 input에 value를 기입하는 동작에 대한 test code를 작성해보자. 테스트를 위한 component는 다음과 같다.
import React, { useState } from "react";
const numberRegex = /^\d+$/;
const isValidNumber = (val: string) => {
return numberRegex.test(val);
};
const TextInput = () => {
const [postcode, setPostcode] = useState("");
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputVal = e.target.value;
if (!isValidNumber(inputVal)) return;
setPostcode(inputVal);
};
return (
<div>
<label htmlFor="postcode">Postcode Input</label>
<input id="postcode" value={postcode} onChange={handleInput} />
</div>
);
};
export default TextInput;
예제 input component는 state를 업데이트 할 때 number string만 허용해서 받으며 number string이 아닌 value를 기입하면 state를 empty string으로 초기화한다.
우선 정상적인 값을 기입했을 때를 검사하는 test code는 다음과 같다.
import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
test("Input Valid Value", async () => {
render(<TextInput />);
const testEl = screen.getByLabelText("Postcode Input");
await userEvent.type(testEl, "123");
expect(testEl).toHaveValue("123");
});
우선 TextInput을 render를 통해 render하고 getByLabelText query를 통해 Email Input text node를 가진 label element와 연동된 input element를 찾는다.
그리고 userEvent의 type method를 이용하면 input element의 특정한 값을 넣을 수 있다. 우리가 테스트 하는 component는 number 형식의 string만 받으므로 위의 테스트는 정상적으로 통과된다.
이제 component가 허용하지 않는 value를 기입하는 경우에 대한 test code를 작성해보자.
test("Input Invalid Value", async () => {
render(<TextInput />);
const testEl = screen.getByLabelText("Postcode Input");
await userEvent.type(testEl, "abc");
expect(testEl).toHaveValue("");
});
만약 input에 number string이 아닌 다른 값을 넣을 경우 input의 value는 empty string으로 초기화 되므로 위의 테스트 역시 정상적으로 통과된다.
Input clear test
Test code에서 input의 값을 추가할 수 있을 뿐만 아니라 input 값을 clear하는 경우 역시 test해볼 수 있다. 테스트를 위해 input component를 다음과 같이 업데이트 한다.
import React, { useState } from "react";
const numberRegex = /^\d+$/;
const isValidNumber = (val: string) => {
return numberRegex.test(val);
};
const TextInput = () => {
const [postcode, setPostcode] = useState("");
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputVal = e.target.value;
if (!isValidNumber(inputVal)) return;
setPostcode(inputVal);
};
const handleInputClear = () => {
setPostcode("");
};
return (
<div>
<label htmlFor="postcode">Postcode Input</label>
<input id="postcode" value={postcode} onChange={handleInput} />
<button onClick={handleInputClear}>Clear Input</button>
</div>
);
};
export default TextInput;
위의 예제에서 clear input button을 누르면 setEmail state이 empty string으로 초기화 된다. 위의 component 동작을 확인하기 위한 test code는 다음과 같다.
test("Clear Input Value", async () => {
render(<TextInput />);
const testEl = screen.getByLabelText("Postcode Input");
const buttonEl = screen.getByRole("button");
await userEvent.type(testEl, "123");
expect(testEl).toHaveValue("123");
await userEvent.click(buttonEl);
expect(testEl).toHaveValue("");
});
위의 test code에선 우선 TextInput component를 render하고 getByLabelText query를 통해 input element를 먼저 서치하고 다음에 getByRole을 통해 button element를 서치한다.
그런 다음 우선 123이라는 value를 type method를 통해 input value에 추가하고 value가 올바르게 들어갔는지 검사를 한 다음 button element에 click action을 발생시켜 input value가 초기화 되었는지 검사한다.
Select test
User가 select box를 통해 특정 값을 선택했을 때의 동작에 대한 test code를 작성해보자. 테스트를 위한 component는 다음과 같다.
import React from "react";
const Select = () => {
return (
<select data-testid="test-select">
<option value="apple">Option - Apple</option>
<option value="grape">Option - Grape</option>
<option value="mango">Option - Mango</option>
</select>
);
};
export default Select;
위의 예제에서 각 option을 선택하면 option element에 설정된 value가 select element의 value로 설정된다. Select element의 select action을 위한 test code에는 selectOptions method를 사용할 수 있다.
test("Select Value", async () => {
render(<Select />);
const testEl = screen.getByTestId<HTMLSelectElement>("test-select");
await userEvent.selectOptions(testEl, "Option - Apple");
expect(testEl).toHaveValue("apple");
});
Keyboard test
User가 shift나 enter와 같은 keyboard를 입력했을 때의 동작에 대한 test code를 작성해 보자. 테스트를 위한 component는 다음과 같다.
import React, { useState } from "react";
const numberRegex = /^\d+$/;
const isValidNumber = (val: string) => {
return numberRegex.test(val);
};
const TextInput = () => {
const [isCtrOn, setIsCtrOn] = useState(false);
const [postcode, setPostCode] = useState("");
const handleEmailInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputVal = e.target.value;
if (!isValidNumber(inputVal)) {
setPostCode("");
return;
}
setPostCode(inputVal);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.ctrlKey) {
setIsCtrOn(true);
}
};
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!e.ctrlKey) {
setIsCtrOn(false);
}
};
return (
<div>
<label htmlFor="postcode">Postcode Input</label>
<input
id="postcode"
value={postcode}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onChange={handleEmailInput}
/>
{isCtrOn && <span data-testid="ctr-on">Only visible when ctr is on</span>}
</div>
);
};
export default TextInput;
위의 예제 component는 input이 focus된 상태에서 키보드 control key를 눌리고 있을 때만 span element가 화면에 나타나고 그렇지 않을 때는 다시 사라지게 되어 있다.
우선 control key를 누르는 경우에 대한 test code를 작성해 보자.
import { fireEvent, render, screen } from "@testing-library/react";
import TextInput from "@/components/inputs/TextInput";
test("Render Span When Control On", async () => {
render(<TextInput />);
const testEl = screen.getByLabelText("Postcode Input");
fireEvent.keyDown(testEl, { ctrlKey: true });
expect(screen.getByTestId("ctr-on")).toBeInTheDocument();
fireEvent.keyUp(testEl, { ctrlKey: false });
expect(screen.queryByTestId("ctr-on")).not.toBeInTheDocument();
});
이번 test code에선 action을 모방하기 위해 fireEvent function을 사용하고 있다. keydown과 같은 action을 특정 element에서 listen해야 할 경우 fireEvent를 사용하면 편리하게 테스트할 수 있다.
우리가 테스트 하는 component는 input에 focus된 상태에서 control key를 눌렀을 때 span element가 화면에 보이고 그렇지 않은 상태에선 span element가 다시 사라지므로 fireEvent의 keydown와 keyup method를 통해 해당 동작을 테스트 하고 있다.
fireEvent.keyDown(testEl, { ctrlKey: true }); 부분에서 input element에서 keydown이 발생하고 control key가 active되는 상황을 테스트 하며 그 이후 data-testid=ctr-on 값을 가진 span element가 정상적으로 render되었는지 검사한다.
fireEvent.keyUp(testEl, { ctrlKey: false }); 부분에서 input element에서 keyup이 발생하고 control key가 release되는 상황을 테스트 하며 그 이후 data-testid=ctr-on 값을 가진 span element가 정상적으로 사라졌는지 검사한다.
span element가 사라진 경우를 검사할 때는 getByTestId가 아닌 queryByTestId를 통해 대상 element가 없어도 error가 나지 않도록 처리하고 있다.
![[ 살펴보기 ] RDB - Relationships](https://cdn.hashnode.com/res/hashnode/image/upload/v1739711556668/48dc9e84-a621-42aa-9c9f-5fc5c436f0ec.jpeg)
![[ 살펴보기 ] MySQL - Data types](https://cdn.hashnode.com/res/hashnode/image/upload/v1739593589113/530f8704-4d27-42c9-a451-bb5c63150b99.jpeg)
![[ 살펴보기 ] TypeORM - Transactions, Migration](https://cdn.hashnode.com/res/hashnode/image/upload/v1739106042581/980b8133-61d4-406a-a026-65be9c28eace.jpeg)
![[ 살펴보기 ] TypeORM - Relations](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666874402/b688bd0b-b6bb-4f43-87d8-c1b46b59f1b7.jpeg)
![[ 살펴보기 ] TypeORM - Basics](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666803591/bef5df17-7dc7-4123-ae55-004d5042df39.jpeg)