++Image Source: We're Building a Visual Regression Testing Library for React Native
+
블로그에 항상 테스트를 도입해야겠다 생각하고 있었는데 이번에 적용하게 되어 도입 배경과 트러블 슈팅 과정을 포스트로 남겨보고자 한다.
+Index
+-
+
- TL;DR! +
- 도입 배경 +
- 시각적 회귀 테스트란 +
- Playwright + + +
- Github Actions +
- 성능 개선 + + +
- 트러블 슈팅
+
-
+
- 로컬 테스트를 포기해야 할까 +
- Timezone +
- 테스트 분기 +
- 1px +
- Image load +
+ - 마무리 +
TL;DR!
+Playwright 및 Github Actions로 시각적 회귀 테스트 및 CI/CD를 적용한다.
+-
+
- 시각적 회귀 테스트로 UI 변경 사항을 배포전에 알아차린다 +
- 빠르게 실패하고 실패한 부분만 재실행하자 +
- 로컬 테스트와 CI Test의 통합은 어렵다 +
도입 배경
+ +++Image Source: 사용자 인터페이스 테스트 통합 테스트 및 단위 테스트로 테스트 피라미드
+
이전부터 블로그에 테스트 코드가 없는 것이 꽤나 찝찝했기 때문에 어떤 방식/도구로 테스트를 적용할까 고민하고 있었다.
+블로그의 특성상 한번 배포된 콘텐츠는 크게 바뀔 일이 없기 때문에 정적 UI 테스트를 도입하기에 적절하다고 생각했고 마침 꽤나 긴 연휴가 있었기 때문에 각 잡고 정적 UI 테스트를 도입하고자 마음먹게 되었다.
+빌드된 결과물을 바탕으로 테스트할 예정이었기 때문에 아래의 두 가지 방식의 테스트를 고려했다.
+-
+
- DOM Snapshot +
- Screen Snapshot +
먼저 DOM 스냅샷 비교를 통해 이후 작업에서 기존 DOM 구조를 변경하는지 확인한다. 하지만 DOM 스냅샷은 CSS의 변경 여부를 알아차리기 어렵다는 단점이 있다.
+따라서 시각적 회귀 테스트인 Screen 스냅샷 비교로 정상적인 렌더링이 되었는지 확인한다.
+시각적 회귀 테스트란
+ +++(좌) 차이가 생긴 렌더링 결과물. (우) 차이가 생긴 부분 히트맵
+
시각적 회귀 테스트(Visual Regression Test)는 코드 변경 전후의 렌더링 된 UI의 스크린샷을 비교하는 테스트이다.
+위의 좌측 이미지를 확인해 보면 더 명확하게 알 수 있다. 모종의 이유로 하위 이미지 크기가 달라졌고 이에 따라 이후의 시각적 구조가 변경 되었다.
+우측 이미지는 Diff 이미지로, 차이가 생긴 영역에 붉게 표시를 해놓았다.
+이로써 우리는 컴포넌트가 실제로 어떻게 렌더링 되었는지 정확하게 알 수 있게 된다.
+Playwright
+ +어떤 방식의 테스트를 할지 결정되었으니 자연스럽게 어떤 도구로 테스트를 작성할지 고민하게 되었다.
+Aspect | +Playwright | +Cypress | +
---|---|---|
Browser 지원 | +Chrome, Firefox, Webkit | +Chrome, Firefox, Electron | +
병렬 실행 | +무료 | +유료 | +
멀티탭(다중 브라우저) | +가능 | +불가능 | +
성능 | +Headless Event-driven socket 방식으로 빠름 | +실제 브라우저에서 실행하므로 상대적으로 느림 | +
++더 자세한 내용은 Cypress vs Playwright: A Detailed Comparison 참고
+
꾸준히 Cypress를 사용해 왔지만 병렬 처리에 상당히 답답함을 느끼고 있었기 때문에 이번 기회에 Playwright에 도전하고자 결정했다.
+물론 Sorry-Cypress로 병렬처리를 할 수 있지만 셀프 호스팅부터 신경 써야 하는 부분이 하나 더 생기기 때문에 기술부채가 싫은 나로서는 선택지에 해당되지 않았다.
+ +무엇보다 성능 부분에 차이가 있다. 테스트 결과를 하루종일 기다렸는데 심지어 실패했다? 한 줄 고치고 다시 하루종일 기다려야 한다.
+정말 하기 싫어진다.
+Playwright는 브라우저와 HTTP request 통신 대신 WebSocket으로 Dev tools에 바로 연결한다. 따라서 브라우저의 큰 메모리나 부가적인 리소스가 필요하지 않기 때문에 실제 브라우저와 통신하는 Cypress에 비해 가볍고 빠르다.
+그렇다면 Playwright로 어떻게 기존의 목적, DOM snapshot과 Screen snapshot을 할 수 있는지 살펴보자.
+DOM Snapshot
+ +나는 Next.js를 사용하고 있으므로 __next
하위의 DOM만 비교하고자 한다.
innerHTML
메서드를 통해 DOM 구조를 가져온 다음 playwright에서 제공하는 toMatchSnapshot 메서드로 DOM 스냅샷을 비교할 수 있다.
Screenshot
+ +스크린샷 또한 playwright에서 제공하는 toHaveScreenshot 메서드로 쉽게 적용할 수 있다.
+나는 전체 화면의 비교를 할 것이므로 fullPage
를 설정했다.
시각적 회귀 테스트는 다양한 이유로 실패할 수 있다
+-
+
- 테스트가 실행되는 OS에 따라 화면이 달라지기 때문에 실패한다(이모지 등) +
- 동일한 OS라도 버전/브라우저에 따라 화면이 달라질 수 있다 +
- 실행된 머신의 타임존에 따라 Date 값이 달라져 실패할 수 있다 +
- Image와 같은 Resource 로딩 시점에 따라 페이지가 달라질 수 있다 +
- Animation 혹은 setTimeout과 같은 시간에 종속된 동작은 일관성을 보장할 수 없다 +
- 눈에 큰 차이가 안 나더라도 실패할 수 있다(1px 차이로 실패 등) +
위의 내용들은 일반적인 E2E 테스트에서도 발생할 수 있는 실패 케이스들이다. 일부 케이스는 밑의 트러블 슈팅에서 다루겠다.
+이처럼 다양한 사이드 이펙트가 존재하기 때문에 동적인 컴포넌트가 많거나 화면이 자주 바뀐다면 도입 전에 ROI를 따져보는 것이 좋다.
+기본적인 설정은 되었으므로 CI/CD를 구축하자.
+Github Actions
+ +++Image Source: CI/CD with GitHub Actions: Step-by-Step Workflow
+
Github Actions를 통해 CI/CD를 간편하게 구축할 수 있다.
+ + +성능 개선
+위의 CI/CD는 3가지 문제가 있다.
+-
+
- 테스트 속도가 느리고 flow를 한눈에 확인하기 어렵다. +
- 캐싱이 전혀 되고 있지 않다. +
- 테스트가 실패하면 다시 처음부터 실행해야 한다. +
이것을 개선해보고자 한다.
+테스트 방식
+ +테스트의 어디까지 성공했는지, 어떤 테스트를 실행 중인지 등의 작업 상황을 보기 위해선 현재는 로그를 확인해야 한다.
+이러한 문제의 근본적인 이유는 특정 기능 단위의 테스트만 실행시키는 방법이 존재하지 않기 때문이다.
+ +따라서 playwright에서 제공하는 grep 명령어를 활용해 원하는 기능별로 테스트를 적용할 수 있다.
+DOM 스냅샷은 @dom-snapshot
키워드를, Screen 스냅샷은 @screen-snapshot
키워드를 가지고 있어야 한다. 그 이외의 테스트는 others
로 실행된다.
이제 우리는 dom, screen, others 세 가지 테스트 피처를 가지게 되었다. 이것은 후술할 CI/CD에서 큰 역할을 하게 된다.
+테스트 속도
+ +테스트 속도에는 많은 것들의 영향이 있겠지만 기본적으로 wait timeout이 가장 좋지 않다.
+특히 위와 같이 반복문으로 작업을 하게 될 경우 N의 배수로 시간이 증가하게 된다.
+이미지 로딩까지 3초의 텀을 두고자 한 위의 코드는 이미지가 빨리 로딩되었다면 불필요한 기다림이 발생하고 이미지가 3초보다 늦게 로딩되면 깨지는 불안정한 코드다.
+ +이처럼 유동적인 사이드 이펙트는 이벤트로 처리하면 보다 안정적으로 처리할 수 있다.
+CI/CD
+앞서 언급한 세 가지 문제
+-
+
- 테스트 속도가 느리고 flow를 한눈에 확인하기 어렵다. +
- 캐싱이 전혀 되고 있지 않다. +
- 테스트가 실패하면 다시 처음부터 실행해야 한다. +
이것은 workflow와 actions를 적절하게 나눠주고 actions/cache를 활용하면 된다.
+ + +앞에서 나눈 테스트 피처 단위로 workflows의 job을 나눠주고 workflow_call을 적절하게 사용한다면 편리하고 가독성 좋은 Flow를 만들 수 있다.
+ +무엇보다 job을 나누게 되면 실패한 부분만 재실행할 수 있기 때문에 더욱 유연한 테스트를 할 수 있게 된다.
+ +Job을 분리하면 불필요한 반복 빌드 작업이 발생하게 되는데 이를 캐싱을 통해 시간을 단축시킬 수 있다.
+특히 잘 변경되지 않는 정적 블로그의 경우 pnpm
, .next
, out
, playwright
를 캐싱해 두면 전체 테스트 시간을 아낄 수 있게 된다.
이로써 절반이상 시간을 줄이고 실패에 더 유연한 CI 테스트를 할 수 있게 되었다.
+완성된 전체 코드는 깃헙에서 확인할 수 있다. .github
및 e2e
를 확인하면 된다.
트러블 슈팅
+로컬 테스트를 포기해야 할까
+팀 단위의 협업에선 로컬 머신 버전을 강제하기 어렵기 때문에 로컬 테스트와 CI 테스트의 동기화가 어렵다.
+따라서 도커를 활용하든가 CI 테스트만 사용하든가 양자택일로 흐르게 된다.
+하지만 지금 나의 플로우와 같이 1인 개발이라면 로컬과 CI 테스트를 어느 정도 맞춰줄 수 있다.
+ +++ +Runner 전체 목록 확인
+
Github에서 제공해주는 Actions Runner에 MacOS가 존재하기 때문에 로컬과 버전을 맞춰줄 수 있다.
+완벽하다고 장담은 못하겠지만 현재까지는 로컬과 CI 테스트가 모두 동일하게 동작하며 통과하고 있다.
+Timezone
+CI 테스트에서 가장 많이 실패하는 부분은 Timezone이다. 우리는 +9의 값을 가지고 있기 때문에 스냅샷 테스트에서 반드시 실패한다.
+ +깃헙 액션에서는 env
로 타임존 값을 넘길수 있다. 이를 통해 편리하게 머신의 타임존을 변경할 수 있다.
playwright에서 어떤 브라우저를 선택하느냐에 따라 타임존 기준점이 조금 달라진다.
+-
+
- 크롬의 경우 기본적으로 머신의 타임존을 따른다. +
- Webkit은 config에 설정한 타임존을 따른다. +
만약 playwright에서 webkit을 사용하고 있다면 아래와 같이 playwright.config.ts
를 변경해야 한다.
테스트 분기
+코드에 테스트로 인한 분기점이 생기는 것을 원하지 않지만 어쩔 수 없는 경우(혹은 편의로) 빌드를 나누어 코드에 적용할 수 있다.
+ +e2e를 위한 빌드 스크립트를 만든 다음 환경변수를 주입해 코드에서 적용할 수 있다.
+ +1px
+ +크롬에서는 스크린샷이 1px 다른 경우가 있다(#18827).
+이때는 clip으로 고정하거나 height를 강제하는 방법으로 처리할 수 있다.
+Image load
+로드와 관련된 트러블 슈팅은 테스트 속도에서 다루었다.
+마무리
+시각적 회귀 테스트를 통해 심신의 안정을 많이 찾을 수 있었다.
+이제 더욱 과감하게 리팩터링을 진행할 수 있게 되었다.
+특히 playwright를 사용하며 경험이 좋았기 때문에 앞으로도 꾸준히 사용해 보고자 한다.
+이 글을 쓰며 참고했던 혹은 유용했던 링크를 남기며 글을 마무리하려고 한다.
+