Query 사용 우선순위
이 전 글들에서는 getByTestId 쿼리를 사용해 테스트를 진행했지만 사실 getByTestId는 testing-library에서 우선순위가 가장 낮은 쿼리입니다. 때문에 testing-library에서 권장하는 Query들의 우선순위를 살펴보고 가겠습니다.
기본 원칙에 따라 테스트는 사용자가 코드(컴포넌트, 페이지 등)와 상호 작용하는 방식과 최대한 유사해야 합니다. 이를 염두에 두고 다음과 같은 우선순위를 권장합니다:
1. 모든 사용자가 접근할 수 있는 쿼리(Queries Accessible to Everyone)
Query 사용 우선순위가 가장 높은 것은 시각/마우스 사용자뿐만 아니라 보조 기술을 사용하는 사용자의 경험을 반영하는 쿼리입니다.
- getByRole: 이 쿼리는 접근성 트리에 노출된 모든 요소를 쿼리 하는 데 사용할 수 있습니다. name 옵션을 사용하면 접근 가능한 이름을 기준으로 반환된 요소를 필터링할 수 있습니다. 거의 모든 항목에서 이 기능을 가장 우선적으로 사용해야 합니다. 대부분 다음과 같이 이름 옵션과 함께 사용됩니다. roles 목록을 확인해주세요
<button onClick={colorChangeBtn} style={{ backgroundColor: "red" }}>
버튼
</button>
... test code ...
const ButtonElement = screen.getByRole('button', {name: '버튼'})
- getByLabelText: 이 쿼리는 label과 연결된 input 태그를 찾아줍니다. 그래서 input과 연결되지 않은 label의 text를 사용하면 요소를 찾지 못합니다. 이 방법은 form 필드에 매우 유용합니다. 웹사이트의 form을 탐색할 때 사용자는 label 텍스트를 사용하여 요소를 찾습니다. 이 방법은 form 동작을 모방하므로 가장 선호하는 방법이어야 합니다.
- getByPlaceholderText: placeholder는 label을 대체할 수 없습니다. 하지만 이것이 전부라면 다른 대안보다는 낫습니다.
- getByText: form 이외의 텍스트 콘텐츠는 사용자가 요소를 찾는 주요 방법입니다. 이 방법은 div, span, paragraphs와 같은 비대화형 요소를 찾는 데 사용할 수 있습니다.
- getByDisplayValue: 양식 요소의 현재 값은 값이 채워진 페이지를 탐색할 때 유용할 수 있습니다.
<input type="text" id="lastName" /> // value = 'Norris'
<textarea id="messageTextArea" /> // value = 'Hello World'
<select>
<option value="">State</option>
<option value="AL">Alabama</option>
<option selected value="AK">Alaska</option>
<option value="AZ">Arizona</option>
</select>
... test code ...
const lastNameInput = screen.getByDisplayValue('Norris')
const messageTextArea = screen.getByDisplayValue('Hello World')
const selectElement = screen.getByDisplayValue('Alaska')
위 코드처럼 getByDisplayValue을 사용하면 input, textarea, select에 채워진 값들을 기반으로 Element를 쉽게 찾을 수 있습니다.
2. Semantic Queries
두 번째로 운선순위가 높은 쿼리는 HTML5 및 ARIA(접근성) selector입니다. 이러한 속성과 상호 작용하는 사용자 경험은 브라우저와 보조 기술에 따라 크게 달라지므로 주의해야 됩니다.
- getByAltText: alt 텍스트(img, area, input 및 모든 사용자 정의 요소)를 지원하는 요소인 경우 이를 사용하여 해당 요소를 찾을 수 있습니다.
- getByTitle: title 속성은 화면 리더에서 일관되게 읽히지 않으며 시각 장애가 있는 사용자에게는 기본적으로 표시되지 않습니다.
3. Test IDs
- getByTestId: 사용자는 이러한 내용을 보거나 들을 수 없으므로 역할이나 텍스트별로 일치시킬 수 없거나 의미가 없는 경우(예: 텍스트가 동적인 경우)에만 이 방법을 사용하는 것이 좋습니다.
fireEvent사용보다는 userEvent 사용하기
userEvent는 fireEvent를 사용해서 만들어졌습니다. userEvent의 내부 코드를 보면 fireEvent를 사용하면서 엘리먼트의 타입에 따라서 Label을 클릭했을 때, checkbox, radio 을 클릭 했을 때 그 엘리먼트 타입에 맞는 더욱 적절한 반응을 보여줍니다. 예를 들어서 fireEvent로 버튼을 클릭하면 버튼이 focus 되지 않습니다. 하지만 userEvent로 클릭하면 버튼이 focus 가 됩니다. 이렇게 실제 사용하는 유저가 보기에 실제 버튼을 클릭하는 행위가 더 잘 표현되기에 userEvent를 사용하는 게 더 추천되는 방법입니다.
userEvent 소개
userEvent는 사용자가 브라우저와 상호작용할 때 브라우저에서 발생하는 실제 이벤트를 시뮬레이션하려고 시도합니다. 예를 들어 userEvent.click(checkbox)는 체크박스의 상태를 변경합니다.
fireEvent와 차이점
fireEvent는 DOM 이벤트를 전송하는 반면, user-event는 전체 상호작용을 시뮬레이션하여 여러 이벤트를 전송하고 도중에 추가 검사를 수행할 수 있습니다.
테스트 라이브러리에 내장된 fireEvent는 브라우저의 저수준 dispatchEvent API를 가볍게 감싸는 래퍼로, 개발자가 모든 요소에서 모든 이벤트를 트리거할 수 있도록 합니다. 문제는 브라우저가 일반적으로 하나의 상호작용에 대해 하나의 이벤트를 트리거하는 것 이상을 수행한다는 것입니다. 예를 들어 사용자가 텍스트 상자에 입력하면 요소에 포커스를 맞춘 다음 키보드 및 입력 이벤트가 발생하고 사용자가 입력할 때 요소의 선택 항목과 값을 조작해야 합니다.
userEvent를 사용하면 구체적인 이벤트를 작성하는 대신 사용자와 상호 작용 하는 것처럼 DOM을 조작합니다. 예를 들어 사용자가 숨겨진 요소를 클릭하거나 비활성화된 텍스트 상자에 입력하는 것을 허용하지 않는다는 점을 고려해야 됩니다. 그렇기 때문에 사용자 이벤트를 사용하여 컴포넌트와의 상호작용을 테스트해야 합니다.
이전 코드에서 Query 우선순위와 userEvent 적용해 보기
// counter.page.test.tsx
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Counter from "@/app/counter/page";
describe("Counter 페이지 테스트", () => {
const user = userEvent.setup();
it("카운터 페이지 렌더링", () => {
render(<Counter />);
const CounterElement = screen.getByRole("heading");
expect(CounterElement).toHaveTextContent("0");
});
it("마이너스 버튼", () => {
render(<Counter />);
const minusButton = screen.getByRole("button", { name: "-" });
expect(minusButton).toHaveTextContent("-");
});
it("플러스 버튼", () => {
render(<Counter />);
const plusButton = screen.getByRole("button", { name: "+" });
expect(plusButton).toHaveTextContent("+");
});
it("마이너스 버튼 클릭시, count -1 ", async () => {
render(<Counter />);
const minusButton = screen.getByRole("button", { name: "-" });
const CounterElement = screen.getByRole("heading");
await user.click(minusButton);
expect(CounterElement).toHaveTextContent("-1");
});
it("플러스 버튼 클릭시, count 1 ", async () => {
render(<Counter />);
const plusButton = screen.getByRole("button", { name: "+" });
const CounterElement = screen.getByRole("heading");
await user.click(plusButton);
expect(CounterElement).toHaveTextContent("1");
});
it("on/off 버튼 컬러는 파란색입니다. ", () => {
render(<Counter />);
const onOffButton = screen.getByRole("button", { name: "on/off" });
expect(onOffButton).toHaveStyle("background-color: blue");
});
it("on/off 버튼 클릭시 +,- 버튼 disabled 속성 변경 ", async () => {
render(<Counter />);
const onOffButton = screen.getByRole("button", { name: "on/off" });
const minusButton = screen.getByRole("button", { name: "-" });
const plusButton = screen.getByRole("button", { name: "+" });
await user.click(onOffButton);
expect(minusButton).toBeDisabled();
expect(plusButton).toBeDisabled();
});
});
// counter/page.tsx
"use client";
import { useState } from "react";
const CounterPage = () => {
const [count, setCount] = useState(0);
const [onOff, setOnOff] = useState(false);
return (
<div>
<h3>{count}</h3>
<button onClick={() => setCount((count) => count - 1)} disabled={onOff}>
-
</button>
<button onClick={() => setCount((count) => count + 1)} disabled={onOff}>
+
</button>
<button
style={{ backgroundColor: "blue" }}
onClick={() => setOnOff(!onOff)}
>
on/off
</button>
</div>
);
};
export default CounterPage;
Git Hub 코드
Merge branch 'feature/refact-conter-test-code' into develop · guswls1846/nextjs-test@cda660c
guswls1846 committed Sep 15, 2023
github.com
참고
'기억보단 기록을 > Next JS (App Router)' 카테고리의 다른 글
NextJS 테스트 코드 작성하기 - 간단한 앱 만들면서 테스트 코드 작성 해보기 (0) | 2023.09.14 |
---|---|
NextJS 테스트 코드 작성하기 - NextJS 테스트를 위한 모듈 설치 및 설정 (0) | 2023.09.13 |
NextJS 테스트 코드 작성하기 - 리액트 테스트에 대하여 (0) | 2023.09.13 |
[NextJS 13] Routing - Middleware (0) | 2023.06.26 |
[NextJS 13] Routing - Route Handlers & 활용방안 (0) | 2023.06.25 |