프론트엔드 테스트 자동화 전략 - 2. 요구사항 분석

저번 글에서는 프론트엔드의 테스트 자동화에 대해서 소개하고, 테스트 전략에 대해서 간단하게 다뤄봤습니다.

테스트 자동화가 아무리 중요하다고 해도, 테스트 작성을 시작하는 것은 쉽지 않습니다. 설정해야 하는 것들이 많고, 효과적인 테스트 작성 방법을 찾는 것도 어렵습니다.

이 문서에서는 프론트엔드에 맞춘 테스트 작성 방법을 고민해봤습니다. 물론 프로젝트마다 상황은 다를 수 있으며, 나중에는 더 나은 방법을 발견할 수도 있습니다. 그래도 이 문서가 테스트를 시작하는 데 도움이 됐으면 좋겠습니다.

어떻게 시작하는게 좋을까?

테스트 작성의 틀이 한번 잡히면 이어나가는 것은 비교적 수월한데, 시작이 제일 어려운 것 같습니다.

특히, 테스트를 작성할 때에는 큰 의미가 없지만, 테스트가 작성되고 시간이 지나야만 효용성이 체감이 되는 경우가 대부분이다보니 테스트 작성을 주저하게 됩니다. 테스트의 장점을 즉시 경험해볼 수 있도록 리팩토링을 진행하는 시점에 테스트를 작성하는 것으로 시작해보면 좋을 것 같습니다.

리팩토링을 하기 전에 테스트를 작성하면 해당 기능의 요구사항을 명확하게 정리할 수 있습니다. 이렇게 하면 아이러니하게도 테스트를 실패시키는 상황이 잘 발생하지 않게 됩니다. 요구사항을 명확히 이해하고 작성함으로써, 버그를 줄이게 된 것입니다.

비슷하게, 시간이 흐른 후에 코드를 다시 검토하게 될 때에도 테스트 코드에 요구사항이 명시되어 있기 때문에 요구사항을 파악하는 데 큰 도움이 될 것입니다.

게다가, 테스트를 작성하고 리팩토링을 진행하면, 테스트의 위양성(false positive)이 어떻게 개발에 영향을 주는지에 대해 이해할 수 있습니다. 리팩토링하면서 코드를 개선하는 방법에 대한 그림이 있는 채로 테스트를 작성하게 되므로, 리팩토링에 내성이 있는 테스트를 작성하고, 위양성을 피할 수 있는 방법을 생각해볼 수 있는 것입니다.

리팩토링하는 도중 요구사항을 벗어나는 내용이 생기면, 테스트가 즉시 실패하는 모습을 관찰할 수 있어 효용성을 바로 체감할 수 있습니다.

이러한 장점들을 고려하여, 이 글에서는 리팩토링하는 과정에서 테스트를 작성하는 예시를 소개하고자 합니다. 물론 여기서 다룬 내용은 다른 상황에서도 적용 가능하지만, 처음으로 테스트를 작성한다면 리팩토링하는 상황에서 시도해보는 것을 적극 권장합니다.

테스트 예제

이 글은 “주소록” 기능을 가진 주문서 페이지를 리팩토링하는 예제를 가정하고 작성해봤습니다.

주소록 기능은 상당히 복잡합니다. 여러 페이지에 걸쳐 작동하며, 프론트엔드에서 직접 제어할 수 없는 서버 API나 우편번호 API와 같은 영역도 포함되어 있기 때문에 고민해야 하는 내용이 많습니다.

따라서, 테스트 작성 과정에서 발생하는 다양한 고민들을 다룰 수 있다는 점에서 좋은 예제라고 생각했습니다.

1.png

2.png

요구사항 분석

테스트 작성은 코드의 요구사항을 분석하는 단계부터 시작합니다. 하지만 대부분의 경우, 개발 초기에 사용된 기획서나 디자인 파일은 시간이 지나면 유효하지 않거나 찾을 수 없는 경우가 많습니다.

