Next.js 블로그에 Cypress를 통한 E2E 테스트 적용해보기 Ver.1
# 프론트엔드 테스트의 개념과 종류를 알아볼까요?
2024년 07월 02일
도입 배경
블로그 프로젝트가 점점 발전하면서 검색이나 AI 요약 기능 등 여러 새로운 기능이 추가되었다. 그러나 이러한 기능을 추가할 때마다 기존 기능과 디자인에 영향을 미칠 가능성이 커졌고, 실제로 기능 추가 시마다 오류가 발생하곤 했다.
앞으로 방문자 기능이나 포트폴리오 섹션을 추가할 예정인데, 이렇게 복잡성이 증가하는 블로그 프로젝트에 자동화된 테스트가 필요해 보였다. 다양한 테스트 종류 중에서 특히 E2E(End-to-End) 테스트는 애플리케이션 전체 흐름을 검증하여 최종 사용자 입장에서 기능 구현의 정확성을 보장하기 때문에 매력적으로 다가왔고, 작은 단위 테스트보다는 실제 배포된 사이트의 전체적인 테스트가 중요하게 느껴져 E2E 테스트를 도입하기로 결정했다. 이러한 이유도 있지만 내 이름이 사이트 메인에 크게 표시되는데, 오류가 발생하면 굉장히 부끄러울 것 같았다.
또한, E2E 테스트를 GitHub Actions와 연결하여 자동화된 테스트를 통해 높은 품질을 유지하고자 한다. 이를 빌드 및 배포 파이프라인에 통합하여, 지속적인 통합 및 배포(CI/CD) 과정에서 자동으로 검증되도록 할 계획이다.
❗ 해당 글은 총 3편으로 나누어 집니다. 첫 번째 편에서는 프론트엔드 테스트에 대한 설명과 필요한 지식에 대해 학습할 예정입니다. 두 번째, 세 번째 편에서는 Cypress를 사용하여 실제 테스트 코드를 작성하고 GitHub Actions을 통해 CI 작업을 진행 할 것입니다.
프론트엔드 테스트 종류 및 필요성
먼저 테스트 코드를 작성하기 전, 프론트엔드 테스트의 종류와 필요성에 대해 알아보자
프론트엔드 테스트란?
프론트엔드 테스트는 소프트웨어 개발 과정에서 사용자 인터페이스(UI)와 사용자 경험(UX)을 검증하는 절차를 의미한다. 해당 테스트는 소프트웨어가 예상대로 작동하는지 확인하고 잠재적인 버그를 찾아내며, 이러한 결함을 개선하는 과정을 포함하는데, 이를 통해 개발자가 기대하는 대로 기능이 작동하고, 애플리케이션이 안정적으로 운영될 수 있도록 보장한다. 즉 프론트엔드 테스트는 단순히 기능적인 검사를 넘어, 시스템의 전체적인 품질을 평가하고 사용자가 만족할 수 있는 경험을 제공하기 위해 필수적라고 할 수 있다.
프론트엔드 테스트의 필요성
1. 신뢰성 확보
프론트엔드 테스트는 코드를 예상치 못한 버그로부터 보호하고, 소프트웨어의 신뢰성을 높일 수 있다. 컴포넌트 간의 상호작용에서 발생할 수 있는 문제를 사전에 식별할 수 있는데, 사용자 인터페이스(UI)와 사용자 경험(UX) 측면에서의 신뢰성을 보장하고, UI 요소 간의 상호작용이나 사용자 입력에 따른 반응을 확인하는 데 중점을 둔다.
2. 유지보수성 향상
프론트엔드 테스트는 변경된 코드가 기존 기능에 영향을 미치지 않음을 확인하여 유지보수성을 높일 수 있다. 유닛 테스트로 핵심 로직이 정확하게 동작하는지 검증하고, 통합 테스트로 여러 모듈 간의 호환성을 확인할 수 있다. 프론트엔드에서는 주로 사용자 인터페이스의 변화에 따라 테스트를 수행하며, 디자인 변경이 기능에 미치는 영향을 확인한다.
3. 개발 생산성 향상
테스트를 통해 안전하게 리팩토링을 수행하거나 새로운 기능을 추가할 수 있다. 이는 코드베이스의 안정성을 확보하면서 개발 속도를 높이는 데 도움이 된다. 프론트엔드에서는 특히, UI 컴포넌트의 재사용성을 높이고, 레이아웃과 스타일 변경 시 발생할 수 있는 이슈를 빠르게 발견할 수 있다.
4. 사용자 경험 향상
성능 및 E2E 테스트를 통해 예상되는 오류를 사전에 제거하여, 사용자 경험을 향상시키고 안정적인 애플리케이션을 제공할 수 있다. 프론트엔드 테스트는 사용자 인터페이스의 반응성과 성능을 중점적으로 다루며, 실제 사용자 환경을 시뮬레이션하여 UI 요소의 렌더링 시간, 애니메이션 성능, 그리고 사용자 입력에 대한 즉각적인 반응을 확인한다.
소프트웨어 테스트 종류
유닛 테스트 (Unit Testing)
유닛 테스트는 소프트웨어의 가장 작은 단위인 "유닛"을 테스트하는 과정이다. 대개 함수, 메서드, 혹은 별도의 모듈이 유닛에 해당한다
- 도구: Jest, Mocha
- 사용 시기: 대규모 프로젝트, CI/CD, 새로운 기능 추가 및 리팩토링 시
유닛 테스트 작성 시, FIRST 원칙을 따르는 것이 좋다고 한다.
FIRST 원칙?
- Fast: 테스트가 빠르게 실행되어야 한다.
- Isolated/Independent: 각 테스트는 독립적이어야 하며, 다른 테스트의 실행 결과에 영향을 받지 않아야 한다.
- Repeatable: 테스트는 어떤 환경에서도 일관되게 반복 가능해야 한다.
- Self-validating: 테스트 자체로 검증이 가능해야 하며 성공 또는 실패 여부를 명확히 해야 한다.
- Timely: 테스트는 개발 초기에 작성되어야 하며 지속적으로 유지보수되어야 한다.
통합 테스트 (Integration Testing)
여러 컴포넌트가 상호작용하는 상황에서 전체 애플리케이션이 예상대로 작동하는지 확인하는 테스트
- 도구: React Testing Library
- 사용 시기: 컴포넌트 간 통합이 중요한 프로젝트, 비즈니스 로직이 복잡한 프로젝트, 데이터 플로우가 중요한 프로젝트
E2E 테스트 (End-to-End Testing)
사용자 관점에서 애플리케이션의 전체적인 흐름을 시뮬레이션하고 애플리케이션이 예상대로 동작하는지 확인하는 테스트
- 도구: Cypress
- 사용 시기: 사용자 경험이 중요한 웹 애플리케이션, 복잡한 비즈니스 프로세스를 갖는 애플리케이션, 다양한 통합 시스템을 가진 애플리케이션
스냅샷 테스트 (Snapshot Testing)
컴포넌트의 렌더링 결과물을 저장하고, 이후 변경사항을 감지하기 위해 기존 스냅샷과 비교하는 방식의 테스트
- 도구: Jest, React Testing Library
- 사용 시기: 컴포넌트 렌더링이 자주 변경되는 프로젝트, 디자인 시스템을 사용하는 프로젝트, 다양한 환경에서 사용되는 프로젝트
소프트웨어 테스트의 7원칙
소프트웨어 테스트 7원칙?
소프트웨어 테스트의 필요성과 효과를 이해하고, 효율적이고 효과적인 테스트를 수행하기 위한 지침.
1. 결함 존재를 보여주는 테스팅 (Testing shows presence of defects)
테스트의 주요 목적은 소프트웨어에 존재하는 결함을 찾아내는 것이다. 테스트를 통해 소프트웨어가 완벽하다는 것을 증명하려는 것이 아니라, 잠재적인 문제점을 발견하고 수정하는 것에 중점을 둬야 한다.
예를 들어, 사용자가 특정 기능을 사용할 때 발생할 수 있는 예외 상황을 테스트하여, 이러한 상황에서 소프트웨어가 어떻게 반응하는지를 확인한다. 테스트를 통해 발견된 결함은 문제를 해결하고 소프트웨어의 품질을 향상시키는 기회를 제공한다.
다만, 결함이 발견되지 않았다고 해서 소프트웨어에 결함이 없다는 것을 의미하지는 않는다. 테스트는 결함을 발견하기 위한 도구로 이해되어야 하며, 발견된 결함을 통해 소프트웨어의 안정성과 신뢰성을 높이는 과정으로 인식해야 한다.
2. 철저한 테스팅은 불가능 (Exhaustive testing is impossible)
모든 가능한 입력 조합과 경로를 테스트하는 것은 현실적으로 불가능하다. 소프트웨어는 복잡한 시스템으로, 모든 변수와 조건을 고려하여 테스트하려면 무한한 시간과 자원이 필요한다. 때문에 위험 기반 접근 방식을 사용하여 가장 중요한 부분에 집중해야 한다. 이 원칙은 테스트 계획에서 우선순위를 설정하는 데 도움이 되며, 제한된 자원으로 최대한의 결함을 발견할 수 있도록 한다. 중요한 기능, 사용자에게 가장 큰 영향을 미치는 부분, 과거에 결함이 많이 발생한 영역 등을 중점적으로 테스트하여 효율적인 테스팅을 수행한다.
💡 글이 좀 어려워서 쉽게 설명하자면, 프로그램을 완벽하게 테스트하는 것은 불가능하다. 프로그램이 너무 복잡해서 모든 경우를 다 확인하는 데는 끝이 없기 때문이다. 그래서 중요한 부분에 집중해서 테스트를 진행해야 한다는 것이다.
3. 조기 테스팅 (Early testing)
테스팅은 가능한 한 소프트웨어 개발 초기에 시작하는 것이 중요하다. 소프트웨어 개발 초기 단계에서 결함을 발견하고 수정하는 것이 더 저렴하고 효과적이다. 예를 들어, 요구사항 분석 단계에서 결함을 발견하면, 이를 수정하는 데 필요한 비용과 시간은 상대적으로 적을것이다. 반면에, 제품이 완성된 후에 발견된 결함은 수정하기 위해 많은 시간과 자원을 소모하게 될 것이다.
4. 결함 클러스터링 (Defect clustering)
소프트웨어의 특정 모듈이나 기능에 대부분의 결함이 집중되는 경향이 있다. 이 원칙은 결함이 많이 발생하는 영역에 집중하여 테스트를 수행해야 함을 의미한다. 예를 들어, 복잡한 알고리즘을 구현하는 모듈이나, 외부 시스템과의 인터페이스를 처리하는 부분에서는 결함이 발생할 확률이 높을것이다. 결함 클러스터링 현상을 이해하고, 이러한 고위험 영역에 더 많은 테스트 자원을 할당함으로써, 효과적으로 결함을 찾아낼 수 있다. 과거의 테스트 결과나 버그 리포트를 분석하여, 결함이 자주 발생하는 영역을 식별하고, 해당 영역에 대한 집중적인 테스트를 수행하는 것이 중요하다.
5. 살충제 패러독스 (Pesticide paradox) ⭐️
동일한 테스트 케이스를 반복하면 더 이상 새로운 결함을 발견할 수 없다. 이는 마치 동일한 살충제를 반복적으로 사용하면 곤충이 내성을 갖게 되어 효과가 없어진다는 것과 유사하다. 따라서, 테스트 케이스를 주기적으로 검토하고 개선하여 새로운 결함을 발견할 수 있도록 해야 한다. 예를 들어, 기존 테스트 케이스가 특정 기능에 집중되어 있다면, 다른 관점에서 테스트 케이스를 작성하여 다양한 시나리오를 검증할 필요가 있다. 테스트 케이스를 업데이트하고, 새로운 테스트 기법을 도입하며, 다양한 데이터와 환경에서 테스트를 수행함으로써, 테스트의 효과를 지속적으로 유지할 수 있고, 이를 통해, 동일한 결함이 반복되지 않도록 하고, 새로운 결함을 지속적으로 발견할 수 있다.
6. 테스팅은 맥락에 의존적 (Testing is context dependent)
소프트웨어 프로젝트마다 테스트 접근 방식이 달라야 한다. 예를 들어, 안전-critical 시스템(의료기기, 항공기 소프트웨어 등)은 매우 엄격하고 철저한 테스트가 필요하다. 반면에, 간단한 웹 애플리케이션은 상대적으로 덜 엄격한 테스트가 필요하다. 각 프로젝트의 특성, 목적, 사용자, 위험 요소 등을 고려하여 적절한 테스트 전략을 수립하는 것이 중요하다. 예를 들어, 금융 소프트웨어는 보안과 정확성이 중요하기 때문에, 보안 테스트와 회계 검증 테스트가 중요한 반면에, 게임 소프트웨어는 사용자 경험과 성능이 중요하기 때문에, 성능 테스트와 UI/UX 테스트가 중요할 것이다. 이러한 맥락에 따라 테스트 방법, 도구, 접근 방식을 조정하여, 각 프로젝트의 요구사항을 충족할 수 있도록 해야 한다.
7. 오류 부재의 궤변 (Absence-of-errors fallacy)
소프트웨어가 결함이 없다고 해서 그것이 사용자의 요구를 충족시키는 것은 아니다. 소프트웨어가 결함 없이 잘 작동하더라도, 사용자 요구사항을 충족하지 못하면 실패한 소프트웨어로 간주될 수 있다. 예를 들어, 모든 기능이 제대로 작동하지만, 사용자 인터페이스가 불편하거나, 성능이 낮거나, 중요한 기능이 빠져 있다면, 사용자는 소프트웨어를 만족스럽게 사용하지 않을 것이다. 따라서, 테스트는 단순히 결함을 찾는 것뿐만 아니라, 소프트웨어가 사용자 요구사항을 충족하고, 기대되는 품질 수준을 제공하는지 확인하는 것이 중요하다. 요구사항 명세서와 사용자의 기대를 바탕으로 테스트를 설계하고 수행함으로써, 소프트웨어가 실제로 사용자에게 가치를 제공하는지 확인해야 한다.
💡 위 글도 좀 어렵다.. 간단하게 설명하면 소프트웨어가 잘 동작한다고 해도, 사용자가 원하는 기능을 충족하지 못하면 실패할 수 있다. 예를 들어 사용하기 불편한 인터페이스나 느린 성능, 중요한 기능이 빠진 경우 사용자는 만족하지 않을것이다. 그래서 테스트는 결함을 찾는 것 뿐만 아니라, 소프트웨어가 사용자가 필요로 하는 기능을 잘 수행하고 품질을 제공하는지 확인하는 작업이다.
❗️ 내용이 워낙 많아 한 문장으로 정리해본다면
- 결함 존재를 보여주는 테스팅: 테스트는 주로 결함을 발견하고 수정하는 데 중점을 두자
- 철저한 테스팅은 불가능: 모든 가능한 입력과 경로를 테스트하는 것은 현실적으로 불가능
- 조기 테스팅: 개발 초기에 결함을 발견하고 수정하는 것이 비용과 효율성 면에서 유리
- 결함 클러스터링: 결함은 특정 모듈이나 기능에 집중되는 경향이 있다.
- 살충제 패러독스: 반복적인 동일한 테스트 케이스는 결함 발견에 한계가 있으므로 다양한 접근이 필요
- 테스팅은 맥락에 의존적: 각 소프트웨어 프로젝트의 특성에 맞는 테스트 전략을 수립
- 오류 부재의 궤변: 소프트웨어가 결함이 없어도 사용자 요구를 충족하지 못하면 실패
E2E 테스트 심화
E2E(End-to-End) 테스트 추가 설명
E2E(End-to-End) 테스트는 소프트웨어 시스템이 실제 사용자의 관점에서 기대한 대로 동작하는지를 확인하는 테스트 방법이다. 전체 시스템을 검사하여 UI, 데이터베이스, 서버 및 기타 여러 시스템 구성 요소가 조화롭게 작동하는지를 확인하는 데 중점을 둔다. E2E 테스트를 통해 최종 사용자가 시스템을 실제로 사용하는 환경에서 모든 부분이 올바르게 동작하는지를 검증할 수 있다.
E2E 테스트의 특징
- 사용자 시나리오 흉내내기: E2E 테스트는 실제 사용자가 애플리케이션을 사용하는 방식을 시뮬레이션한다. 일상적인 사용자 행동, 예를 들어 로그인, 데이터 입력, 페이지 전환 등의 행위를 자동화하여 테스트한다.
- 통합 테스트: 다양한 시스템 구성 요소가 제대로 상호작용하는지를 확인한다. 이는 데이터베이스, 서버, API 호출 등이 포함되며, 전체 애플리케이션의 흐름과 기능이 제대로 작동하는지를 평가한다.
- 고비용 테스트: 전체 사용자 시나리오를 끝에서 끝까지 테스트하기 때문에 비용이 많이 들 수 있다. 따라서 반드시 필요한 부분만 테스트하여 효율적으로 리소스를 활용한다.
E2E 테스트를 해야하는 이유
- 사용자 경험 향상: 실제 사용 환경에서 애플리케이션이 올바르게 동작하는지 확인하여 사용자 경험을 보장하는데, 이는 사용자 만족도를 높이고 이탈률을 낮추는 데 중요하다.
- 전체 시스템 검증: 각 부분이 올바르게 상호작용하는지 확인하여 전체 시스템을 통합적으로 검증한다. 이를 통해 단순 기능적 검사를 넘어서 시스템 전반의 안정성을 확인할 수 있다.
- 버그 조기 발견: 사용자 시나리오를 테스트하므로 개발 초기에 버그를 조기에 발견할 수 있다. 이는 문제를 단시간에 해결하게 도와주고, 버그가 제품 배포 후에 나타나는 것을 방지한다.
- 자동화로 반복 테스트 수행: E2E 테스트를 자동화하면 테스트를 반복적으로 실행하여 안정적인 소프트웨어를 제공할 수 있다. 이는 배포 전에 모든 기능이 예상대로 작동하는지를 지속적으로 확인할 수 있게 해준다.
E2E 테스트 예시: 사용자 로그인 시나리오
사용자가 웹 애플리케이션에 로그인하는 시나리오를 생각해보면,
Given (주어진 상황): 웹 애플리케이션이 실행 중이며, 로그인 화면이 표시된다.
When (언제): 사용자가 로그인 화면에 유효한 사용자 이름과 비밀번호를 입력하고 "로그인" 버튼을 클릭한다
Then (그러면):
- 메인 페이지로 이동해야 한다.
- 로그인 후 사용자의 이름이 프로필에 표시되어야 한다.
- 로그인 성공 메시지가 나타나야 한다.