목록 보기
시각적 회귀 테스트 BackstopJS 적용하기 (Visual Regression Test)
기타

시각적 회귀 테스트 BackstopJS 적용하기 (Visual Regression Test)

시각적 회귀 테스트란 코드 변경에 따른 UI의 변화를 시각적으로 확인하기 위해 진행하는 테스트입니다. 최근에 시각적 회귀 테스트의 필요성을 느껴 테스트 구축을 위해 많은 유료 서비스들(Percy, Chromatic, Applitools..)을 검토해봤지만, 최종적으로는 무료 오픈소스이고 이미지를 겹쳐서 동시에 비교할 수 있다는 기능을 고려해서 BackstopJS를 선택하게 되었습니다. 이 글에서는 BackstopJS를 통해 시각적 회귀 테스트를 구축한 제 경험을 공유해보고자 합니다. BackstopJS BackstopJS란 시각적 회귀 테스트 라이브러리 중 하나로, 렌더링 된 결과가 기존과 동일한지 비교해 달라진 영역을 쉽게 확인할 수 있는 기능을 제공합니다. 렌더링 결과 화면을 저장해놓고 테스트를 실행할 때마다 현재 렌더링 결과와 저장된 이미지를 비교해서, 기존과 UI가 동일하게 출력되는지를 보장할 수 있도록 합니다. 크롬 headless 브라우저를 통해 렌더링 결과 화면을 캡처하고, puppeteer를 활용해 다양한 상황을 스크립트로 설정해 줄 수 있습니다. BackstopJS을 통해 테스트를 수행하면 이전 렌더링 결과가 저장된 스크린샷을 기준으로 달라진 부분이 색칠되어 나타나며, 하단 오른쪽 이미지와 같이 동시에 변경 전/후 비교가 가능합니다. BackstopJS 테스트 결과 예시 BackstopJS를 프로젝트에 도입하게 된 계기는 공통 컴포넌트를 수정할 때 발생하는 예상치 못한 사이드이펙트들을 체크하기 위함이었습니다. 예를 들어 Next/Image를 도입해서 프로젝트에 일괄 적용한다면, 단순히 img 태그를 사용했을 때와 비교했을 때 이미지 스타일이 전반적으로 영향을 받을 수 있습니다. 이 때 padding과 같은 스타일이 기존에 의도했던 것과는 다르게 적용되어 예상치 못하게 이미지 사이즈 자체가 달라질 수 있는 등의 위험이 발생할 가능성이 있습니다. BackstopJS를 사용하게 되면, 수정 후 달라지는 UI가 있다면 테스트 failed 처리가 되면서 달라진 부분에 대한 비교가 가능합니다. 설치 및 실행 1. $ npm install backstopjs backstopjs 패키지를 설치합니다. 2. $ backstop init /backstop_data

  • /engine_scripts backstop.json backstop init 명령어를 실행하면 node_modules/backstopjs/capture/engine_scripts의 engine_scripts 폴더를 그대로 복사해 프로젝트 루트 경로에 backstop_data/engine_scripts 폴더가 생성됩니다. engine_scripts에서는 테스트 실행할 때 필요한 스크립트를 상황에 따라 추가할 수 있게 되어있습니다. init 명령어 실행이 완료되면 프로젝트 루트 경로에 backstop.json 파일이 생성되며, 이 파일에 backstopJS 설정 시나리오를 작성할 수 있습니다. 3. backstop.config.js JS 파일을 생성합니다. (옵션) module.exports = ( Same object as backstop.json ) backstop.json 대신 JS 파일을 생성하여 사용할 수 있습니다. 설정값들은 JSON 파일과 동일합니다. 설정값에 대한 자세한 내용은 Backstop 테스트 설정에서 이어서 설명하겠습니다. backstop init 명령어를 통해 생성된 JSON 파일을 계속 사용해도 되지만, 복잡한 설정들을 각각의 모듈로 나눠서 관리하기 위해서는 JS 파일로 관리하는 것이 더 바람직합니다. 따라서 다음과 같이 backstop.config.js 파일을 새로 생성한 후, backstop.json 파일 내부에 있는 설정값들을 그대로 복붙해 node module로 export 해주는 것을 추천합니다. module.exports = ( backstop.json에서 복붙한 설정값들 ) 다만 이렇게 커스텀 js 설정 파일을 사용하고자 한다면 이후 수행할 모든 backstop 명령에 뒤에 —config 파일경로를 붙여줘야 합니다. $ backstop test --config=backstop.config.js 4. $ backstop reference /backstop_data ...
  • /bitmaps_reference ... 새로운 BackstopJS를 설정합니다. 첫 번째 테스트를 실행하기 전, 비교 할 reference 이미지를 세팅해줍니다. 테스트 시나리오를 돌려서 나온 이미지들을 전부 bitmaps_reference에 저장하게 됩니다. 이후 테스트를 진행할 때 여기에 저장된 이미지와 비교합니다. 이 명령어는 초기 세팅 시에만 사용되기 때문에 다음 테스트부터는 approve 명령어를 사용해주면 됩니다. 5. $ backstop test /backstop_data ...
  • /bitmaps_test
  • /html_report ... config 파일에서 설정했던 세팅 값들과 시나리오대로 테스트를 실행합니다. 테스트 결과는 /bitmaps_test 에 날짜폴더로 리포트가 생성됩니다. config 설정 값 중 "report": ["browser"]로 설정하게되면 /html_report가 생성되어 테스트 결과를 브라우저에서 확인할 수 있습니다. 6. $ backstop openReport 테스트는 실행하지 않고 결과만 다시 확인하고 싶다면, 마지막 테스트를 기준으로 다시 브라우저가 뜨게 됩니다. 7. $ backstop approve 추가 테스트 진행 시 변경된 사항을 적용하고 싶다면 backstop approve를 합니다. backstop test 명령어 실행 시 생성되었던 /bitmaps_test 폴더에서 최근 테스트를 실행한 결과 이미지들을 전부 복사하여 /bitmaps_reference로 테스트 이미지들이 교체됩니다. 8. gitignore 설정 # BackstopJS backstop_data/html_report/ bitmaps_test/ 계속해서 테스트 결과값이 쌓이는 구조라서 (편의에 따라) .gitignore에 추가합니다. Backstop 테스트 설정 설정 파일은 위에서 설명했듯이 backstop.json 또는 backstop.config.js 에서 설정합니다. puppeteer를 활용해 다양한 상황을 스크립트로 설정해 줄 수 있는데, /engine_scripts/puppet 폴더의 cookies, onRefore, onReady 등 파일들에서 스크립트 수정이 가능합니다. 모든 설정값을 살펴보고 싶으시다면 backstopJS 공식 문서를 참고해주시고, 몇가지 주요 설정값만 살펴보겠습니다. - viewports 설정 viewports 설정을 사용하면 모든 테스트를 설정된 뷰포트에 대해 개별적으로 진행하게 됩니다. viewports: [ ( label: 'phone', width: 390, height: 844, ), ( label: 'pc', width: 2400, height: 1300, ), ] 이러한 설정으로 테스트를 진행하게 되면 phone 사이즈와 pc 사이즈 각각 테스트합니다. 따라서 하나의 시나리오 당 결과 이미지는 총 2개가 생성됩니다. - scenarios 설정 url: .storybookOutput/iframe.html?id=$(key[1])&ampviewMode=story, 원하는 페이지별로 url을 추가해서 테스트합니다. 저희 팀은 테스트 목적이 리얼 서비스 페이지가 아니라 공통 컴포넌트의 사이드 이펙트를 막기 위한 목적이었기 때문에, 스토리북 빌드 경로를 추가했습니다. 스토리북 링크로 테스트를 진행할 경우, iframe 모드로 스토리북 컨트롤러 및 메뉴가 테스트에 잡히지 않게 해야 합니다. // backstop.config.js const createBackstopScenarios = require('./createBackstopScenarios') module.exports = ( scenarios: createBackstopScenarios, ) 여러 가지 페이지 또는 컴포넌트들을 실행하기 위해서는 중복으로 시나리오 세팅 값들을 설정해주어야 합니다. 중복된 세팅 값 설정을 피하고자 config 파일을 JS 파일로 설정하는 것이 유리합니다. 동일한 시나리오 세팅 값으로 테스트하고 싶다면, createBackstopScenarios.js 파일을 생성하여 테스트할 시나리오 배열을 추가한 뒤 backstop.config.js에서 불러오도록 할 수 있습니다. 저희 프로젝트는 현재 아토믹 디자인을 사용하고 있습니다. 스토리북의 모든 케이스를 테스트하기에는 양이 많기 때문에 공통 컴포넌트들을 전부 포함하는 organism 단위로 테스트할 스토리들을 시나리오에 추가하게 되었습니다. // createBackstopScenarios.js module.exports = [ ['Test', 'test-story'], // 테스트할 스토리들 추가... ].map(key => (( label: key[0], url: .storybookOutput/iframe.html?id=$(key[1])&ampviewMode=story, referenceUrl: '', delay: 500, hideSelectors: ['#backstopHideSelector'], removeSelectors: [], hoverSelector: '', clickSelector: '', misMatchThreshold: 0, ... ))) 시나리오에도 여러 가지 설정을 할 수 있습니다. label : 시나리오 이름 url : 시나리오 테스트를 진행할 곳 referenceUrl : reference(비교된 이미지)를 볼 수 있는 링크 delay : 타이밍 지연하는 기능 hideSelectors : 해당 영역을 숨기고 해당 영역 테스트를 무시하는 기능. 원하는 요소에 추가할 selector 이름 removeSelectors : 해당 영역을 숨기고 해당 영역 테스트를 무시하는 기능. 원하는 요소에 추가할 selector 이름 hoverSelector : hover 기능. 원하는 요소에 추가할 selector 이름 clickSelector : click 기능. 원하는 요소에 추가할 selector 이름 misMatchThreshold : 백분율로, 테스트 시 성공 허용하는 차이 (기본값 0.1) hideSelectors, removeSelectors는 이미지나 데이터같이 동적으로 변하는 UI를 테스트에서 제외하기 위해 사용합니다. misMatchThreshold는 조금이라도 틀려도 테스트가 실패하게 설정하고 싶다면 0으로 설정하면 됩니다. 테스트 결과에서 filename에 마우스 호버하면 url, referenceUrl을 클릭할 수 있고 label, diff 값들을 확인할 수 있습니다. - backstop 실행 스크립트 추가 ( "backstop:init": "backstop init --config=backstopConfig.js", "backstop:test": "backstop test --config=backstopConfig.js", "backstop:approve": "backstop approve --config=backstopConfig.js", "backstop:openReport": "backstop openReport --config=backstopConfig.js" ) 로컬에서 backstop을 실행할 때 npm run backstop:test를 실행하면 됩니다. ( "docker:backstop:approve": "docker-compose -f backstop-approve.yml up --build", ) docker-compose를 이용하여 도커 환경에서 테스트가 실행되도록 추가합니다. 도커 스크립트에 관한 내용은 아래에 이어서 설명하겠습니다. - docker 설정 로컬에서 backstop 테스트를 여러 번 진행하다보니 분명히 수정한 내용이 없는데도 불구하고 테스트 failed가 발생하는 경우가 생겼습니다. 글자같은 경우에는 환경에 따라 폰트가 변경될 수 있기 때문에 수정내역이 없어도 다음과 같이 테스트가 실패했습니다. 수정내역이 없어도 글자만 테스트가 깨진 모습 크롬 headless 브라우저를 통해 스크린샷을 캡처하기 때문에, 로컬로 테스트를 돌린다면 각 환경에 따라 브라우저에 띄운 화면이 달라질 수 있습니다. 따라서 같은 환경에서 테스트할 수 있도록 도커를 사용하게 되었습니다. # Dockerfile.backstop-approve

