Cypress를 사용한 시각적 회귀 테스트: 실용적인 접근 방식

게시 됨: 2022-03-11

구성 요소 라이브러리인 Picasso의 새 버전이 출시될 때마다 새 기능을 최대한 활용하고 사이트의 모든 부분에서 디자인을 정렬하기 위해 모든 프런트 엔드 응용 프로그램을 업데이트합니다.

지난 달, 우리는 인재가 일자리를 찾고 클라이언트와 상호 작용하는 데 사용하는 플랫폼인 Toptal Talent Portal에 대한 Picasso 업데이트를 출시했습니다. 릴리스에는 주요 디자인 변경 사항이 수반되며 예상치 못한 문제를 최소화하기 위해 시각적 회귀 테스트 기술을 사용하여 릴리스 전에 문제를 찾는 데 도움이 된다는 것을 알고 있었습니다.

시각적 회귀 테스트는 새로운 개념이 아닙니다. Picasso 자체를 포함하여 Toptal의 다른 많은 프로젝트에서 이미 이를 사용하고 있습니다.

Percy, Happo 및 Chromatic과 같은 도구는 팀이 건강한 시각적 회귀 파이프라인을 구축하는 데 사용할 수 있으며 처음에는 추가를 고려했습니다. 우리는 궁극적으로 설정 프로세스가 너무 시간이 많이 걸리고 일정이 지연될 수 있다고 결정했습니다. 마이그레이션을 시작하기 위한 코드 동결 날짜가 이미 정해져 있었고 마감일까지 며칠밖에 남지 않았기 때문에 창의력을 발휘할 수 밖에 없었습니다.

UI 테스팅을 통한 시각적 회귀 테스팅

프로젝트에서 시각적 회귀 테스트는 없었지만 Cypress를 사용한 UI 통합 테스트는 잘 다루었습니다. 도구가 주로 사용되는 것은 아니지만 Cypress의 문서에는 시각적 테스트 전용 페이지가 있고 다른 페이지에는 시각적 테스트를 위해 Cypress를 구성하는 데 도움이 되는 사용 가능한 모든 플러그인이 나열되어 있습니다.

Cypress에서 스크린샷으로

사용 가능한 문서를 살펴본 후 cypress-snapshot-plugin을 사용해 보기로 결정했습니다. 설정하는 데 몇 분 밖에 걸리지 않았으며 설정하고 나면 기존의 시각적 회귀 출력을 추구하지 않는다는 것을 빠르게 깨달았습니다.

대부분의 시각적 회귀 도구는 스냅샷을 비교하고 알려진, 수용된 기준선과 페이지 또는 구성 요소의 수정된 버전 간의 픽셀 차이를 감지하여 원치 않는 변경을 식별하는 데 도움이 됩니다. 픽셀 차이가 설정된 허용 오차 임계값보다 크면 페이지 또는 구성 요소에 수동으로 검사하도록 플래그가 지정됩니다. 그러나 이번 릴리스에서는 대부분의 UI 구성 요소에 몇 가지 작은 변경 사항이 있을 것이라는 것을 알고 있었기 때문에 임계값을 설정할 수 없었습니다. 주어진 구성 요소가 100% 다른 경우에도 새 버전의 컨텍스트에서는 여전히 정확할 수 있습니다. 유사하게, 몇 픽셀만큼 작은 편차는 구성 요소가 현재 생산에 적합하지 않다는 것을 의미할 수 있습니다.

테스트 실행의 예상 결과와 실제 결과를 나타내는 스크린샷.
그림 1. 위음성을 유발하는 사소한 픽셀 차이의 예

그 시점에서 두 가지 대조되는 사실이 명확해졌습니다. 픽셀 차이를 알아차리는 것은 문제를 식별하는 데 도움이 되지 않으며 구성 요소를 나란히 비교하는 것이 정확히 우리가 필요로 하는 것이었습니다. 우리는 스냅샷 플러그인을 제쳐두고 Picasso 업데이트가 적용되기 전과 후에 구성 요소로 이미지 컬렉션을 만들기 시작했습니다. 그렇게 하면 모든 변경 사항을 빠르게 살펴보고 새 버전이 여전히 사이트의 요구 사항과 라이브러리의 표준과 일치하는지 확인할 수 있습니다.

