웹 앱 엔드투엔드 테스트, 고통 없이 하는 방법
원문은 Michael Lynch님이 에 게재했습니다. 이 블로그 구독하기
좋아요, 의심하고 계시죠. 다른 가이드들은 고통 없는 웹 앱 테스트를 약속해 놓고는 막상 보면 특정 기술 스택에서만 동작하거나 유료 외부 서비스가 필요하다고 밝히곤 합니다. 저는 그렇게 하지 않겠습니다.
이 가이드는 거의 모든 웹 앱에 적용할 수 있는 간단하고 유연한 엔드투엔드 테스트 템플릿을 제공합니다. 유일한 요구사항은 여러분의 앱을 Docker에서 실행할 수 있다는 것뿐입니다.
정말 그게 전부입니다! Ruby 앱이든, React 앱이든, Enterprise Java Beans 앱이든, 심지어 여러분이 직접 만든 독특한 웹 스택이라도 테스트할 수 있습니다. Windows에서 개발하든 Linux에서 하든 Mac에서 하든 상관없습니다. 무엇보다 복잡한 설정을 하거나 Docker 외에 다른 소프트웨어를 설치할 필요가 없습니다.
이 튜토리얼에서는 무료 오픈소스 도구만 사용하며, 어디에도 가입하지 않고 바로 실행할 수 있습니다. Circle이나 Travis 같은 지속적 통합 환경에서 테스트를 실행할 때도 특별한 작업이 필요 없습니다. 개발 머신에서 사용하는 것과 똑같은 한 줄짜리 명령어로 테스트를 실행하면 됩니다.
쇼의 주인공, Cypress
업데이트(2022-10-25): 저는 더 이상 웹 애플리케이션의 엔드투엔드 테스트에 Cypress를 권장하지 않습니다. 새로운 프로젝트에서는 대신 Playwright를 사용하는 것을 권장합니다.
이 테스트를 가능하게 하는 도구는 Cypress로, 브라우저 자동화 분야에 비교적 최근에 등장한 도구입니다. 오픈소스 엔드투엔드 테스트 프레임워크이며, 전담 팀이 활발히 개발하고 있습니다. 이들의 비즈니스 모델은 Docker와 유사합니다. 두 회사 모두 무료 오픈소스 도구를 공개하고, 해당 도구의 관리형 서비스를 판매해 개발을 지원합니다.

Cypress는 웹 앱 자동 테스트를 위한 오픈소스 도구입니다.
저는 지난해 지역 소프트웨어 콘퍼런스에서 Gleb Bahmutov가 시연하는 것을 보고 Cypress를 처음 알게 되었습니다. Cypress가 Selenium에 전혀 의존하지 않는다는 말을 들었을 때 흥미가 생겼습니다. 그때까지 제가 경험한 엔드투엔드 테스트는 모두 끔찍했고, 그 고통의 근원에는 항상 Selenium이 있었습니다.

Selenium은 가장 오래되고 널리 쓰이는 브라우저 자동화 도구이지만, 투박하고 구식입니다.
Selenium은 압도적으로 가장 인기 있는 브라우저 자동화 프레임워크이지만, 15년 전에 설계된 Java 기반 도구에서 기대할 수 있는 모든 문제를 안고 있습니다. 설치가 번거롭고 문법이 어색하며, 테스트가 실패했을 때 제공하는 정보도 빈약합니다. Gleb이 시연한 Cypress의 매끄러운 데모에서는 이런 문제들을 모두 해결해 줄 것처럼 보였습니다.

Cypress의 멋진 기능 중 하나는 테스트의 모든 단계에서 브라우저를 녹화해 실패 원인을 진단할 수 있도록 돕는다는 점입니다.
저는 기대감을 갖고 Cypress 문서를 읽었지만, 거의 모든 문서가 사용자가 Node.js 스택을 사용하고 헤드리스 콘솔이 아닌 그래픽 환경에서 개발한다는 전제하에 작성되어 있어 실망했습니다.
그럼에도 Cypress는 미래가 유망해 보였습니다. 1년 뒤 다시 살펴보니 Cypress와 Docker Compose를 결합한 새로운 샘플 애플리케이션을 발견했습니다. 그 순간 모든 것이 명확해졌습니다. Docker Compose에서 동작하는 Cypress를 보는 순간, 그 패턴을 어떤 웹 앱에도 적용할 수 있겠다는 확신이 들었습니다. 오늘은 그 패턴과 여러분의 앱에 적용하는 방법을 소개하려고 합니다.
재사용 가능한 엔드투엔드 테스트 패턴
Cypress와 Docker Compose를 결합하면 거의 모든 웹 앱에 적용할 수 있을 만큼 유연한 테스트 패턴이 완성됩니다. 앱 구현에 대해 이것저것 가정하는 다른 테스트 도구와 달리, 이 방식은 테스트 프레임워크를 테스트 대상 앱으로부터 완전히 분리합니다.