프로젝트에 맞는 베이스를 설정해 주세요.

Alpine을 베이스로 하셨다면 아래 코드를 추가해주세요.

RUN apk add --no-cache
chromium
nss
freetype
freetype-dev
harfbuzz
ca-certificates

ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser 도커 컨테이너에서 Headless 브라우저를 실행할 때 Alpine 리눅스 환경에서는 이슈가 발생해서 Chromium과 몇몇 라이브러리를 직접 설치해야 합니다. # Dockerfile.backstop-approve

backstop test

CMD sh -c "
build-storybook -s ./public -c .storybook -o .storybookOutput
rm -rf ./packages/backstop_data/bitmaps_test
npm run backstop:test
npm run backstop:approve
" 스토리북 테스트를 하기 위해서 도커에서 스토리북을 빌드 후 backstop test 및 approve를 진행합니다. 테스트를 진행할 때마다 /bitmaps_test가 추가되는데 .gitignore에 추가했어도 로컬파일이 계속해서 쌓이지 않도록 하기 위해서 테스트 전에 폴더를 삭제해 주도록 합니다. docker-compose를 사용하여 도커에서 backstop approve 한 결과 이미지를 프로젝트에 복사합니다. backstop-approve.yml services: backstop_approve: build: context: . dockerfile: Dockerfile.backstop-approve image: backstop_approve container_name: backstop_approve volumes: - ./:/(프로젝트 경로)/ BackstopJS 테스트 자동화 기존에 프로젝트 내부에서 사용하고 있던 프리뷰 서버 생성해주는 도커파일 때문에 생략된 내용이 많습니다. 프리뷰 서버란, github 배포 자동화를 통해 pr을 올릴 때 해당 git branch를 base로 띄워진 테스트용 서버를 말합니다. 기본적인 테스트 자동화에 대한 설명은 생략하고 backstop에 관한 내용만 살펴보겠습니다. 테스트 자동화 시나리오는 다음과 같습니다. git push ➞ backstop test ➞ 테스트 결과 preview 생성 git push를 할 때 스토리북 프리뷰 서버를 생성해주는 도커파일에 backstop 테스트 관련 코드를 추가해주었습니다. backstop test를 돌린 후 테스트 결과를 빌드된 storybookOutput 폴더에 복사합니다. 아래와 같이 추가하게 되면, 스토리북 프리뷰 서버로 https://(스토리북_preview_url)/backstop/html_report/ 와 같이 들어갈 수 있습니다. # Dockerfile.storybook