새로운 계획은 구성 요소의 스크린샷을 찍어 로컬에 저장하고 업데이트된 Picasso 버전이 있는 분기에서 동일한 구성 요소의 새 스크린샷을 만든 다음 단일 이미지로 병합하는 것이었습니다. 궁극적으로 이 새로운 접근 방식은 우리가 시작한 것과 크게 다르지 않았지만 플러그인을 가져오고 새 명령을 사용할 필요가 더 이상 없기 때문에 구현 단계에서 더 많은 유연성을 제공했습니다.

시각적 비교 흐름을 보여주는 다이어그램, 시각적 테스트 실행 후 새 버전과 이전 버전의 이미지가 병합되는 방법.
그림 2. 시각적 비교 흐름

비교 이미지를 위한 API 활용

명확한 목표를 염두에 두고 Cypress가 우리가 필요한 스크린샷을 얻는 데 어떻게 도움을 줄 수 있는지 살펴볼 때였습니다. 언급한 바와 같이, 우리는 Talent Portal의 대부분을 다루는 많은 양의 UI 테스트를 가지고 있었기 때문에 가능한 한 많은 중요한 구성 요소를 수집하기 위해 각 상호 작용 후에 개별 요소의 스크린샷을 찍기로 결정했습니다.

대안적인 접근 방식은 테스트 중 중요한 순간에 전체 페이지의 스크린샷을 찍는 것이지만 해당 이미지를 비교하기에는 너무 어려울 것이라고 결정했습니다. 또한 이러한 비교는 바닥글이 변경된 것을 놓치는 것과 같이 인적 오류가 발생하기 쉽습니다.

세 번째 옵션은 캡처할 항목을 결정하기 위해 모든 단일 테스트 사례를 검토하는 것이지만 훨씬 더 많은 시간이 소요되므로 페이지에 사용된 모든 요소를 ​​고수하는 것은 실용적인 절충안처럼 보였습니다.

우리는 이미지를 생성하기 위해 Cypress의 API를 사용했습니다. cy.screenshot() 명령은 기본적으로 구성 요소의 개별 이미지를 생성할 수 있으며 After Screenshot API를 사용하면 파일 이름을 변경하고 디렉토리를 변경하며 시각적 회귀 실행을 표준 실행과 구별할 수 있습니다. 두 가지를 결합하여 기능 테스트에 영향을 미치지 않는 실행을 생성하고 적절한 폴더에 이미지를 저장할 수 있었습니다.

먼저 플러그인 디렉토리의 index.js 파일을 확장하여 두 가지 새로운 실행 유형(기준 및 비교)을 지원했습니다. 그런 다음 실행 유형에 따라 이미지의 경로를 설정합니다.

 // plugins/index.js const fs = require('fs') const path = require('path') module.exports = (on, config) => { // Adding these values to your config object allows you to access them in your tests. config.env.baseline = process.env.BASELINE || false config.env.comparison = process.env.COMPARISON || false on('after:screenshot', details => { // We only want to modify the behavior of baseline and comparison runs. if (config.env.baseline || config.env.comparison) { // We keep track of the file name and number to make sure they are saved in the proper order and in their relevant folders. // An alternative would have been to look up the folder for the latest image, but this was the simpler approach. let lastScreenshotFile = '' let lastScreenshotNumber = 0 // We append the proper suffix number to the image, create the folder, and move the file. const createDirAndRename = filePath => { if (lastScreenshotFile === filePath) { lastScreenshotNumber++ } else { lastScreenshotNumber = 0 } lastScreenshotFile = filePath const newPath = filePath.replace( '.png', ` #${lastScreenshotNumber}.png` ) return new Promise((resolve, reject) => { fs.mkdir(path.dirname(newPath), { recursive: true }, mkdirErr => { if (mkdirErr) { return reject(mkdirErr) } fs.rename(details.path, newPath, renameErr => { if (renameErr) { return reject(renameErr) } resolve({ path: newPath }) }) }) }) } const screenshotPath = `visualComparison/${config.env.baseline ? 'baseline' : 'comparison'}` return createDirAndRename(details.path .replace('cypress/integration', screenshotPath) .replace('All Specs', screenshotPath) ) } }) return config }