Docker Compose, Cypress, 그리고 웹 앱이 함께 동작하는 방식
Docker Compose를 사용하면 Cypress는 하나의 컨테이너에서, 앱은 다른 컨테이너에서 실행할 수 있습니다. 앱은 Cypress에 대해 아무것도 알 필요가 없고, Cypress 역시 앱에 대해 HTTP 요청을 보낼 네트워크 포트만 알면 됩니다.
테스트할 간단한 웹 앱
테스트 예시로 사용할 웹 앱으로, 세상에서 가장 단순한 텍스트 감정 분석기인 Sentimentalyzer를 소개합니다. 이 앱은 사용자가 입력한 글에서 기분을 추측하려고 합니다.
It's a nice day today라는 텍스트를 입력하면, Sentimentalyzer는 사용자가 행복하다고 판단합니다:


행복한 텍스트를 분석하는 Sentimentalyzer
Who ate ALL MY WAFFLES?라는 텍스트를 입력하면, Sentimentalyzer는 사용자가 화가 났다고 판단합니다:


화난 텍스트를 분석하는 Sentimentalyzer
알고리즘은 단순합니다. 문자 중 50% 이상이 대문자라면 사용자가 소리를 지르고 있다고 보고 화가 난 것으로 간주합니다. 그렇지 않으면 기분이 괜찮은 것으로 판단합니다.
프로젝트 구조
다음은 예제 프로젝트의 파일 구조입니다:
main.go <- source for my web app, Sentimentalyzer
Dockerfile <- defines how to run Sentimentalyzer in a Docker container
e2e/ <- folder that contains all the files for my end-to-end tests
cypress.json <- Cypress configuration
docker-compose.yml <- glue that binds together my app container with the Cypress container
integration/
spec.js <- defines the end-to-end test for Sentimentalyzer
모든 프로덕션 로직은 루트 폴더에 있고, 모든 엔드투엔드 테스트 코드는 e2e 폴더에 있습니다.
Sentimentalyzer를 로컬에서 실행하기
여기서 앱의 소스 코드를 일부러 보여드리지 않는 이유는, 앱 구현을 전혀 보지 않고도 Cypress 테스트를 작성할 수 있다는 점을 강조하기 위해서입니다. Sentimentalyzer는 Go로 만들어졌지만, Python이나 Angular로 구현했더라도 테스트는 동일했을 것입니다. 궁금하시다면 소스 코드는 GitHub에서 확인하실 수 있습니다.
여러분의 머신에서 Sentimentalyzer를 직접 실행해 보려면 다음 명령어를 실행하세요:
git clone https://github.com/mtlynch/hello-world-cypress.git
cd hello-world-cypress
docker build --tag sentimentalyzer .
docker run \
--interactive \
--tty \
--env PORT=8123 \
--publish 8123:8123 \
sentimentalyzer
위 명령어는 로컬 머신에서 Sentimentalyzer 서버를 http://localhost:8123 주소로 실행합니다.
이제 앱을 Docker 컨테이너에서 실행할 수 있으니, Cypress로 엔드투엔드 테스트를 만들 준비가 되었습니다.
엔드투엔드 테스트 만들기
첫 번째 Cypress 엔드투엔드 테스트를 작성하려면 세 개의 파일만 있으면 됩니다:
cypress.jsondocker-compose.ymlintegration/spec.js
cypress.json
이 파일은 Cypress의 설정 옵션을 지정합니다:
{
"pluginsFile": false,
"supportFile": false
}
이 설정들은 그다지 흥미로운 내용은 아니지만, 불필요한 헬퍼 파일이 자동으로 생성되는 것을 막기 위해 false로 설정했습니다.
docker-compose.yml
이 파일은 Sentimentalyzer용 Docker 컨테이너와 Cypress용 Docker 컨테이너를 정의하고, 두 컨테이너가 서로 통신할 수 있도록 합니다:
version: "3.2"
services:
sentimentalyzer:
build: ../
environment:
- PORT=8123
cypress:
image: "cypress/included:4.4.0"
depends_on:
- sentimentalyzer
environment:
- CYPRESS_baseUrl=http://sentimentalyzer:8123
working_dir: /e2e
volumes:
- ./:/e2e
몇 가지 주목할 만한 부분을 짚어보겠습니다:
image: "cypress/included:4.4.0"
cypress/included는 Cypress가 이미 설치된 상태로 제공되는 Cypress Docker 이미지 계열입니다. cypress/base나 cypress/browsers 같은 다른 계열은 클라이언트가 런타임에 Cypress를 설치한다고 가정합니다. cypress/included 이미지를 사용하면 컨테이너가 시작되자마자 Cypress가 테스트를 실행하도록 보장할 수 있습니다.
depends_on:
- sentimentalyzer
depends_on 구문을 사용하면 Sentimentalyzer가 완전히 실행된 뒤에 Cypress가 테스트를 시작하도록 보장할 수 있습니다.
environment:
- CYPRESS_baseUrl=http://sentimentalyzer:8123
CYPRESS_baseUrl 환경 변수는 Cypress가 Sentimentalyzer에 접근할 수 있는 URL을 알려줍니다. Cypress와 Sentimentalyzer가 동일한 Docker Compose 설정에서 실행되므로, Cypress는 컨테이너 이름(sentimentalyzer)을 호스트 이름으로 사용해 Sentimentalyzer에 네트워크 요청을 보낼 수 있습니다.
working_dir: /e2e
volumes:
- ./:/e2e
마지막으로 Docker의 볼륨 마운트 기능을 사용해 Cypress Docker 컨테이너가 호스트 머신의 파일 시스템 일부를 공유하도록 합니다.
호스트 머신의 ./e2e 디렉터리에 있는 모든 내용은 Docker 컨테이너 안의 /e2e 경로에 그대로 나타납니다. 덕분에 Cypress가 실행 중에 로그, 스크린샷, 비디오를 생성하면 컨테이너에서 호스트로 수동으로 복사할 필요 없이 즉시 호스트 머신에서 확인할 수 있습니다. 이런 방식으로 호스트 볼륨을 바인딩하면 전체 Docker 이미지를 다시 빌드하지 않고도 테스트를 쉽게 수정하고 다시 실행할 수 있습니다.
working_dir 항목은 Cypress가 파일 시스템에서 /e2e 디렉터리를 현재 작업 디렉터리로 인식하도록 합니다.
integration/spec.js
이제 설정을 마쳤으니 재미있는 부분인 테스트 작성으로 넘어가 보겠습니다.
it("detects angry sentiment", () => {
cy.visit("/analyze");
cy.get("#feelings").type("I REALLY need some COFFEE");
cy.get("form").submit();
cy.get(".results p").should("contain", "You are feeling: Angry");
});
it("detects content sentiment", () => {
cy.visit("/analyze");
cy.get("#feelings").type("I think coffee in the morning is just swell!");
cy.get("form").submit();
cy.get(".results p").should("contain", "You are feeling: Content");
});
Cypress API에 익숙하지 않더라도, 문법이 충분히 직관적이라 테스트 내용을 직감적으로 이해하실 수 있을 겁니다. 쉽게 풀어 설명하면 두 테스트 모두 같은 순서를 따릅니다:
- 브라우저에서 Sentimentalyzer 웹 앱의
/analyze경로로 이동한다. - 텍스트 필드를 찾는다.
- 텍스트를 입력한다.
- 폼을 제출한다.
- 결과를 확인한다.
첫 번째 테스트를 한 줄씩 살펴보겠습니다:
cy.visit("/analyze");
이 코드는 Cypress에게 Sentimentalyzer의 /analyze 경로를 브라우저에서 열도록 지시합니다. Cypress는 위에서 docker-compose.yml에 정의한 CYPRESS_baseUrl 환경 변수와 이 경로를 결합하므로 전체 URL은 http://sentimentalyzer:8123/analyze가 됩니다. 개발 머신에서는 이 URL에 직접 접속할 수 없지만, Cypress 컨테이너 안에서는 유효한 주소입니다.
cy.get("#feelings").type("I REALLY need some COFFEE");
다음으로 Cypress에게 텍스트 필드를 찾으라고 지시합니다. 텍스트 필드에는 feelings라는 고유한 ID가 있으므로 CSS 선택자 문법인 #feelings으로 요소를 지정하면 쉽게 찾을 수 있습니다.

