React를 이용해 웹을 개발하며 컴포넌트를 만들때 예상치 못한 에러로인해 추가적인 커밋과 PR을 경험한 적있다. 이번에 새롭게 참여한 프로젝트에서는 UI 테스팅 도구인 Storybook과 다른 테스팅 도구인 Testing-library/react를 사용하게되었다. 만드는 컴포넌트를 테스트하는 과정을 통해 발생 가능성이 있는 에러를 사전에 방지할 수 있기에 이번 프로젝트에서는 테스트 과정을 본격적으로 다룰 예정이다.
왜 React Testing-Library 인가?
React Testing Library는 React 컴포넌트를 테스트하기 위해 설계된 라이브러리 이며, 실사용 환경과 비슷한 환경을 인스턴스가 아닌 실제 DOM 노드를 사용해 테스트하므로 더욱 신뢰할 수 있는 환경을 제공한다.
또한 props나 state가 컴포넌트에서 변경되는지 테스트하는 대신 사용자의 입장에서 상호작용하는 동작을 테스트하도록 설계되었다. 유효한 사용자 인터페이스(접근 가능)를 구축하고 HTML 구성시 테스트 케이스를 준수할 수 있다.
사용법
이전에 Jest를 다룬 글에서 사용한 방법과 비슷하다.
우선 쿼리문을 이용해 테스트할 컴포넌트의 요소를 지정한다.
쿼리문은 Variant와 Queries를 합쳐 사용한다.
Variant | Queries | ||
getBy~ | 조건에 일치하는 DOM 요소를 한개 선택 (요소가 없으면 에러 발생) |
ByLabelText | label이 있는 input의 label내용으로 input을 선택 |
getAllBy~ | 조건에 일치하는 DOM 요소 여러개 선택 (요소가 없으면 에러 발생) |
ByPlaceholderTest | placeholder 값으로 input 및 textarea를 선택 |
queryBy~ | 조건에 일치하는 DOM 요소를 한개 선택 (요소가 없어도 에러가 발생하지 않는다) |
ByText | 엘리먼트가 갖고있는 텍스트 값으로 DOM 선택(정규식 가능) |
queryAllBy~ | 조건에 일치하는 DOM 요소 여러개 선택 (요소가 없어도 에러가 발생하지 않는다) |
ByAltText | 해당 alt 속성을 갖는 DOM 선택 |
findBy~ | 조건에 일치하는 DOM 요소를 한개 선택 (4500ms 내에 요소가 안나타나면 에러발생) |
ByTitle | 해당 title 속성을 갖는 DOM 혹은 SVG를 선택 |
findAllBy~ | 조건에 일치하는 DOM 요소 여러개 선택 (4500ms 내에 요소가 안나타나면 에러발생) |
ByDisplayValue | input, textarea, select가 갖는 현재 값을 갖는 엘리먼트 선택 |
ByRole | 특정 role을 갖는 엘리먼트 선택 | ||
ByTestId | 다른 방법으로 테스트할 수 없을때 사용, 특정 DOM에 테스트 id를 달아 사용 |
위의 표에 있는 Variant와 Queries를 이용해 테스트할 요소를 지정할 수 있다.
실사용 코드
이제 내가 실제 프로젝트에 사용한 모습을 살펴보자.
const TyleHeader = ({ width }: Props) => {
return (
<Wrapper>
<ClippedRect width={width}>
<Info>
<InfoContent>
{width < 600 ? (
<SelectBar data-testid="rowSelectBar">
{selectBarInfo.subscribe.map((v, idx) => (
<p key={idx}>{v}</p>
))}
</SelectBar>
) : null}
<InfoTop>
<NickName>
<p>김일반</p>
<FollowBtn>팔로우</FollowBtn>
</NickName>
{width < 1150 ? <ProfileImg data-testid="sProfileImg" /> : null}
</InfoTop>
<InfoBottom>
<BadgeArea>
<p>뱃지를 모아서</p>
<p> 나를 소개해요!</p>
</BadgeArea>
<DescriptionArea>
<Location>
<img src={placeMarkerPNG} alt="위치아이콘" />
<p>대한민국, 서울</p>
</Location>
<SNSLink>
<img src={linkPNG} alt="링크아이콘" />
<img src={instagramPNG} alt="인스타아이콘" />
{/* 추후 map으로 변경 */}
</SNSLink>
<Following>
<img src={PeoplePNG} alt="" />
following 10
</Following>
<Follower>
<img src={PeoplePNG} alt="" />
follower 200
</Follower>
<Email>Email : teridot@site.com</Email>
</DescriptionArea>
{width < 1150 ? <EditBtn data-testId="sEditBtn">나의 Tyle 수정하기</EditBtn> : null}
</InfoBottom>
</InfoContent>
{width > 1150 ? (
<ProfileImg data-testid="wProfileImg">
<EditBtn>나의 Tyle 수정하기</EditBtn>
</ProfileImg>
) : null}
</Info>
{width > 600 ? (
<SelectBar data-testid="colSelectBar">
{selectBarInfo.subscribe.map((v, idx) => (
<p key={idx}>{v}</p>
))}
</SelectBar>
) : null}
</ClippedRect>
</Wrapper>
);
};
해당 컴포넌트는 기능 구현이 안되있는 초기 단계이며 화면 너비에 따라 엘리먼트의 렌더링 유무를 테스트를 하겠다.
위에서 너비에 따른 조건은 크게 3가지 이다.
- 너비가 600 미만일 때
- 너비가 600이상 1150 미만일 때
- 너비가 1150 이상일 때
이 세 조건에 따라 테스트 코드를 작성하면 되겠다.
너비가 600 미만일 때
우선 너비가 600 미만인 경우에 렌더링 되는 컴포넌트는 <SelectBar data-testid="rowSelectBar">, <ProfileImg data-testid="sProfileImg" />, <EditBtn data-testId="sEditBtn"> 이다.
<ProfileImg data-testid="sProfileImg" />
<EditBtn data-testId="sEditBtn">나의 Tyle 수정하기</EditBtn>
<SelectBar data-testid="rowSelectBar">
{selectBarInfo.subscribe.map((v, idx) => (
<p key={idx}>{v}</p>
))}
</SelectBar>
그렇다면 위의 조건에 부합하는지 확인하는 테스트 코드를 작성해보자.
test("길이가 500인 경우", () => {
render(
<TyleHeader
width={500}
/>
);
const profileImg = screen.getByTestId("sProfileImg");
const sEditBtn = screen.getByTestId("sEditBtn");
const selectBar = screen.getByTestId("rowSelectBar");
expect(profileImg).toBeEmptyDOMElement();
expect(sEditBtn).toHaveTextContent("나의 Tyle 수정하기");
expect(selectBar).toBeInTheDocument();
})
너비가 600 이상 1150 미만일 때
두 번째로 너비가 600 이상 1150 미만인 경우를 살펴보자.이 경우에는 렌더링 되는 컴포넌트는 아래와 같다.
<ProfileImg data-testid="sProfileImg" />
<EditBtn data-testId="sEditBtn">나의 Tyle 수정하기</EditBtn>
<SelectBar data-testid="colSelectBar">
{selectBarInfo.subscribe.map((v, idx) => (
<p key={idx}>{v}</p>
))}
</SelectBar>
이제 테스트 코드를 작성해보자.
test("길이가 1100인 경우", () => {
render(
<TyleHeader
width={1100}
/>
);
const profileImg = screen.getByTestId("sProfileImg");
const selectBar = screen.getByTestId("colSelectBar");
const sEditBtn = screen.getByTestId("sEditBtn");
expect(profileImg).toBeEmptyDOMElement();
expect(selectBar).toBeInTheDocument();
expect(sEditBtn).toHaveTextContent("나의 Tyle 수정하기");
});
너비가 1150 이상일 때
마지막 경우에는 렌더링 되는 컴포넌트는 아래와 같다.
<ProfileImg data-testid="wProfileImg">
<EditBtn>나의 Tyle 수정하기</EditBtn>
</ProfileImg>
<SelectBar data-testid="colSelectBar">
{selectBarInfo.subscribe.map((v, idx) => (
<p key={idx}>{v}</p>
))}
</SelectBar>
테스트 코드는 아래와 같다.
test("길이가 1200인 경우", () => {
render(
<TyleHeader
width={1200}
/>
);
const profileImg = screen.getByTestId("wProfileImg");
const selectBar = screen.getByTestId("colSelectBar");
expect(profileImg).toHaveTextContent("나의 Tyle 수정하기");
expect(selectBar).toBeInTheDocument();
});
각 상황에 맞는 matcher를 이용해 테스트를 진행했다.testid 사용은 지양해야 한다고 공식문서에 나와있다.그래서 testid 없이 테스트 할 수 있는 방법이 있다면 그 방법을 사용해 테스트를 진행했다.예를 들어 toHaveTextContent를 이용해 ProfileImg 컴포넌트 안에 "나의 Tyle 수정하기"라는 content가 있다는 테스트를 진행한 것처럼 testid를 이용하지 않고 테스트를 할 수 있다면 올바른 matcher를 이용해 테스트를 진행했다.아래의 링크는 다양한 matcher를 소개하는 공식 문서의 일부분이다.https://github.com/testing-library/jest-dom/blob/main/README.md#custom-matchers
참고자료
https://github.com/testing-library/jest-dom/blob/main/README.md#jest-dom
'FE 이모저모 공부' 카테고리의 다른 글
React-Query를 이용한 병렬 데이터 관리 (0) | 2024.02.18 |
---|---|
React-Query에 대하여 (0) | 2024.01.18 |
Emotion 가볍게 공부하기 (0) | 2024.01.05 |
컴포넌트 UI 테스트를 위한 StoryBook (0) | 2024.01.02 |
CSS 애니메이션과 JS 애니메이션 (0) | 2023.09.09 |