그렇기 때문에, 리팩토링을 진행할 때에는 현재 배포된 코드와 화면을 분석하면서 새로운 요구사항을 작성해보는 것을 권장합니다. 원본 기획서나 디자인이 남아있을 수도 있지만, 후속 요구사항이 반영되면서 초기 요구사항과 크게 달라진 경우가 많기 때문입니다.

화면을 분석해본 후에는 “유저 스토리"를 먼저 작성해볼 수 있습니다. 예를 들어 “구매자는 주소를 다시 입력하는 번거로움을 줄이기 위해 입력한 주소를 저장하고 불러오고 싶다"와 같은 유저 스토리가 있을 수 있습니다.

유저 스토리는 제품이 제공하는 근본적인 가치를 나타내며, 다른 요구사항들은 이 유저 스토리에서 파생됩니다. 따라서 유저 스토리는 애자일 개발에서 매우 중요한 개념이며, 제품의 본질에 더 가까이 다가갈 수 있도록 도와줍니다. 기획 단계에서도 중요한 내용이기 때문에, 관련해서 더 찾아보는 것도 추천드립니다.

유저 스토리를 작성한 후에는 화면을 더 자세히 분석하고, 사용자 관점에서의 인수 조건(Acceptance Criteria, AC)을 작성해볼 수 있습니다. 예를 들어 “구매자가 ‘저장’ 버튼을 누르면 주소가 저장되고, 이전 화면으로 이동한다"와 같은 조건이 있을 수 있습니다.

인수 조건은 순수하게 사용자의 관점에서 작성되어야 하며, 서버, 외부 API, 데이터베이스 등의 다양한 구성요소의 동작 방식은 고려하지 않습니다.

이렇게 유저 스토리와 인수 조건을 작성한 후에는 요구사항을 새롭게 갱신하는 작업이 완료됩니다. 이제 이 요구사항을 기반으로 테스트를 작성해보도록 합시다.

테스트 케이스 만들기

테스트 케이스를 작성할 때에는 각 시스템과 시스템 간의 인터페이스에 대해 고민해야 합니다.

특히, 직접 제어할 수 있는 영역에 초점을 맞추어야 합니다. 예를 들어, 프론트엔드 관점에서는 백엔드나 주소 API의 내부 동작 방식에 대해서는 중요하지 않습니다.

하지만, 백엔드에 대한 API 요청을 보내는 것 자체는 프론트엔드 관점에서 신경 써야 할 일입니다. 백엔드의 동작 자체가 중요한 것이 아니라, API 요청과 같은 인터페이스에 주목해야 합니다.

예를 들어, “구매자가 ‘저장’ 버튼을 누르면 주소가 저장된다"라는 인수 조건을 프론트엔드 관점에서 고려해봅시다. 구매자가 “저장” 버튼을 누르면 저장 API가 호출되어야 합니다. 그러나 DB에 주소가 실제로 저장되는지 여부는 우리가 직접 제어할 수 있는 영역을 벗어나는 일입니다. 따라서 프론트엔드 테스트에서는 이 부분을 고려하지 않아도 됩니다. 대신, “구매자가 ‘저장’ 버튼을 누르면 저장 API가 호출되고, 이전 화면으로 이동한다”와 같이, 프론트엔드 관점으로 테스트 케이스를 만들 수 있습니다.

3.png

이렇게 테스트 케이스를 고려함으로써 여러 시스템 간의 상호작용과 의존성을 파악할 수 있습니다. 또한, 백엔드에서 필요한 API를 도출하는 데에도 큰 도움이 됩니다.

테스트 분배하기

테스트 케이스를 작성한 후에는 컴포넌트 단위로 테스트 케이스를 분배할 수 있습니다.

예를 들어, 현재 주문 가능한 주소를 나타내는 기능을 검증하고자 한다고 가정해봅시다. 주소가 배송이 불가능한 경우 회색으로 표시되어야 합니다. (실제로는 없는 기능입니다.)

4.png