type() 함수는 지정한 필드에 텍스트를 입력하라고 Cypress에 지시합니다.
다음으로 Cypress가 폼을 제출해야 합니다. Cypress는 이 흔한 작업을 위해 submit() 함수를 제공합니다. 페이지에는 <form> 요소가 하나뿐이므로, CSS 선택자인 form으로 쉽게 가져온 뒤 폼을 제출하면 됩니다:
cy.get("form").submit();
폼을 제출하면 Sentimentalyzer의 결과 페이지로 이동해야 합니다. Cypress는 "You are feeling: Angry"라는 텍스트가 있는지 확인해야 하는데, 해당 <p> 태그에는 ID 속성이 없어 조금 더 까다롭습니다:

이번에도 CSS 선택자 문법을 사용해 클래스가 "results"인 DOM 노드 아래의 <p> 요소를 지정하여 관련 텍스트를 찾습니다:
cy.get(".results p").should("contain", "You are feeling: Angry");
contain 어서션은 <p> 태그에 예상한 텍스트가 포함되어 있는지 검증합니다.
테스트 실행하기
이제 모든 준비가 끝났으니 Cypress가 동작하는 모습을 확인해 보겠습니다. 다음 간단한 명령어로 테스트를 실행합니다:
cd e2e
docker-compose up --exit-code-from cypress
--exit-code-from cypress 플래그는 Docker Compose에게 docker-compose 명령어의 종료 코드를 Cypress 컨테이너의 종료 코드로 사용하라고 지시합니다. 즉, 테스트가 통과하면 종료 코드가 0이 되고, 실패하면 0이 아닌 값이 됩니다. 이 동작은 명령어의 종료 코드를 이용해 성공 여부를 판단하는 빌드 스크립트나 지속적 통합 설정에 유용합니다.
콘솔에서 전체 과정은 다음과 같이 보입니다:
Cypress는 모든 테스트 실행을 비디오로 녹화합니다. 이 기능은 테스트 실패를 진단하는 데 큰 도움이 되기 때문에 제가 가장 좋아하는 기능입니다:
Cypress 녹화 영상 - 엔드투엔드 테스트 (1/4 속도로 느리게 재생)
테스트 실패 시 스크린샷
위에서는 통과한 테스트를 보여드렸습니다. Cypress 테스트가 실패하면 어떻게 될까요? 이 경우에도 테스트 실행 영상을 생성하지만, 실패한 어서션을 보여주는 스크린샷도 함께 출력합니다:

테스트가 실패했을 때 Cypress가 생성한 스크린샷 (Cypress는 “Furious”라는 단어를 예상했지만 대신 “Angry”를 발견함)
이는 제가 다른 도구에서 겪었던 큰 불편을 해결해 줍니다. Selenium도 스크린샷을 지원하지만 어서션 전이나 후에만 찍을 수 있습니다. 그 제한 때문에 Selenium이 테스트가 실패했다고 알리면서도 스크린샷에서는 올바른 동작이 보이는 답답한 상황이 종종 발생했습니다. 테스트가 실패한 뒤에 브라우저 상태가 바뀌었기 때문입니다.
Cypress는 스크린샷을 어서션과 동시에 찍기 때문에 이런 문제를 피할 수 있습니다. 테스트가 실패하면 스크린샷에 실패 시점에 Cypress가 본 화면이 정확히 담깁니다.
여러분의 웹 앱에 적용하기
이 세 파일만 있으면 웹 앱의 엔드투엔드 테스트를 시작할 수 있습니다. 단계는 다음과 같습니다:
- e2e 폴더를 여러분의 프로젝트에 복사합니다.
docker-compose.yml에서sentimentalyzer섹션을 여러분 앱의 Docker 컨테이너로 교체합니다.- 앱의 UI 흐름에 맞게
integration/spec.js를 다시 작성합니다.
소스 코드 및 추가 예제
이 데모의 전체 소스는 GitHub에서 확인하실 수 있습니다:
다른 일반적인 Cypress 시나리오를 보여주는 브랜치도 만들어 두었습니다:
- Circle CI에서 테스트를 실행하는 방법
- Travis CI에서 테스트를 실행하는 방법
- Chrome 브라우저에서 테스트를 실행하는 방법
- Firefox 브라우저에서 테스트를 실행하는 방법
더 읽어보기
이 가이드는 Cypress에 대한 기본적인 소개를 제공했습니다. 더 고급 기능은 공식 Cypress 문서를 확인해 보세요:
업데이트(2019-05-02): 이 글에 대한 응답으로 Cypress 팀은 Cypress가 미리 설치된 공식 Docker 이미지를 공개했습니다. 새로운 이미지를 반영해 이 튜토리얼을 수정했습니다. 이미지에 대한 추가 정보와 Cypress와 Docker를 함께 사용하는 더 많은 팁은 Cypress 블로그 글을 확인해 보세요.
일러스트: Loraine Yow. Cypress 팀의 Gleb Bahmutov님께 이 글에 대한 초기 피드백을 제공해 주셔서 감사드립니다.
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기