RUN npm run backstop:test exit 0 RUN cp -rf packages/backstop_data/ packages/.storybookOutput/backstop/ 생성된 스토리북 preview에서 실패한 backstopJS 테스트는 failed 필터링을 클릭하면 확인할 수 있고, 검색을 통한 필터링도 가능합니다. preview 링크에서 확인한 결과 왼쪽(수정 전)과 오른쪽(수정 후) 결과를 동시에 비교하여 아이콘 크기가 달라진 것을 빨간색 기준선을 움직이면서 확인할 수 있다. 테스트 결과 달라진 부분을 핑크색으로 색칠하여 표시해줍니다. (색깔은 설정에서 변경이 가능합니다.) backstop 테스트 결과 timeout 나는 에러 TimeoutError: waiting for selector ".~~" failed: timeout 30000ms exceeded https://github.com/garris/BackstopJS/issues/946#issuecomment-456953669 puppeteer 테스트에서 타임아웃 생기는 문제는 도커 리소스 수정과 engineOptions 수정으로 해결했습니다. module.exports = ( // ... engine: 'puppeteer', engineOptions: ( slowMo: 500, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--force-device-scale-factor=1', '--disable-infobars=true', '--hide-scrollbars', '--disable-dev-shm-usage', '--shm-size 512mb', '--cap-add=SYS_ADMIN', ], ), asyncCaptureLimit: 30, asyncCompareLimit: 50, ) asyncCaptureLimit 와 asyncCompareLimit 는 테스트를 실행할 때 동시에 캡처하는 이미지 개수와 비교하는 개수입니다. 테스트 속도를 높이기 위해 높게 수정했는데, 상황에 맞춰서 조절하면 좋을 것 같습니다. 마무리 BackstopJS의 가장 큰 장점은 이미지를 비교 할 때 동시에 비교 할 수 있다는 점인 것 같습니다. 또한 url을 입력해서 헤드리스 브라우저로 테스트하기 때문에 다른 라이브러리의 영향을 받지 않습니다. 스토리북 테스트 뿐만 아니라, 실제 프로덕트 url을 넣을 수도 있고 원하는 링크들을 시나리오에 추가할 수 있어서 확장성 측면에서 좋은 것 같습니다. 저희 팀에서는 스토리북 컴포넌트 관련 수정 pr을 올릴 때 backstop 테스트가 깨지는 부분을 확인하기 위해 backstop test preview 링크를 첨부합니다. backstop 테스트 결과로 확인하면 코드상으로 스타일 변경을 확인하는 것보다 한 눈에 변경사항을 확인할 수 있습니다. 이후 pr이 머지되는 시점에 backstop approve로 적용합니다. 팀마다 원하는 방식에 따라 시나리오를 작성한다면 활용도 높게 사용할 수 있을 것 같습니다. 혹시 시각적 회귀테스트 적용을 고민하던 분들이 계셨다면 도움이 되었길 바랍니다.

댓글 0

댓글을 작성하려면 로그인이 필요합니다.

댓글을 불러오는 중...