웹 프론트엔드 테스트 코드를 작성할 때, 컴포넌트를 렌더링한 후에 필요한 요소를 어떻게 선택해와야 할지 고민해 보신 적이 있으신가요?
Testing Library는 웹 페이지와 사용자가 상호 작용하는 방식과 최대한 유사한 테스트를 작성하는 것을 원칙으로 하고 있습니다. 요소를 선택할 때도 사용자가 웹 페이지와 상호 작용하는 방식과 유사한 방식을 택할수록 신뢰할 수 있는 테스트를 작성할 수 있습니다.
이 글에서는 Testing Library를 활용해 테스트를 작성할 때 렌더링 된 요소를 잘 선택하는 방법을 소개합니다.
어떤 요소를 기준으로 검색할까?
요소를 찾기 위해서는 어떤 요소부터 검색을 시작할지 정해야 합니다. render
함수가 반환하는 view 객체를 기준으로 쿼리할 수도 있지만, Testing Library에서 제공하는 screen 객체를 기준으로 쿼리를 수행하는 것이 좋습니다. 요소가 body 밖에 렌더링되는 것이 아니라면, screen 객체를 사용하세요.
Screen 객체
Testing Library에서는 document.body
를 감싸서 확장한 screen
객체를 제공하고 있습니다. screen 객체를 활용하면 document.body
에서 사용할 수 있는 모든 쿼리를 사용할 수 있습니다.
1import { screen } from "@tesiting-library/react";
render
호출 시 반환되는 container 객체로 요소를 선택하는 것과 screen을 사용해 요소를 선택했을 때의 결과는 똑같습니다. 그럼에도 screen을 사용하는 것이 권장되는 이유는, render
를 발생시키는 방식과 요소를 선택하는 방식을 분리할 수 있기 때문입니다.
render
와 요소의 선택이 분리되면, 테스트 코드 내에서 render
를 발생시키는 부분이 변경되어도 요소를 선택하는 부분은 영향을 받지 않게 해서 테스트 코드의 변경을 최소화할 수 있습니다.
render의 역할
Testing Library는 render를 수행할 때 렌더링할 컴포넌트를 div 요소로 한 번 감싸 주는데, 이 div 요소를 container라고 부릅니다. render 함수를 호출했을 때 반환되는 객체의 container 프로퍼티가 이 container 요소입니다.
Testing Library에서 제공하는 render 함수를 사용해 특정 React Element를 렌더링하면, DOM 트리는 아래와 같이 구성됩니다.
1body2ㄴ div (container)3 ㄴ render의 인자로 넘어온 Element
render는 호출될 때마다 body 하위에 렌더링할 요소를 추가해서 렌더링합니다. 그래서 render를 여러번 호출하면 아래 캡처와 같이 body 하위에 여러개의 container div 요소가 존재할 수 있습니다.
1render(<HashtagList hashtagList={hashtags} maxHastagCount={3} />);2render(<HashtagList hashtagList={hashtags} maxHastagCount={3} />);3render(<HashtagList hashtagList={hashtags} maxHastagCount={3} />);45screen.debug();

만약 특정 container에 렌더링된 요소를 교체하고 싶다면, render가 반환하는 객체의 rerender 메서드를 호출하면 됩니다.
1const view = render(<HashtagList hashtagList={hashtags} maxHastagCount={3} />);2view.rerender(<HashtagList hashtagList={hashtags} maxHastagCount={4} />);3view.rerender(<HashtagList hashtagList={hashtags} maxHastagCount={5} />);

screen.debug()
테스트를 하다 보면, 렌더링 후에 어떤 DOM 구조를 가지고 있는지 확인하고 싶을 때가 있습니다. 그럴 때는 screen.debug()
를 호출해 주면 해당 시점에 렌더링된 DOM을 테스트 결과에 출력해 줍니다. 아래 캡처는 컴포넌트 렌더링 후 screen.debug()
를 호출했을 때의 테스트 결과 화면의 일부입니다.
어떤 함수를 사용할까?
Testing Library에서는 크게 세 종류의 쿼리 API를 제공합니다. 각 쿼리 API는 아래와 같은 특징이 있습니다.
구분 | getBy… | queryBy… | findBy… |
---|---|---|---|
요소 발견 시 반환값 | 찾은 Element | 찾은 Element | 찾은 Element를 resolve하는 Promise |
해당되는 요소가 없다면 | 에러 발생 | null 반환 | Promise를 reject |
권장되는 사용법 | 일반적인 경우 | 요소가 존재하지 않는 것을 확인하고 싶은 경우 | waitFor를 사용해서 요소를 찾고 싶은 경우 |
일반적인 경우에는 getBy…
를 사용하면 됩니다.
만약 어떤 요소가 DOM 상에 존재하지 않는다는 것을 확인해야 한다면, queryBy…
를 아래와 같은 방식으로 사용할 수 있습니다.
1expect(screen.queryByText("텍스트")).not.toBeInTheDocument();
findBy…
는 timeout으로 설정된 시간동안 요소를 찾으려고 시도합니다. findBy…
를 호출하면 시간 안에 요소를 찾으면 해당 요소를 resolve하고, 찾지 못했다면 reject하는 Promise를 반환합니다. timeout 시간은 기본적으로 1000ms로 설정되어 있습니다. 컴포넌트 렌더링 후 바로 렌더링되지 않을 수도 있는 요소를 찾아야 한다면 findBy…
를 사용하세요.
요소의 어떤 속성으로 찾을까?
이제 요소가 가진 속성을 활용해 스크린 안에서 요소를 찾아야 합니다. 이때 가장 권장되는 방법은 접근성이나 시맨틱과 관련된 속성을 활용하는 것입니다.
- 접근성 - 유저가 시각적으로 볼 수 있거나 스크린 리더를 통해 접근할 수 있는 방식으로 선택하기
getByRole
- accessibility tree 상의 모든 요소에 사용 가능. role attribute를 주거나, 해당 태그의 role을 사용하기getByLabelText
- form 필드에서 주로 사용getByPlaceholderText
getByText
- 상호작용이 없는 요소를 찾는 데 사용 가능getByDisplayValue
- 시맨틱 - HTML5, ARIA 속성 사용하기
getByAltText
getByTitle
- Test ID
getByTestId
- 사용자가 볼 수 없는 값이므로, 최후의 수단으로 사용
이런 속성을 활용해서 요소를 선택하기 위해서 마크업 시 웹 표준을 준수하는 것이 좋습니다. 적절한 ARIA 속성과 시맨틱 태그를 활용해 마크업하면 좋은 사용자 경험을 제공할 수 있을 뿐만 아니라 테스트 작성에도 큰 도움이 됩니다.
접근성이나 시맨틱과 관련된 속성을 주기 어려운 요소를 선택해야 한다면, data-testid 속성을 활용할 수 있습니다. 이 속성이 있는 요소는 getByTestId
메서드로 요소를 선택할 수 있습니다. 하지만 data-testid 속성은 사용자가 상호작용을 하기 위해 사용되는 값은 아니기 때문에, data-testid를 사용하는 것은 최후의 수단으로 남겨두는 게 좋습니다.