E2E 테스트 여정 1부: STUI에서 WebdriverIO로
게시 됨: 2019-11-21참고: 이것은 #frontend@twiliosendgrid의 게시물입니다. 다른 엔지니어링 게시물을 보려면 기술 블로그 롤로 이동하십시오.
SendGrid의 프론트엔드 아키텍처가 웹 애플리케이션 전반에서 성숙하기 시작함에 따라 우리는 일반적인 단위 및 통합 테스트 레이어 외에 또 다른 수준의 테스트를 추가하기를 원했습니다. 우리는 브라우저 자동화 도구를 사용하여 E2E(종단 간) 테스트 범위를 사용하여 새로운 페이지와 기능을 구축하려고 했습니다.
우리는 고객의 관점에서 테스트를 자동화하고 스택의 어느 부분에서나 발생할 수 있는 큰 변경 사항에 대해 가능한 수동 회귀 테스트를 피하기를 원했습니다. 우리는 프론트엔드 애플리케이션에 대해 일관되고 디버그 가능하고 유지 관리 가능하고 가치 있는 E2E 자동화 테스트를 작성하고 CICD(지속적 통합 및 지속적 배포)와 통합하는 방법을 제공한다는 목표 를 갖고 있었고 지금도 갖고 있습니다 .
우리는 E2E 테스트를 위한 이상적인 솔루션을 완성할 때까지 여러 기술적 접근 방식을 실험했습니다. 높은 수준에서 이것은 우리의 여정을 요약합니다.
- 자체 맞춤형 Ruby Selenium 솔루션 구축, SiteTestUI(일명 STUI)
- STUI에서 노드 기반 WebdriverIO로 전환
- 설정에 만족하지 못하고 마침내 Cypress로 마이그레이션
이 블로그 게시물은 도움이 되는 패턴 및 테스트 전략으로 E2E 테스트를 연결하는 방법에 대해 귀하와 다른 개발자를 안내하기 위해 사용된 각 접근 방식에 대한 우리의 경험, 교훈 및 절충안을 문서화하고 강조하는 두 부분 중 하나입니다.
1부에서는 STUI와의 초기 투쟁, WebdriverIO로 마이그레이션했지만 여전히 STUI로 유사한 몰락을 많이 경험한 방법을 다룹니다. WebdriverIO를 사용하여 테스트를 작성하고 컨테이너에서 실행하도록 테스트를 Docker화하고 궁극적으로 CICD 제공업체인 Buildkite와 테스트를 통합하는 방법을 살펴보겠습니다.
오늘 E2E 테스트가 있는 곳으로 건너뛰려면 STUI 및 WebdriverIO에서 Cypress로의 최종 마이그레이션과 여러 팀에서 설정하는 방법을 살펴보는 2부로 진행하십시오.
TLDR: 우리는 Selenium 래퍼 솔루션인 STUI와 WebdriverIO에서 비슷한 고통과 어려움을 겪었고 결국 Cypress에서 대안을 찾기 시작했습니다. 우리는 E2E 테스트 작성과 Docker 및 Buildkite와의 통합을 다루기 위해 많은 통찰력 있는 교훈을 배웠습니다.
목차:
E2E 테스트 첫 진출: STUI로 알려진 siteTESUI
STUI에서 WebdriverIO로 전환
1단계: WebdriverIO에 대한 종속성 결정
2단계: 환경 구성 및 스크립트
3단계: 로컬에서 ENE 테스트 구현
4단계: 모든 테스트 도커화
5단계: CICD와 통합
WebdriverIo와의 트레이드오프
사이프러스로 이동
E2E 테스트에 대한 첫 번째 진출: SiteTestUI(일명 STUI)
처음에 브라우저 자동화 도구를 찾을 때 우리의 SDET(테스트 중인 소프트웨어 개발 엔지니어)는 Ruby 및 Selenium, 특히 Rspec과 Gridium이라는 사용자 지정 Selenium 프레임워크로 구축된 자체 사용자 지정 사내 솔루션을 만드는 데 몰두했습니다. 우리는 브라우저 간 지원, QA(품질 보증) 엔지니어 테스트 사례를 위해 TestRail과의 자체 사용자 정의 통합을 구성할 수 있는 능력, 모든 프론트엔드 팀이 한 위치에서 E2E 테스트를 작성하고 일정에 따라 실행합니다.
우리를 위해 구축된 도구를 사용하여 처음으로 일부 E2E 테스트를 작성하고자 하는 프론트엔드 개발자로서 우리는 이미 출시한 페이지에 대한 테스트를 구현하기 시작했으며 사용자와 시드 데이터를 적절하게 설정하는 방법에 대해 숙고했습니다. 우리가 테스트하고 싶었던 기능. 도우미 기능을 구성하기 위한 페이지 개체 및 페이지별로 상호 작용하려는 요소 선택기를 구성하는 것과 같은 몇 가지 훌륭한 것을 배웠고 이 구조를 따르는 사양을 형성하기 시작했습니다.
우리는 유사한 패턴을 따라 동일한 리포지토리의 여러 팀에 걸쳐 상당한 테스트 스위트를 점진적으로 구축했지만 곧 다음과 같이 새로운 개발자와 STUI에 지속적으로 기여하는 사람들의 진행 속도를 크게 늦추는 많은 좌절을 경험했습니다.
- 시작하고 실행하려면 테스트 스위트를 실행하기 전에 모든 브라우저 드라이버, Ruby Gem 종속성 및 올바른 버전을 설치하는 데 상당한 시간과 노력이 필요 했습니다. 우리는 때때로 테스트가 한 사람의 컴퓨터와 다른 사람의 컴퓨터에서 실행되는 이유와 설정이 어떻게 다른지 파악해야 했습니다.
- 테스트 스위트가 급증하고 완료될 때까지 몇 시간 동안 실행되었습니다. 모든 팀이 동일한 리포지토리에 기여했기 때문에 모든 테스트를 연속적으로 실행하면 전체 테스트 제품군이 실행될 때까지 몇 시간을 기다려야 했고 여러 팀이 새 코드를 푸시할 때 잠재적으로 다른 곳에서 또 다른 테스트가 중단될 수 있었습니다.
- 우리는 불안정한 CSS 선택기와 복잡한 XPath 선택기에 불만을 갖게 되었습니다 . 아래의 이 그림은 XPath를 사용하여 상황을 더 복잡하게 만드는 방법을 충분히 설명하며 이것들은 더 간단한 것들 중 일부입니다.
- 테스트 디버깅은 고통스러웠 습니다. 모호한 오류 출력을 디버깅하는 데 문제가 있었고 일반적으로 어디에서 어떻게 실패했는지 알 수 없었습니다. 우리는 테스트를 반복적으로 실행하고 브라우저를 관찰하여 어디에서 실패했는지 그리고 어떤 코드가 실패했는지 추론할 수 있었습니다. CICD의 도커 환경에서 테스트가 실패했을 때 콘솔 출력 외에는 크게 볼 것이 없었을 때 로컬에서 재현하고 문제를 해결하기 위해 고군분투했습니다.
- Selenium 버그 및 속도 저하 가 발생했습니다. 테스트는 서버에서 브라우저로 전송되는 모든 요청으로 인해 느리게 실행되었으며 때때로 페이지에서 많은 요소를 선택하려고 할 때 또는 테스트 실행 중에 알 수 없는 이유로 테스트가 완전히 중단되었습니다.
- 테스트를 수정하고 건너뛰는 데 더 많은 시간이 소요되었으며 계획된 빌드 테스트 실행이 무시되기 시작했습니다. 테스트는 시스템의 실제 오류를 실제로 의미하는 데 가치를 제공하지 않았습니다.
- 우리 프론트엔드 팀은 E2E 테스트가 해당 웹 애플리케이션과 별도의 리포지토리에 존재하기 때문에 연결이 끊어졌다고 느꼈습니다 . 우리는 종종 두 저장소를 동시에 열어야 했고 테스트가 실행될 때 브라우저 탭 외에도 코드베이스 사이를 계속 앞뒤로 살펴봐야 했습니다.
- 프론트엔드 팀은 매일 JavaScript 또는 TypeScript로 코드를 작성하는 것에서 Ruby로 컨텍스트를 전환하고 STUI에 기여할 때마다 테스트 작성 방법을 다시 배워야 하는 것을 좋아하지 않았습니다.
- 테스트에 기여할 때 많은 사람들이 처음으로 시도했기 때문에 로그인을 위해 UI를 통해 상태를 구축하고, API를 통해 충분한 분해 또는 설정을 수행하지 않고, 문서가 충분하지 않은 것과 같은 많은 안티 패턴에 빠졌습니다. 훌륭한 시험이 되는 것을 따르기 위해.
하나의 리포지토리에서 여러 팀을 위한 상당한 수의 E2E 테스트를 작성하고 도움이 되는 몇 가지 패턴을 학습하는 방향으로 발전했음에도 불구하고 전반적인 개발자 경험, 여러 실패 지점 및 가치 있고 안정적인 테스트의 부족으로 골치 아픈 문제를 겪었습니다. 전체 스택을 확인합니다.
우리는 다른 프론트엔드 개발자와 QA가 테스트의 재사용, 근접성 및 소유권을 촉진하기 위해 자체 애플리케이션 코드와 함께 상주하는 JavaScript를 사용하여 안정적인 E2E 테스트 제품군을 구축할 수 있는 방법을 높이 평가했습니다. 이로 인해 우리는 맞춤형 Ruby Selenium 사내 솔루션인 STUI의 초기 대체품으로 브라우저 자동화 테스트를 위한 JavaScript 기반 Selenium 프레임워크인 WebdriverIO를 조사하게 되었습니다.
우리는 나중에 그 몰락을 경험하고 결국 Cypress로 이동할 것입니다(WebdriverIO 항목이 마음에 들지 않으면 여기에서 2부로 빨리 감). 그러나 우리는 각 팀의 리포지토리에 표준화된 인프라를 구축하고 프론트엔드용 CICD에 E2E 테스트를 통합하는 귀중한 경험을 얻었습니다. 우리의 여정에서 문서화할 가치가 있는 기술 패턴을 채택하고 다른 사람들이 WebdriverIO 또는 기타 E2E 테스트 솔루션에 뛰어들려는 사람에 대해 알 수 있도록 합니다.
STUI에서 WebdriverIO로 전환
우리가 경험한 좌절을 희망적으로 완화하기 위해 WebdriverIO를 시작할 때 우리는 각 프론트엔드 팀이 Ruby Selenium 접근 방식으로 작성된 기존 자동화 테스트를 JavaScript 또는 TypeScript의 WebdriverIO 테스트로 변환하고 안정성, 속도, 개발자 경험 및 전반적인 유지 관리를 비교하도록 실험했습니다. 테스트.
프론트엔드 팀의 애플리케이션 저장소에 상주하고 CICD 및 예정된 파이프라인 모두에서 실행되는 E2E 테스트의 이상적인 설정을 달성하기 위해 유사한 목표를 가진 E2E 테스트 프레임워크를 온보딩하려는 모든 팀에 일반적으로 적용되는 다음 단계를 요약했습니다. :
- 테스트 프레임워크와 연결할 종속성 설치 및 선택
- 환경 구성 및 스크립트 명령 설정
- 다른 환경에 대해 로컬로 통과하는 E2E 테스트 구현
- 테스트 도커화
- CICD 공급자와 Docker화된 테스트 통합
1단계: WebdriverIO에 대한 종속성 결정
WebdriverIO는 개발자에게 테스트 실행을 시작할 많은 프레임워크, 리포터 및 서비스 중에서 선택하고 선택할 수 있는 유연성을 제공합니다. 이를 위해서는 팀이 시작하기 위해 특정 라이브러리에 정착하기 위해 많은 수정과 연구가 필요했습니다.
WebdriverIO는 무엇을 사용할지에 대한 규범적이지 않기 때문에 프론트엔드 팀이 다양한 라이브러리와 구성을 가질 수 있는 기회를 제공했지만 전체 핵심 테스트는 WebdriverIO API를 사용할 때 일관되었습니다.
우리는 각 프론트엔드 팀이 선호도에 따라 맞춤화할 수 있도록 하기로 결정했으며 일반적으로 테스트 프레임워크로 Mocha, 리포터로 Mochawesome, Selenium Standalone 서비스 및 Typescript 지원을 사용했습니다. 우리 팀은 이전에 Mocha에 대한 친숙함과 이전 경험을 고려하여 Mocha와 Mochawesome을 선택했지만 다른 팀도 다른 대안을 사용하기로 결정했습니다.
2단계: 환경 구성 및 스크립트
WebdriverIO 인프라를 결정한 후 WebdriverIO 테스트를 각 환경에 대해 다양한 설정으로 실행할 방법이 필요했습니다. 다음은 이러한 테스트를 실행하고 지원하려는 이유에 대한 대부분의 사용 사례를 보여주는 목록입니다.
- localhost(예: http://localhost:8000)에서 실행되는 Webpack 개발 서버 에 대해 해당 개발 서버는 테스트 또는 스테이징(예: https://testing.api.com 또는 https://)과 같은 특정 환경 API를 가리킵니다. staging.api.com).
왜요? 더 강력한 방식으로 요소와 상호 작용하기 위해 테스트에 대해 보다 구체적인 선택기를 추가하는 것과 같이 로컬 웹 앱을 변경해야 하거나 새로운 기능을 개발하는 중에 기존 자동화 테스트를 조정하고 검증해야 하는 경우가 있습니다. 새로운 코드 변경 사항에 대해 로컬로 전달됩니다. 애플리케이션 코드가 변경되고 아직 배포된 환경으로 푸시하지 않을 때마다 이 명령을 사용하여 로컬 웹 앱에 대한 테스트를 실행했습니다. - 테스트 또는 스테이징과 같은 특정 환경(예: https://testing.app.com 또는 https://staging.app.com)에 배포된 앱에 대해
왜요? 다른 경우에는 애플리케이션 코드가 변경되지 않지만 약간의 결함을 수정하기 위해 테스트 코드를 변경해야 할 수도 있습니다. CICD 파이프라인에서 테스트가 실행되는 방식을 보다 밀접하게 시뮬레이션하기 위해 배포된 앱에 대해 로컬로 테스트를 업데이트하거나 디버그하는 데 이 명령을 많이 활용했습니다. - 테스트 또는 스테이징과 같은 특정 환경을 위해 배포된 앱에 대해 Docker 컨테이너 에서 실행
왜요? 이는 CICD 파이프라인을 위한 것이므로 예를 들어 스테이징 배포된 앱에 대해 Docker 컨테이너에서 실행되도록 E2E 테스트를 트리거하고 코드를 프로덕션에 배포하기 전에 또는 전용 파이프라인에서 예약된 테스트 실행에서 테스트를 통과하는지 확인할 수 있습니다. 이러한 명령을 처음 설정할 때 CICD 공급자인 Buildkite와 연결하기 전에 다양한 환경 변수 값으로 Docker 컨테이너를 실행하고 적절한 테스트가 성공적으로 실행되었는지 테스트하기 위해 많은 시행착오를 수행했습니다.
이를 달성하기 위해 각 환경 구성 파일이 기본 파일과 병합되고 실행 요청에 따라 속성을 덮어쓰거나 추가하도록 공유 속성과 많은 환경 특정 파일이 있는 일반 기본 구성 파일을 설정합니다. 기본 파일이 필요 없이 각 환경에 대해 하나의 파일을 가질 수 있었지만 공통 설정에서 많은 중복이 발생했습니다. 우리는 그것을 처리하기 위해 deepmerge
와 같은 라이브러리를 사용하기로 선택했지만 병합이 중첩된 개체 또는 배열과 항상 완벽하지는 않다는 점에 유의하는 것이 중요합니다. 올바르게 병합되지 않은 중복 속성이 있는 경우 정의되지 않은 동작으로 이어질 수 있으므로 결과 출력 구성을 항상 다시 확인하십시오.
다음과 같이 공통 기본 구성 파일 wdio.conf.js
를 구성했습니다.
환경 API를 가리키는 로컬 웹팩 개발 서버에 대해 E2E 테스트를 실행하는 첫 번째 주요 사용 사례에 맞추기 위해 다음과 같이 localhost 구성 파일 wdio.localhost.conf.js
를 생성했습니다.
기본 파일을 병합하고 localhost 특정 속성을 파일에 추가하여 파일을 더 압축하고 유지 관리하기 쉽게 만들었습니다. 또한 Selenium Standalone 서비스를 사용하여 다양한 유형의 브라우저(기능)를 가동합니다.
배포된 웹 앱에 대해 E2E 테스트를 실행하는 두 번째 사용 사례의 경우 테스트 및 스테이징 앱 구성 파일 `wdio.testing.conf.js` 및 wdio.staging.conf.js
를 다음과 유사하게 설정합니다.
여기에서 스테이징 전용 사용자에 대한 로그인 자격 증명과 같은 몇 가지 추가 환경 변수를 구성 파일에 추가하고 배포된 스테이징 앱 URL을 가리키도록 'baseUrl'을 업데이트했습니다.
CICD 공급자 영역 내 배포된 웹 앱에 대해 Docker 컨테이너에서 E2E 테스트를 실행하는 세 번째 사용 사례의 경우 CICD 구성 파일 wdio.cicd.testing.conf.js
및 wdio.cicd.staging.conf.js
와 같이:
나중에 Docker Compose 파일의 별도 서비스에 Selenium Chrome, Selenium Hub 및 애플리케이션 코드를 설치할 것이기 때문에 Selenium Standalone 서비스를 더 이상 사용하지 않는 방법에 주목하세요. 이 구성은 또한 로그인 자격 증명 및 'baseUrl'과 같은 스테이징 구성과 동일한 환경 변수를 표시합니다. 배포된 스테이징 앱에 대해 테스트를 실행할 것으로 예상하고 유일한 차이점은 이러한 테스트가 Docker 컨테이너 내에서 실행된다는 것입니다. .
이러한 환경 구성 파일이 설정되면 테스트의 기반이 될 package.json
스크립트 명령을 간략하게 설명했습니다. 이 예에서는 WebdriverIO로 UI 테스트를 나타내기 위해 명령 앞에 "uitest"를 붙였습니다. 테스트 파일도 *.uitest.js
로 끝냈기 때문입니다. 다음은 스테이징 환경에 대한 몇 가지 샘플 명령입니다.
3단계: 로컬에서 E2E 테스트 구현
모든 테스트 명령을 사용하여 WebdriverIO 테스트로 변환할 수 있도록 STUI 리포지토리에서 테스트 범위를 지정했습니다. 중소 규모의 페이지 테스트에 집중하고 페이지 개체 패턴을 적용하여 각 페이지의 모든 UI를 체계적으로 캡슐화하기 시작했습니다.
우리는 많은 헬퍼 함수나 객체 리터럴 또는 다른 전략으로 구조화된 파일을 가질 수 있었지만 핵심은 유지 관리 가능한 테스트를 신속하게 제공하고 이를 고수하는 일관된 방법을 갖는 것이었습니다. 특정 페이지에 대해 UI 흐름 또는 DOM 요소가 변경된 경우 관련 페이지 개체와 테스트 코드를 리팩터링하기만 하면 테스트를 다시 통과할 수 있습니다.
다른 모든 페이지 개체가 확장되는 공유 기능을 가진 기본 페이지 개체를 사용하여 페이지 개체 패턴을 구현했습니다. 브라우저에서 페이지의 URL을 "열거나" 방문하기 위해 모든 페이지 개체에 일관된 API를 제공하기 위해 open과 같은 기능이 있었습니다. 다음과 같았습니다.
특정 페이지 개체를 구현하는 것은 기본 Page
클래스에서 확장하고 상호 작용하거나 주장하려는 특정 요소에 선택기를 추가하고 페이지에서 작업을 수행하기 위한 도우미 기능을 추가하는 것과 동일한 패턴을 따랐습니다.
SomePage.open()
호출로 페이지를 방문할 수 있도록 페이지의 특정 경로와 함께 super.open(...)
을 통해 기본 클래스를 여는 방법을 주목하세요. 또한 SomePage.submitButton
또는 SomePage.tableRows
와 같은 요소를 참조하고 WebdriverIO 명령을 사용하여 해당 요소와 상호 작용하거나 해당 요소에 대해 주장할 수 있도록 이미 초기화된 클래스를 내보냈습니다. 페이지 객체가 생성자에서 자체 멤버 속성으로 공유되고 초기화되어야 하는 경우 클래스를 직접 내보내고 new SomePage(...constructorArgs)
를 사용하여 테스트 파일에서 페이지 객체를 인스턴스화할 수도 있습니다.
선택기와 일부 도우미 기능이 있는 페이지 개체를 배치한 후 E2E 테스트를 작성하고 일반적으로 이 테스트 공식을 모델링했습니다.
- 실제 테스트를 실행하기 전에 테스트 조건을 예상 시작점으로 재설정하는 데 필요한 항목을 API를 통해 설정하거나 해제합니다.
- 테스트 를 위해 전용 사용자로 로그인 하여 페이지를 직접 방문할 때마다 로그인 상태를 유지하고 UI를 통과할 필요가 없습니다. 우리는 로그인 페이지에 사용하는 것과 동일한 API 호출을 하고 결국 로그인을 유지하고 보호된 API 요청의 헤더를 전달하는 데 필요한 인증 토큰을 반환하는 사용자 이름과 비밀번호를 사용하는 간단한
login
도우미 함수를 만들었습니다. 다른 회사에는 시드 데이터 및 구성을 사용하여 새로운 사용자를 신속하게 생성할 수 있는 더 많은 사용자 지정 내부 엔드포인트 또는 도구가 있을 수 있지만 불행히도 충분히 구체화되지 않았습니다. 우리는 구식 방식으로 수행하고 UI를 통해 다양한 구성으로 환경에 전용 테스트 사용자를 생성하고 종종 리소스 충돌을 방지하고 테스트가 병렬로 실행될 때 격리된 상태로 유지하기 위해 고유한 사용자가 있는 페이지에 대한 테스트를 분할했습니다. 우리는 전용 테스트 사용자가 다른 사용자에 의해 건드리지 않았는지 확인해야 했습니다. 그렇지 않으면 누군가가 무의식적으로 그 중 하나를 만졌을 때 테스트가 중단될 수 있었습니다. - 최종 사용자가 기능/페이지와 상호 작용하는 것처럼 단계를 자동화합니다. 일반적으로 기능 흐름이 있는 페이지를 방문하여 최종 사용자가 입력을 채우고, 버튼을 클릭하고, 모달이나 배너가 나타나기를 기다리고, 다음과 같이 변경된 출력에 대한 테이블을 관찰하는 것과 같은 상위 수준 단계를 따르기 시작합니다. 행동의 결과. 편리한 페이지 개체와 선택기를 사용하여 각 단계를 신속하게 구현했으며, 그 과정에서 온전한 상태를 확인하면서 사용자가 기능 흐름 중에 페이지에서 어떤 항목이 예상대로 작동하는지 확인하거나 표시해서는 안 된다고 주장합니다. 각 단계 전후. 우리는 또한 높은 가치의 행복한 경로 테스트와 때로는 쉽게 재현할 수 있는 일반적인 오류 상태를 선택하는 것에 대해 신중했고 나머지 하위 수준 테스트는 단위 및 통합 테스트로 연기했습니다.
다음은 E2E 테스트의 일반적인 레이아웃에 대한 대략적인 예입니다(이 전략은 우리가 시도한 다른 테스트 프레임워크에도 적용됨).
참고로, 이 블로그 게시물 시리즈에서 WebdriverIO 및 E2E 모범 사례에 대한 모든 팁과 잡다한 내용을 다루지는 않았지만 향후 블로그 게시물에서 이러한 주제에 대해 이야기할 예정이므로 계속 지켜봐 주십시오!
4단계: 모든 테스트 도커화
클라우드의 새 AWS 머신에서 각 Buildkite 파이프라인 단계를 실행할 때 "npm run uitests:staging"을 단순히 호출할 수 없었습니다. 해당 머신에는 실제로 테스트를 실행할 노드, 브라우저, 애플리케이션 코드 또는 기타 종속성이 없기 때문입니다. .
이를 해결하기 위해 WebdriverIO 테스트가 성공적으로 실행될 수 있도록 Node, Selenium, Chrome 및 애플리케이션 코드와 같은 모든 종속성을 Docker 컨테이너에 번들로 묶었습니다. 우리는 Docker 및 Docker Compose를 활용하여 시작 및 실행에 필요한 모든 서비스를 어셈블했으며, 이 서비스는 Dockerfiles
및 docker-compose.yml
파일로 변환되었으며 Docker 컨테이너를 로컬로 회전하여 작동하도록 하는 많은 실험을 했습니다.
더 많은 컨텍스트를 제공하기 위해 우리는 Docker의 전문가가 아니었기 때문에 모든 것을 통합하는 방법을 이해하는 데 상당한 시간이 걸렸습니다. WebdriverIO 테스트를 Dockerize하는 방법에는 여러 가지가 있으며 다양한 서비스를 함께 오케스트레이션하고 문제가 해결될 때까지 다양한 Docker 이미지, Compose 버전 및 자습서를 살펴보는 것이 어렵다는 것을 알았습니다.
우리 팀의 구성 중 하나와 일치하는 대부분의 파일을 시연할 것이며 이것이 Selenium 기반 테스트 Dockerizing의 일반적인 문제를 해결하는 모든 사람에게 통찰력을 제공하기를 바랍니다.
높은 수준에서 테스트는 다음을 요구했습니다.
- Selenium 은 브라우저에 대해 명령을 실행하고 브라우저와 통신합니다. Selenium Hub를 사용하여 원하는 대로 여러 인스턴스를 스핀업하고 docker-compose 파일에서
selenium-hub
서비스에 대한 이미지 "selenium/hub"를 다운로드했습니다. - 실행할 브라우저 입니다. Selenium Chrome 인스턴스를 불러오고
docker-compose.yml file
에selenium-chrome
서비스용 이미지 "selenium/node-chrome-debug"를 설치했습니다. - 설치된 다른 노드 모듈과 함께 테스트 파일을 실행하기 위한 애플리케이션 코드 . Node가 있는 환경을 제공하여 npm 패키지를 설치하고
package.json
스크립트를 실행하고, 테스트 코드를 복사하고,docker-compose.yml
파일에서uitests
라는 테스트 파일 실행 전용 서비스를 할당하기 위해 새로운Dockerfile
을 생성했습니다.
WebdriverIO 테스트를 실행하는 데 필요한 모든 애플리케이션 및 테스트 코드로 서비스를 시작하기 위해 Dockerfile
라는 Dockerfile.uitests
을 만들고 모든 node_modules
를 설치하고 코드를 Node 환경의 이미지 작업 디렉터리에 복사했습니다. 이것은 uitests
Docker Compose 서비스에서 사용되며 다음과 같은 방식으로 Dockerfile
설정을 달성했습니다.
WebdriverIO 테스트를 실행하기 위해 Selenium Hub, Chrome 브라우저 및 애플리케이션 테스트 코드를 함께 불러오기 위해 docker-compose.uitests.yml
파일에 selenium-hub
, selenium-chrom
e 및 uitest
s 서비스의 개요를 설명했습니다. :
Selenium Hub와 Chrome 이미지를 환경 변수 depends_on
을 통해 연결하고 포트를 서비스에 노출했습니다. 테스트 애플리케이션 코드 이미지는 결국 우리가 관리하는 개인 Docker 레지스트리에서 푸시업 및 풀링됩니다.
태그 및 보다 구체적인 이름으로 이미지를 참조하기 위해 VERSION
및 PIPELINE_SUFFIX
와 같은 특정 환경 변수를 사용하여 CICD 동안 테스트 코드용 Docker 이미지를 빌드합니다. 그런 다음 Selenium 서비스를 시작하고 uitests
서비스를 통해 명령을 실행하여 WebdriverIO 테스트를 실행합니다.
Docker Compose 파일을 구축하면서 우리는 시스템에 설치된 Mac Docker와 함께 docker docker-compose up
및 docker-compose down
과 같은 유용한 명령을 활용하여 Buildkite와 통합하기 전에 이미지가 적절한 구성을 갖고 원활하게 실행되었는지 로컬로 테스트했습니다. 태그가 지정된 이미지를 구성하고, 레지스트리로 푸시하고, 풀다운하고, 환경 변수 값에 따라 테스트를 실행하는 데 필요한 모든 명령을 문서화했습니다.
5단계: CICD와 통합
작동하는 Docker 명령을 설정하고 다양한 환경에 대해 Docker 컨테이너 내에서 테스트를 성공적으로 실행한 후 CICD 제공업체인 Buildkite와 통합하기 시작했습니다.
Buildkite는 리포지토리 파이프라인에 대한 코드 또는 Buildkite 설정 UI를 통해 설정된 Bash 스크립트 및 환경 변수를 사용하여 AWS 머신에서 .yml
파일의 단계를 실행하는 방법을 제공했습니다.
또한 Buildkite를 사용하여 내보낸 환경 변수를 사용하여 기본 배포 파이프라인에서 이 테스트 파이프라인을 트리거할 수 있었고 QA가 모니터링하고 볼 일정에 따라 실행되는 다른 격리된 테스트 파이프라인에 대해 이러한 테스트 단계를 재사용했습니다.
높은 수준에서 WebdriverIO 및 이후 Cypress에 대한 테스트 Buildkite 파이프라인은 다음과 유사한 단계를 공유했습니다.
- Docker 이미지를 설정합니다 . 테스트에 필요한 Docker 이미지를 빌드하고 태그를 지정하고 레지스트리로 푸시하면 이후 단계에서 풀다운할 수 있습니다.
- 환경 변수 구성을 기반으로 테스트를 실행합니다 . 특정 빌드에 대해 태그가 지정된 Docker 이미지를 풀다운하고 배포된 환경에 대해 적절한 명령을 실행하여 설정된 환경 변수에서 선택한 테스트 제품군을 실행합니다.
다음은 "UITests Docker 이미지 빌드" 단계에서 Docker 이미지를 설정하고 "Chrome에 대해 Webdriver 테스트 실행" 단계에서 테스트를 실행하는 방법을 보여주는 pipeline.uitests.yml
파일의 가까운 예입니다.
한 가지 주목해야 할 것은 첫 번째 단계인 "UITests Docker 이미지 빌드"와 테스트를 위해 Docker 이미지를 설정하는 방법입니다. Docker Compose build
명령을 사용하여 모든 애플리케이션 테스트 코드로 uitests
서비스를 빌드하고 latest
및 ${VERSION}
환경 변수로 태그를 지정하여 나중에 이 빌드에 대한 적절한 태그로 동일한 이미지를 풀다운할 수 있습니다. 단계.
각 단계는 AWS 클라우드 어딘가에 있는 다른 머신에서 실행될 수 있으므로 태그는 특정 Buildkite 실행에 대한 이미지를 고유하게 식별합니다. 이미지에 태그를 지정한 후 재사용할 수 있도록 최신 버전과 태그가 지정된 이미지를 비공개 Docker 레지스트리로 푸시했습니다.
"Chrome에 대해 Webdriver 테스트 실행" 단계에서는 첫 번째 단계에서 빌드, 태그 지정 및 푸시한 이미지를 풀다운하고 Selenium Hub, Chrome 및 테스트 서비스를 시작합니다. $UITESTENV
및 $UITESTSUITE
UITESTSUITE 와 같은 환경 변수를 기반으로 npm run uitest:
와 같이 실행할 명령 유형과 --suite $UITESTSUITE
와 같은 이 특정 Buildkite 빌드에 대해 실행할 테스트 스위트를 선택하고 선택합니다.
이러한 환경 변수는 Buildkite 파이프라인 설정을 통해 설정되거나 Bash 스크립트에서 동적으로 트리거되어 Buildkite 선택 필드를 구문 분석하여 실행할 테스트 제품군과 환경을 결정합니다.
다음은 전용 테스트 파이프라인에서 트리거된 WebdriverIO 테스트의 예입니다. 이 파이프라인은 동일한 pipeline.uitests.yml
파일도 재사용했지만 파이프라인이 트리거된 위치에 환경 변수가 설정되어 있습니다. 이 빌드는 실패했으며 Artifacts
탭 아래에서 살펴보고 Logs
탭에서 콘솔 출력을 볼 수 있는 오류 스크린샷이 있습니다. pipeline.uitests.yml
.uitests.yml(https://gist.github.com/alfredlucero/71032a82f3a72cb2128361c08edbcff2#file-pipeline-uitests-yml-L38)의 artifact_paths
, `wdio.conf'의 `mochawesome` 설정 스크린샷을 기억하세요. ` 파일(https://gist.github.com/alfredlucero/4ee280be0e0674048974520b79dc993a#file-wdio-conf-js-L39) 및 `docker-compose.uitests.yml`의 `uitests` 서비스에 볼륨 마운트 (https://gist.github.com/alfredlucero/d2df4533a4a49d5b2f2c4a0eb5590ff8#file-docker-compose-yml-L32)?
우리는 직접 다운로드할 수 있도록 Buildkite UI를 통해 액세스할 수 있는 스크린샷을 연결할 수 있었고 아래와 같이 테스트 디버깅에 도움이 되도록 바로 확인할 수 있었습니다.
Buildkite 파이프라인 설정에 이미 구성된 환경 변수를 제외하고 pipeline.uitests.yml
라인.uitests.yml 파일을 사용하여 특정 페이지의 일정에 따라 별도의 파이프라인에서 실행되는 WebdriverIO 테스트의 또 다른 예가 아래에 표시됩니다.
특정 구문, GUI 설정, Bash 스크립트 또는 기타 수단이 포함된 .yml
파일을 통해 새 코드를 병합할 때 모든 CICD 공급자는 단계를 일종의 배포 프로세스에 통합하는 기능과 방법이 다릅니다.
Jenkins에서 Buildkite로 전환했을 때 팀이 각자의 코드베이스 내에서 자체 파이프라인을 정의하고 필요에 따라 머신을 확장하는 단계를 병렬화하고 더 읽기 쉬운 명령을 활용하는 기능을 크게 개선했습니다.
사용하는 CICD 공급자에 관계없이 테스트 통합 전략은 이식성 및 유연성을 위해 환경 변수를 기반으로 Docker 이미지를 설정하고 테스트를 실행하는 것과 유사합니다.
WebdriverIO와의 트레이드오프
상당한 수의 사용자 지정 Ruby Selenium 솔루션 테스트를 WebdriverIO 테스트로 변환하고 Docker 및 Buildkite와 통합한 후 일부 영역에서는 개선되었지만 여전히 이전 시스템과 유사한 어려움을 느꼈고 결국에는 Cypress for 우리의 E2E 테스트 솔루션.
다음은 사용자 지정 Ruby Selenium 솔루션과 비교하여 WebdriverIO에 대한 경험에서 찾은 몇 가지 장점 목록입니다.
- 테스트는 Ruby가 아닌 JavaScript 또는 TypeScript로 순전히 작성되었습니다 . 이는 언어 간 컨텍스트 전환이 줄어들고 E2E 테스트를 작성할 때마다 Ruby를 다시 배우는 데 소요되는 시간이 줄어듦을 의미합니다.
- Ruby 공유 리포지토리에 테스트를 배치하지 않고 애플리케이션 코드와 함께 테스트를 배치 했습니다. 우리는 더 이상 다른 팀의 테스트 실패에 의존하지 않고 리포지토리의 기능에 대한 E2E 테스트의 직접적인 소유권을 가져갔습니다.
- 우리는 브라우저 간 테스트 옵션을 높이 평가했습니다 . WebdriverIO를 사용하면 Chrome, Firefox 및 IE와 같은 다양한 기능이나 브라우저에 대한 테스트를 실행할 수 있지만 사용자의 80% 이상이 Chrome을 통해 앱을 방문했기 때문에 주로 Chrome에 대한 테스트를 실행하는 데 중점을 두었습니다.
- 우리는 타사 서비스와의 통합 가능성을 즐겼습니다 . WebdriverIO 문서는 BrowserStack 및 SauceLabs와 같은 타사 서비스와 통합하여 모든 장치 및 브라우저에서 앱을 다루는 데 도움이 되는 방법을 설명했습니다.
- 자체 테스트 러너, 리포터 및 서비스를 선택할 수 있는 유연성이 있었습니다 . WebdriverIO는 무엇을 사용할지에 대한 규정이 없었으므로 각 팀은 Mocha 및 Chai 또는 Jest 및 기타 서비스와 같은 것을 사용할지 여부를 자유롭게 결정할 수 있었습니다. 이것은 팀이 서로의 설정에서 벗어나기 시작했고 우리가 선택할 각 옵션을 실험하는 데 상당한 시간이 필요했기 때문에 단점으로 해석될 수도 있습니다.
- WebdriverIO API, CLI 및 설명서는 테스트를 작성하고 Docker 및 CIC D와 통합하기에 충분히 서비스 가능했습니다 . 다양한 구성 파일을 갖고, 사양을 그룹화하고, 명령줄을 통해 테스트를 실행하고, 페이지 개체 패턴에 따라 테스트를 작성할 수 있습니다. 그러나 문서는 더 명확할 수 있었고 우리는 많은 이상한 버그를 파헤쳐야 했습니다. 그럼에도 불구하고 Ruby Selenium 솔루션에서 테스트를 변환할 수 있었습니다.
우리는 이전 Ruby Selenium 솔루션에서 부족했던 많은 영역에서 진전을 이루었지만 다음과 같이 WebdriverIO를 사용하지 못하게 막는 많은 쇼스토퍼를 만났습니다.
- WebdriverIO는 여전히 Selenium 기반 이기 때문에 이상한 시간 초과, 충돌 및 버그가 많이 발생하여 이전 Ruby Selenium 솔루션의 부정적인 플래시백을 상기시킵니다. 때로는 페이지에서 많은 요소를 선택하고 테스트가 원하는 것보다 느리게 실행될 때 테스트가 완전히 중단됩니다. 테스트를 작성할 때 많은 Github 문제를 통해 해결 방법을 알아내거나 특정 방법론을 피해야 했습니다.
- 전반적인 개발자 경험은 차선책이었습니다 . 설명서는 명령에 대한 높은 수준의 개요를 제공했지만 모든 사용 방법을 설명하기에 충분한 예제는 아닙니다. 우리는 Ruby로 E2E 테스트를 작성하는 것을 피했고 마침내 JavaScript나 TypeScript로 테스트를 작성하게 되었지만 WebdriverIO API는 다루기가 약간 혼란스러웠습니다. 몇 가지 일반적인 예는 단수 요소와 복수 요소에 대한
$
대$$
, 요소가 표시되지 않을 때까지 기다리는$('...').waitForVisible(9000, true)
및 기타 직관적이지 않은 명령입니다. 우리는 불안정한 선택자를 많이 경험했고 모든 것에 대해 명시적으로$(...).waitForVisible()
을 사용해야 했습니다. - 디버깅 테스트는 개발자와 QA에게 극도로 고통스럽고 지루 했습니다. Whenever tests failed, we only had screenshots, which would often be blank or not capturing the right moment for us to deduce what went wrong, and vague console error messages that did not point us in the right direction of how to solve the problem and even where the issue occurred. We often had to re-run the tests many times and stare closely at the Chrome browser running the tests to hopefully put things together as to where in the code our tests failed. We used things like
browser.debug()
but it often did not work or did not provide enough information. We gradually gathered a bunch of console error messages and mapped them to possible solutions over time but it took lots of pain and headache to get there. - WebdriverIO tests were tough to set up with Docker . We struggled with trying to incorporate it into Docker as there were many tutorials and ways to do things in articles online, but it was hard to figure out a way that worked in general. Hooking up 2 to 3 services together with all these configurations led to long trial and error experiments and the documentation did not guide us enough in how to do that.
- Choosing the test runner, reporter, assertions, and services demanded lots of research time upfront . Since WebdriverIO was flexible enough to allow other options, many teams had to spend plenty of time to even have a solid WebdriverIO infrastructure after experimenting with a lot of different choices and each team can have a completely different setup that doesn't transfer over well for shared knowledge and reuse.
To summarize our WebdriverIO and STUI comparison, we analyzed the overall developer experience (related to tools, writing tests, debugging, API, documentation, etc.), test run times, test passing rates, and maintenance as displayed in this table:
Moving On to Cypress
At the end of the day, our WebdriverIO tests were still flaky and tough to maintain. More time was still spent debugging tests in dealing with weird Selenium issues, vague console errors, and somewhat useful screenshots than actually reaping the benefits of seeing tests fail for when the backend or frontend encountered issues.
We appreciated cross-browser testing and implementing tests in JavaScript, but if our tests could not pass consistently without much headache for even Chrome, then it became no longer worth it and we would then simply have a STUI 2.0.
With WebdriverIO we still strayed from the crucial aspect of providing a way to write consistent, debuggable, maintainable, and valuable E2E automation tests for our frontend applications in our original goal. Overall, we learned a lot about integrating with Buildkite and Docker, using page objects, and outlining tests in a structured way that will transfer over to our final solution with Cypress.
If we felt it was necessary to run our tests in multiple browsers and against various third-party services, we could always circle back to having some tests written with WebdriverIO, or if we needed something fully custom, we would revisit the STUI solution.
Ultimately, neither solution met our main goal for E2E tests, so follow us on our journey in how we migrated from STUI and WebdriverIO to Cypress in part 2 of the blog post series.