테스트를 작성해 보기 위해, 블로그 서비스를 개발하고 있는 상황을 가정해 보겠습니다. 지금 만들고 있는 컴포넌트는 아래와 같은 '포스트 카드' 컴포넌트입니다.
이 컴포넌트 안에서도 특히 기능과 정책이 포함된 영역이 있습니다. 바로 하단에 있는 해시태그 영역입니다.
이 영역에 대한 정책은 아래와 같습니다.
- 처음에는 최대 5개의 해시태그만 보여준다
- 5개보다 많은 해시태그가 있으면 ‘더보기’ 버튼 노출
- 더보기를 누르면 최대 10개의 해시태그를 보여주고, ‘더보기’ 버튼이 ‘접기’ 버튼으로 바뀜
- 해시태그를 누르면 해시태그 이름으로 검색어가 설정됨
- 해시태그에 마우스를 올리면 밑줄이 생기고, 글자색과 배경색이 약간 연해짐
위의 정책을 토대로, '해시태그 영역' 컴포넌트를 TDD 기반으로 구현해 보려고 합니다. 이 컴포넌트의 이름은 HashtagList
라고 부르겠습니다.
🤔 무엇을 테스트할까?
TDD를 하려면 먼저 테스트를 작성해야 하는데, 구현된 코드가 없으니 어떤 부분을 테스트해야 할지 모르겠네요. 일단 저희가 알고 있는 것은 요구사항뿐이니 요구사항을 기반으로 사용자 시나리오를 작성해 보겠습니다.
혹시 사용자 시나리오를 작성하기 어렵다면, 아래 내용을 생각해 보세요.
- 사용자는 UI와 어떤 방식으로 상호작용하게 될까? 그 상호작용의 기대 결과는 무엇일까?
- 내가 QA 팀이라면 이 컴포넌트를 어떤 식으로 테스트할까?
위 내용을 고려하여, ‘해시태그 영역’에 대한 사용자 시나리오를 아래와 같이 작성해 봤습니다.
1- 사용자가 처음으로 해시태그 영역을 보게 되면2 - 포스트의 해시태그가 5개 이하일 경우3 - 모든 해시태그가 표시된다4 - ‘더보기’ 버튼이 노출되지 않는다5 - 포스트의 해시태그가 5개보다 많을 때6 - 해시태그가 5개까지 노출된다7 - ‘더보기’ 버튼이 노출된다8- 사용자가 해시태그를 클릭하면9 - 검색어에 해당 해시태그 이름이 설정된다10- 사용자가 해시태그에 마우스를 올리면11 - 해시태그에 밑줄이 생기고, 글자색과 배경색이 연해진다12- 사용자가 ‘더보기’ 버튼을 누르면13 - ‘더보기’ 버튼이 노출되지 않는다14 - ‘접기’ 버튼이 노출된다15 - 포스트의 해시태그가 10개 이상일 경우16 - 해시태그가 10개까지만 노출된다17 - 포스트의 해시태그가 5개 이상, 10개 이하일 경우18 - 해시태그가 모두 노출된다19- 사용자가 ‘접기’ 버튼을 누르면20 - ‘더보기’ 버튼을 누르기 이전 상태로 돌아간다
사용자 시나리오를 계층화하기 위해 describe-context-it
패턴을 활용할 수도 있습니다.
계층 | 역할 | 예시 |
---|---|---|
describe | 테스트할 대상 | HashtagList 컴포넌트에서 |
context | 대상이 놓인 상황, 환경 | 사용자가 ‘더보기’ 버튼을 눌렀을 때, 포스트의 해시태그가 10개 이상일 경우 |
it | 기대 결과 | 해시태그가 10개까지만 노출된다 |
이렇게 계층화하면 시나리오를 파악하기 편하고, 나중에 테스트 코드로 변환했을 때에도 각 테스트의 선행 조건 등을 관리하기 편합니다.
그런데, 방금 작성한 모든 시나리오를 단위 테스트의 테스트 케이스로 사용할 필요는 없습니다. 예를 들어, 해시태그에 밑줄이 생기고, 글자색과 배경색이 연해진다
같은 테스트는 단위 테스트로 검증하기 보다는 스토리북을 활용해서 확인하는 게 더 나을 수도 있습니다.
Testing Library로는 기능적인 부분을 테스트하고, 시각적인 부분을 테스트하는 건 스토리북에 렌더링 된 실제 UI를 체크하는 것이 더 효과적입니다.
이제 테스트를 생성해 보겠습니다. 이때, 테스트의 내용은 작성하지 않고 이름만 작성하겠습니다.
공통되는 조건이나 상황을 describe
로 묶고, 이에 대한 기대 결과를 test
로 작성했습니다. 아직 테스트의 내용을 작성하지 않았기 때문에, test.todo
를 사용해 테스트의 이름만 명시했습니다.
💣 실패하는 테스트 작성하기
테스트를 작성하기 위해서 빈 컴포넌트를 먼저 만들어 두면 편합니다. Fragment 만을 반환하는, 아무런 UI도, 로직도 가지고 있지 않은 아주 기본적인 컴포넌트를 만들었습니다. 이 컴포넌트가 어떤 속성과 로직을 가질지는 테스트를 작성하는 과정에서 결정됩니다.
이 컴포넌트를 가지고 포스트의 해시태그가 5개보다 많을 경우, 해시태그가 5개까지 표시된다
에 대한 테스트를 작성해 보겠습니다.
1describe("포스트의 해시태그가 5개보다 많을 경우", () => {2 test("해시태그가 5개까지 표시된다", () => {3 const hashtags = [4 "해시태그 1",5 "해시태그 2",6 "해시태그 3",7 "해시태그 4",8 "해시태그 5",9 "해시태그 6",10 ];11 render(<HashtagList />);1213 const listitems = screen.getAllByRole("listitem");1415 expect(listitems).toHaveLength(5);16 expect(listitems[0]).toHaveTextContent("해시태그 1");17 expect(listitems[1]).toHaveTextContent("해시태그 2");18 expect(listitems[2]).toHaveTextContent("해시태그 3");19 expect(listitems[3]).toHaveTextContent("해시태그 4");20 expect(listitems[4]).toHaveTextContent("해시태그 5");2122 expect(screen.queryByText("해시태그 6")).not.toBeInTheDocument();23 });24});
테스트를 실행해 보면, 잘 실패하는 것을 확인할 수 있습니다. ‘listitem’이란 역할의 element를 찾지 못했다고 하네요.
🩹 테스트를 성공시키기
1describe("포스트의 해시태그가 5개보다 많을 경우", () => {2 test("해시태그가 5개까지 표시된다", () => {3 const hashtags = [4 "해시태그 1",5 "해시태그 2",6 "해시태그 3",7 "해시태그 4",8 "해시태그 5",9 "해시태그 6",10 ];11 render(<HashtagList />);1213 const listitems = screen.getAllByRole("listitem");1415 expect(listitems).toHaveLength(5);16 expect(listitems[0]).toHaveTextContent("해시태그 1");17 expect(listitems[1]).toHaveTextContent("해시태그 2");18 expect(listitems[2]).toHaveTextContent("해시태그 3");19 expect(listitems[3]).toHaveTextContent("해시태그 4");20 expect(listitems[4]).toHaveTextContent("해시태그 5");2122 expect(screen.queryByText("해시태그 6")).not.toBeInTheDocument();23 });24});
이 테스트를 성공시키기 위해서는 컴포넌트를 수정해야 합니다.
- property로 해시태그 리스트를 받아오기
- 해시태그 리스트를 listitem role을 가지는 element로 렌더링하기
먼저 컴포넌트를 수정해 주겠습니다.
테스트에서 HashtagList
컴포넌트에 hashtagList
속성을 넘겨주게 변경하면, listitem 역할의 요소가 6개 존재하기 때문에 실패합니다.
화면에 보여줄 해시태그 리스트 배열을 따로 관리하도록 수정했습니다.
1export const HashtagList = ({ hashtagList }: Props) => {2 const visibleHashtagList = hashtagList.slice(0, 5);3 return (4 <ul>5 {visibleHashtagList.map((hashtag) => (6 <li key={hashtag}>{hashtag}</li>7 ))}8 </ul>9 );10};
다시 테스트를 실행해 보면, 방금 작성한 테스트를 통과했습니다! 🎉
🔧 리팩토링하기
그 밖의 다른 테스트는 따로 작성해서, 모든 테스트를 통과시키는 컴포넌트를 완성했습니다! 아래 캡쳐는 모든 테스트를 통과시키는 HashtagList
컴포넌트가 실제로 렌더링 된 모습입니다.
확실히 디자인 팀에서 원한 모습은 이게 아닐 것 같네요. 그렇지만 테스트를 작성해 뒀기 때문에, 기능의 동작을 보장하면서 안전하게 UI를 수정할 수 있습니다.
🧚 테스트 100% 활용하기
코드를 유지보수하는 것처럼, 테스트도 꾸준히 유지보수하면 견고하고 안정적인 프로젝트를 만드는 데 큰 도움이 될 수 있습니다.
- 이 컴포넌트와 관련된 기획이 추가되었다면, 그 기획과 관련된 테스트도 추가하세요! 테스트가 기능 명세의 역할을 해줄 수 있습니다.
- 이 컴포넌트에서 버그가 발견되었다면, 그 버그에 대한 테스트도 추가해 두세요! 나중에 컴포넌트에 수정이 생길 때, 같은 버그를 다시 발생시키지 않도록 예방할 수 있습니다.
- 바빠서 테스트를 작성하기 어렵다면,
test.todo
를 활용해서 어떤 테스트를 작성해야 하는지를 기록해 두는 것만으로도 도움이 될 수 있습니다.
👏 마치며
기능 개발 초기에 테스트를 작성하지 않으면, 개발이 진행될수록 테스트를 작성하기 어렵고 귀찮아져서 결국에는 무시되는 경우가 많습니다. 그렇기 때문에 TDD로 개발하는 습관을 들여 두면 좋습니다.
프론트엔드에서 TDD로 테스트 코드를 작성하게 되면 이런 점이 좋습니다.
- 기능이 정상적으로 동작한다는 것을 자동으로 보장해 줍니다. 초기 개발 단계뿐만 아니라, 이후에 새로운 기획이 추가되어 추가 기능을 개발할 때도 기존 기능이 정상 동작함을 보장받을 수 있습니다.
- 기획 요소에 대한 기능 명세서의 역할을 해줍니다. 테스트 케이스에 해당 컴포넌트의 기능이 모두 명세 되어 있다면, 그 컴포넌트의 역할을 기획 문서를 따로 찾아보지 않아도 코드로 한눈에 파악할 수 있습니다.
- 접근성과 시맨틱 구조를 고려해 개발하게 됩니다. Testing Library에서 접근성과 시맨틱 구조를 활용해 요소를 선택하도록 권장하고 있기 때문에, 이를 만족시키기 위해 웹 표준을 준수해 개발하게 됩니다.
아직 테스트 작성에 익숙하지 않으시다면, 본문과같이 간단한 컴포넌트에 대한 테스트를 작성하는 것부터 시작해 보면 좋습니다. 이번 글에서는 간단한 컴포넌트에 대한 테스트를 작성해 봤지만, 복잡하고 큰 컴포넌트나 훅도 본문에서 소개한 방법을 토대로 충분히 테스트를 작성해 볼 수 있습니다.