이러한 기능을 “주소록 페이지” 컴포넌트에서 테스트하는 대신, “주소 항목” 컴포넌트에 해당 기능을 위임하는 것도 생각해볼 수 있습니다. “주소 항목” 컴포넌트에서는 주문 가능 여부를 확인하는 API를 직접 호출하고, 스스로 회색으로 표시하도록 설계해볼 수 있습니다.

이렇게 기능을 위임함으로써 상위 컴포넌트는 하위 컴포넌트의 동작을 신뢰하고, 상위 컴포넌트에서는 관련된 동작에 대한 테스트를 건너뛸 수 있게 됩니다. 다시 말해, 컴포넌트를 재사용함으로써 관련된 동작의 검증도 건너뛸 수 있게 되는 것입니다.

하지만, 이렇게 컴포넌트의 요구사항을 테스트 케이스로 고정하게 되면, 해당 컴포넌트의 동작을 변경하기가 어려워집니다. 컴포넌트에 의존하는 상위 컴포넌트들이 모두 영향을 받기 때문에, 동작을 변경하면 상위 컴포넌트들도 모두 테스트해야 합니다.

5.png

따라서 궁극적으로는 테스트가 컴포넌트의 동작을 문서화하는 것이라고 보는게 좋습니다. 요구사항이 명확하지 않거나 설계에 혼란이 있는 경우에는 상위 컴포넌트에 작성하는 것이 좋습니다.

이렇게 테스트 케이스를 분배하는 것은 실제로 컴포넌트의 설계를 고민하는 것과 유사합니다. 테스트 케이스를 통해 컴포넌트의 재사용 가능성을 고민함으로써 설계 과정에서도 도움을 줄 수 있습니다.

테스트 케이스까지 고민을 마치고 나면, 이제 테스트를 작성해볼 수 있습니다. 구체적으로 테스트를 작성하는 방법에 대해서는 다음 글에서 알아보겠습니다.

마치며

다뤘던 내용을 간단하게 정리하면서 마무리해보겠습니다.

  • 테스트 자동화를 진행하면, 시간이 흐르고 프로젝트가 커질 수록 효과가 커집니다.
  • 바로 효용성을 체감해볼 수 있도록, 리팩토링을 진행하기 전에 테스트를 작성해보기를 권장합니다.
  • 테스트는 요구 사항을 분석하는 것부터 시작합니다. 개발에 쓰였던 기획서 원본은 유효하지 않은 경우가 많아, 제품을 직접 사용해 보면서 분석해보는 것이 좋습니다.
  • 유저 스토리는 제품이 제공하는 근본적인 가치를 의미하고, 모든 요구사항은 여기서부터 나옵니다.
  • 인수 조건은 사용자 관점에서 제품이 제대로 동작하는지 확인하기 위한 요구사항입니다.
  • 테스트 케이스는 인수 조건을 토대로, 각 시스템들과의 상호작용을 고민해보면서 작성합니다. 예를 들어, 프론트 관점에서는 서버의 동작이 중요한게 아니라, 서버로 API 요청을 보내는 것이 중요합니다.
  • 각 컴포넌트에 테스트 케이스를 분배해봅니다. 컴포넌트에 어떤 역할을 할당하는지에 따라 설계가 크게 달라질 수 있으므로 유의해야 합니다. 상위 컴포넌트는 하위 컴포넌트에게 요구사항을 위임합니다.

이렇게 해서, 프론트엔드에서 요구사항과 테스트 케이스를 산출해내는 방법에 대해 알아봤습니다. 결국 제품의 본질을 찾아내고, 거기에 맞도록 테스트를 작성하는게 중요한 것 같습니다.

또한, 개발자의 입장에서, 어떤 서버 API가 필요한지, 어떻게 설계하면 좋은지 탐구하는 데에도 요구사항을 위주로 검색하는게 큰 도움이 되는 것 같습니다.

다음 글에서는 구체적으로 테스트를 구현하는 방법들에 대해서 알아보겠습니다. 유닛 테스트, 통합 테스트와 같은 코드를 실제로 작성하는 방법에 대해서 고민해보겠습니다.



comments powered by Disqus