그런 다음 프로젝트의 package.json 에서 Cypress 호출에 해당 환경 변수를 추가하여 각 실행을 호출했습니다.

 "scripts": { "cypress:baseline": "BASELINE=true yarn cypress:open", "cypress:comparison": "COMPARISON=true yarn cypress:open" }

새 명령을 실행하면 실행 중에 찍은 모든 스크린샷이 적절한 폴더로 이동되었음을 알 수 있습니다.

실행 중에 촬영하여 폴더로 이동한 이미지를 보여주는 스크린샷.
그림 3. 시각적 실행 결과

다음으로, DOM 요소를 반환하는 Cypress의 기본 명령인 cy.get() 을 덮어쓰고 기본 구현과 함께 호출된 모든 요소의 스크린샷을 찍으려고 했습니다. 불행히도 cy.get() 은 자체 정의에서 원래 명령을 호출하면 무한 루프로 이어지기 때문에 변경하기 까다로운 명령입니다. 이 제한 사항을 해결하기 위해 제안된 접근 방식은 별도의 사용자 지정 명령을 만든 다음 요소를 찾은 후 새 명령이 스크린샷을 찍도록 하는 것입니다.

 Cypress.Commands.add("getAndScreenshot", (selector, options) => { // Note: You might need to tweak the command when getting multiple elements. return cy.get(selector).screenshot() }); it("get overwrite", () => { cy.visit("https://example.cypress.io/commands/actions"); cy.getAndScreenshot(".action-email") })

그러나 페이지의 요소와 상호 작용하기 위한 호출은 이미 내부 getElement() 함수로 래핑되었습니다. 그래서 우리가 해야 할 일은 래퍼가 호출될 때 스크린샷이 찍혔는지 확인하는 것뿐이었습니다.

시각적 회귀 테스트를 통해 얻은 결과

스크린샷이 생성되면 병합하는 일만 남았습니다. 이를 위해 Canvas를 사용하여 간단한 노드 스크립트를 만들었습니다. 결국 스크립트를 통해 618개의 비교 이미지를 생성할 수 있었습니다! Talent Portal을 열어서 차이점 중 일부를 쉽게 발견할 수 있었지만 일부 문제는 명확하지 않았습니다.

요소에 빨간색과 검은색을 표시하는 Picasso의 잘못된 사용 전후 예.
그림 4. 새로운 Picasso 지침을 따르지 않은 예 차이 예상되었지만 새 버전에는 빨간색 배경과 흰색 텍스트가 있어야 했습니다.

"After" 이미지의 확인란 옆에 정렬되지 않은 텍스트를 표시하는 약간 깨진 구성 요소 레이아웃의 전후 예.
그림 5. 약간 깨진 구성 요소 레이아웃의 예

UI 테스팅에 가치를 더하다

우선, 추가된 시각적 회귀 테스트가 유용하다는 것이 입증되었으며 테스트 없이는 놓칠 수 있었던 몇 가지 문제를 발견했습니다. 구성 요소의 차이점을 예상했지만 실제로 변경된 사항을 아는 것은 문제가 있는 사례를 줄이는 데 도움이 되었습니다. 따라서 프로젝트에 인터페이스가 있지만 아직 이러한 테스트를 수행하지 않는 경우 시작하십시오!

여기서 두 번째 교훈, 그리고 아마도 더 중요한 교훈은 완전한 것은 선의 적이라는 것을 다시 한 번 상기시켜 주었다는 것입니다. 사전 설정이 없었기 때문에 이 릴리스에 대한 시각적 회귀 테스트를 실행할 가능성을 배제했다면 마이그레이션 중에 몇 가지 버그를 놓쳤을 수 있습니다. 그 대신 우리는 이상적이지는 않지만 신속하게 실행할 수 있는 계획에 동의했고 이를 위해 노력했고 성과를 거두었습니다.

프로젝트에서 강력한 시각적 회귀 파이프라인을 구현하는 방법에 대한 자세한 내용은 Cypress의 시각적 테스트 페이지를 참조하고 요구 사항에 가장 적합한 도구를 선택하고 자습서 비디오를 시청하십시오.