시각적 회귀 테스트

시각적 회귀 테스트

  • 스타일 변화는 다양한 요소에 영향을 받으므로 코드만으로 예측하기는 어렵다.

  • 이상적인 방법으로 브라우저별로 모든 페이지를 육안으로 확인하는 것은 어렵기에 불가능

  • 이전에 작성한 CSS 코드를 수정하거나 삭제하다보면 의도치 않은 곳에서 변경이 되기도 함

    • 이같은 부작용을 대처하고자 '문제가 생기면 이전에 작성한 코드는 건드리지 않고 새로 작성한 코드에서 해결한다" 라는 소극적인 방침을 세우는 팀도 존재

    • 이런 소극적인 방침은 리팩터링을 불가능하게 만드는 대증치료에 불과함

  • SPA는 작은 UI 컴포넌트를 조합하여 화면을 만듬

    • 마치 레고 블럭을 조립하는 방식과 비슷하며 컴포넌트 기반 개발이라 불린다.

    • 로직과 중복되는 CSS 코드를 한곳에서 관리 할 수 있게 된다.

    • 많은 화면이 수많은 공통 컴포넌트를 공유하기 때문에 한 컴포넌트의 수정 여파가 크다는 단점도 있음

시각적 회귀 테스트를 스냅샷 테스트로 대체할 수 있나?

  • 스냅샷 테스트는 시각적 회귀 테스트의 한 방법이지만 전역에 정의한 스타일의 영향을 알긴 어렵다.

  • 마치 단위 테스트에서 알아채지못한 문제를 통합 테스트에서 발견하는 상황과 비슷

  • 스냅샷은 단순히 HTML 출력 결과를 비교할 뿐이라 모든 시각적 회귀 테스트를 대체할 수 없다.

시각적 회귀 테스트 선택지

  • 가장 신뢰도가 높은 방법은 브라우저에 렌더링된 화면을 비교하는 것

  • 테스트할 화면을 브라우저에 렌더링하고 특정 시점에 캡처한 이미지들을 픽셀단위로 비교하는 것이 가장 기본 방법

  • 시각적 회귀 테스트는 크로미엄같은 브라우저를 헤드리스 모드로 실행한 상태에서 실시함

    • 헤드리스 브라우저는 보통 E2E 테스트 프레임워크에 포함되어있음

    • E2E 프레임워크에선 공식적으로 시각적 회귀 테스트를 지원한다.

    • 페이지 단위로 캡처하여 스타일 변경 전후의 차이점을 발견

    • 단점으로 세세한 부분 (A가 변경되어 다른 영역에 영향을 끼친 경우 전부 변경됬다고 보게됨) 어디가 정확히 변경됬는지 알지 못함

  • 세세하게 검출하고 싶다면 UI 컴포넌트 단위로 실시해야함

    • 컴포넌트 단위로 이미지를 캡처하면 영향을 받는 중간 크기의 컴포넌트를 검출할 수 있다.

    • 작은 크기의 컴포넌트와 중간 크기의 컴포넌트를 개별적인 스토리로 등록하면 컴포넌트 탐색기에서 확인할 수 있을 뿐만 아니라 시각적 회귀 테스트에서도 활용 가능

reg-cli로 이미지 비교

  • 시각적 회귀 테스트용 플랫폼으로 스토리북을 사용할 수 있지만 별도 시각적 회귀 테스트 프레임워크를 사용해보자

  • reg-suit 는 AWS S3 같은 실제 버킷이 없어도 로컬에서 테스트 가능하다.

    • reg-suit 의 핵심 기능인 reg-cli 를 사용해서 이미지를 비교해보자.

스토리북 - 스토리캡 도입

  • 스토리북을 활용한 시각적 회귀 테스트 방법

  • 스토리캡은 스토리북에 등록된 스토리를 캡처하는 도구

  • 스토리캡도 reg-suit를 중심으로 한 reg-viz 생태계에 속하지만, reg-suit 플러그인과 달리 별도로 설치해야한다.

  • 스토리 단위로 캡처하기 떄문에 스토리북을 잘 활용할수록 테스트 효율이 높아진다.

$ npm i storycap --save-dev

설정

  • 스토리캡 설정을 스토리북 설정 파일에 추가

  • 해당 설정들을 추가하면 프로젝트에 있는 모든 스토리 파일이 캡처 대상이되며 시각적 회귀 테스트를 할 수 있게됨

// .storybook/preview.js
import { withScreenshot } from 'storycap';
export const decorators = [withScreenshot]

// .storybook/main,js
module.exports = {
  addons: [
    //...
    "storycap"
  ]
}

실행

  • 스토리를 캡처하기 전 스토리북을 빌드

    • 빌드를 해야 응답 속도가 빨라지므로 사전에 빌드하는 것을 권장

// package.json
{
  //...
  "scripts": {
    "storybook:build": "storybook build",
    "storycap": "storycap --serverCmd \"npx http-server storybook-static -a localhost -p
6006\" http://localhost:6006",
  }
}
  • 빌드가 끝나고 npm run storycap 을 실행하면 빌드된 스토리북을 정적 사이트로 실행해 모든 스토리를 캡처한다.

  • 캡처가 완료된 이미지는 __screenshots__ 디렉터리에 저장됨

    • 디렉터리 컨벤션에 맞추려면 mv __screenshots__ expected

    • actual: 기존 이미지를 저장하는 디렉터리

    • expected: 비교할 이미지를 저장하는 디렉터리

    • diff: 비교 결과를 검출할 이미지를 저장하는 디렉터리

  • 스타일을 수정 후 스토리북을 다시 빌드 & 스토리캡을 실행하면 __screenshots__ 에 변경된 스타일이 반영된 스토리 이미지가 저장됨

    • 해당 디렉터리를 actual 로 변경

reg-cli로 이미지 간 차이 검출하기

  • expectedactual 디렉터리가 만들어졌으므로 reg-cli 로 이미지 간 차이점을 검출해보자.

  • HTML 리포트를 열어 차이점을 확인

$ npx reg-cli actual expected diff -R index.html

reg-suit 도입하기

  • reg-cli 를 사용하여 로컬 환경에서만 시각적 회귀 테스트를 진행했다.

  • 시각적 회귀 테스트를 자동화해서 깃허브와 연동해보자.

  • 깃허브와 연동하면 저장소에 push 할 때 마다 토픽 브랜치의 시각적 회귀 테스트가 실시되기 때문에 변경된 코드에서 어떤 차이점이 발생하는지 자동으로 리포트 받을 수 있다.

reg-suit 도입

  • 프로젝트 저장소 최상위 경로로 이동 후 npx reg-suit init 실행

    • 개발 환경에 reg-suit 을 글로벌 설치했다면 reg-suit init 만 실행해도 됨

  • 커맨드 실행 후 어떤 플러그인을 설치할지 물어보는데 기본값 세팅 그대로 엔터키 ㄱ

    • 해당 플러그인들은 임의의 CI 환경에 reg-suit 를 도입시켜주는 편리한 라이브러리임

    • reg-keygen-git-hash-plugin, reg-publish-s3-plugin은 원격 환경에서 이미지를 비교하기 위한 라이브러리임

  • 커밋 해시로 파일명을 지은 스냅샷셋과 검증 결과 리포트를 외부 파일 저장소 서비스(AWS S3)에 전송한다.

  • 토픽 브랜치의 기반이 되는 커밋에서 스냅샷셋을 추출하여 기댓값으로 두고, 이후 커밋에서 추출한 이미지와 차이점을 검출한다.

  • 검증 결과를 PR에 알려주는 reg-notify-github-plugin 을 사용하면 통상적인 워크플로에 시각적 회귀 테스트를 도입할 수 있다.

    • 슬랙 등의 채팅 도구에 알림을 보내는 플러그인도 존재

reg-suit 설정파일 생성

# 기본적인 .reg인 상태로 엔터키를 누름
? Working directory of freg-suit. .reg
# 스토리캡 결과물을 저장할 디렉토리로 __screenshots__ 를 지정한다
? Directory contains actual images. __screenshots__
# 이미지 비교의 강도를 0에서 1 사이로 설정, 일단 기본값인 0인 상태로 엔터
? Threshold, ranges from 0 to 1, Smaller value makes the comparison more senstive. 0
# reg-suit Github App을 저장소에 추가하기 위해 Yes 선택
? notify-github plugin requires a client ID of reg-suit Github app. Open installation window in your browser Yes
# 이서서 수동으로 설치할 예정이므로 입력하지 않고 엔터키 누름
? This repository's Client ID of reg-suit GitHub app
# 버킷은 이후에 작성하므로 No
? Create a new S3 bucket No
# 마찬가지로 이후에 작성하므로 입력하지 않고 엔터
? Existing bucket name
# 설정 파일을 업데이트 하기 위해 YES
? Update configuration file yes
# 예시용 이미지는 불필요하므로 No
? Copy sample images to working dir No
  • reg-notify-github-plugin 질문에 Yes를 선택하면 브라우저 창 열림

    • reg-suit 의 깃허브 애플리케이션 설치와 저장소 연동을 위한 것

    • 이렇게 설치하는 것만으로 PR에 검증 결과를 알려주게 됨

    • [Configure] 버튼을 클릭하면 저장소 연동화면으로 이동되며, 테스트를 실시할 저장소를 선택,

    • 연동이 완료되면 선택된 저장소가 화면에 표시됨

    • 연동한 저장소에 있는 [Get Client ID] 버튼을 클릭하면 모달이 열림

      • 해당 Client ID는 환경 변수로 설정할 것이므로 [Copy to clipboard] 버튼ㅇ르 클릭해서 복사

Client ID 취득

  • 설치가 완료되면 reg-suit 설정 파일인 regconfig.json 이 생성됨

  • 파일을 열어 clientId 에는 "$REG_NOTIFY_CLIENT_ID"bucketName 에는 "$AWS_BUCKET_NAME" 를 입력

  • 이값들은 깃허브 액션 실행 시에 환경 변수에서 참조된다.

// regconfig.json
{
  "core": {
    "workingDir": ".reg",
    "actualDir": "__screenshots__",
    "thresholdRate": 0,
    "ximgdiff": {
      "invocationType": "client"
    }
  },
  "plugins": {
    "reg-keygen-git-hash-plugin": true,
    "reg-notify-github-plugin": {
      "prComment": true,
      "prCommentBehavior": "default",
      "clientId": "$REG_NOTIFY_CLIENT_ID", // 깃허브의 Screts에 별도로 값을 설정한다.
    },
    "reg-publish-s3-plugin": {
      "bucketName": "$AWS_BUCKET_NAME" // 깃허브의 Screts에 별도로 값을 설정한다.
    }
  }
}

실제 운영 환경을 고려한 오차 범위 설정

  • 실제 운영 환경에 자동화된 시각적 회귀 테스트는 불안정한 테스트(코드나 테스트에 변경 사항이 없는데도 상황에 따라 성공하기도 실패하기도 하는 테스트)로 변질되곤 한다.

  • 이는 브라우저에서 여러 요소가 트리 구조로 계층화될 때 안티에일리어싱(위신호 제거)이 발생하는 과정에 차이점이 검출되는것이 원인

  • 불안정한 테스트가 발견되면 차이점을 검출하는 강도를 낮추는것을 검토해야함

    • thresholdRate: 차이점이 검출된 곳의 픽셀 수를 전체 대비 비율로 계산

    • thresholdPixel: 차이점이 검출된 픽셀수의 절대값

    • 을 조정함녀서 안정적인 운용이 가능한 임계치를 찾아야함

// regconfig.json
{
  "core": {
    "workingDir": ".reg",
    "actualDir": "__screenshots__",
    "thresholdRate": 50, // 차이점으로 검출될 부분을 허용하는 임계치
    "ximgdiff": {
      "invocationType": "client"
    }
  }
}

외부 스토리지 서비스 설정

  • 스냅샷 에셋과 검증 결과 리포트를 저장할 외부 스토리지 서비스를 설정

  • reg-publish-s3-plugin 을 선택했으므로 AWS S3 버킷을 생성

버킷 생성

  • AWS Management Console 에 로그인해서 S3에 새 버킷을 생성

  • reg-suit 깃허브 애플리케이션이 생성된 버킷에 검증 결과 리포트를 전송, 열람할 수있도록 일부 권한을 설정

  • 우선 객체 소유권에서 ACL(접근 제어 목록)access control list을 활성화한다.

  • 버킷의 퍼블릭 액세스 차단 설정에서 일부 차단 설정을 해제

    • 새 ACL을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단 - 해제

    • 임의의 ACL을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단 - 해제

IAM 사용자 생성

  • 버킷에 접근할 사용자를 IAM에서 생성해보자.

  • 원하는 사용자명을 입력 후 [다음] 클릭

  • AmazonS3FullAccess 권한을 연결해서 사용자가 S3 버킷에 자유롭게 접근할 수 있도록 설정

  • 사용자 생서을 완료 후 액세스 키와 비밀 액세스 키를 취득

    • .csv 파일을 저장하거나 다른 곳에 복사

    • 액세스 키들은 절대 저장소에 커밋하지 않도록 주의

깃허브 액션에 reg-suit 연동하기

  • 깃허브 액션이 완료되면 PR을 생성할 때 마다 자동으로 시각적 회귀 테스트가 실시되며, 테스트가 끝나면 검증 결과를 PR에 알린다.

크리덴셜을 Actions Screts에 등록

  • 지금까지 메모한 크리덴셜을 저장소의 Actions Scretes에 설정한다.

    • [Settings] -> [Secrets and variables] -> [Actions] -> [New repository Secret] 버튼을 클릭

    • AWS_ACCESS_KEY_ID : IAM 사용자의 액세스 키

    • AWS_BUCKET_NAME : S3 버킷 이름

    • AWS_SECRET_ACCESS_KEY: IAM 사용자의 비밀 액세스 키

    • REG_NOTIFY_CLIENT_ID: reg-suit의 Client ID

  • AWS_BUCKET_NAME, REG_NOTIFY_CLIENT_ID는 최종적으로 regconfig.json에 적용된다.

    • 이 값들은 중요한 정보가 아니므로 json 에 직접 작성해도 되지만, 저장소별로 설정해야하는 값이므로 편의상 환경 변수에서 불러오도록 세팅

깃허브 액션 설정

  • 깃허브 액션 워크플로 작성

  • fetch-depth: 0 반드시 추가해야 한다.

    • 이 값이 없으면 부모 커밋을 취득하지 못해 테스트가 실패하게 됨

// .github/workflows/vrt.yaml

name: Run VRT

on: push

env:
  REG_NOTIFY_CLIENT_ID: ${{ secrets.REG_NOTIFY_CLIENT_ID }}
  AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }}
  
jobs: 
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0 # 이 값이 없다면 비교를 진행 불가
      - uses: actions/setup-nod@v3
        with:
          node-version: 18
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@master
        with: 
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2
      - name: Install dependencies
        run: npm ci
      - name: Build Storybook
        run: npm run storybook:build
      - name: Run StroyCap
        run: npm run vrt:snapshot
      - name: Run reg-suit
        run: npm run vrt:run     
// package.json
{
  "scripts": {
    "storybook:build": "storybook build",
    "vrt:snapshot": "storycap --serverCmd \"npx http-server storybook-static -a localhost 
-p 6006\" http://localhost:6006",
    "vrt:run": "reg-suit run",
  }
}

연동 확인

  • 깃허브 액션에서 시각적 회귀 테스트가 실행되는지 확인해보자.

  • PR 생성 후 깃헙 액션이 완료되면 reg-suit 봇이 코멘트를 남김

    • 뻘건색 동그라미: 차이점이 검출된 아이템

    • 흰색 동그라미: 새롭게 추가된 아이템

    • 검은색 동그라미: 삭제된 아이템

    • 파란색 동그라미: 차이점이 발견되지 않은 아이템

  • 봇이 남긴 코멘트에 있는 [this report] 링크를 클릭하면 S3에 저장한 검증 결과 리포트를 확인 가능

  • 차이점이 없어지거나 리뷰어가 PR을 승인하면 체크 상태가 녹색으로 변경됨

시각적 회귀 테스트를 활용한 적극적 리팩토링

  • 회귀 테스트는 릴리스 전에 실시한다고 생각하지만, 반복 작업을 줄이고 싶다면 개발 과정에 도입하는 것이 좋음

반응형 디자인에 활용

  • 반응형 프로젝트는 효율적으로 시각적 회귀 테스트를 활용할 수 있다.

  • 프로젝트 사정상 PC용 디자인을 먼저 완성하고 이후 모바일 용 디자인을 구현해도 시각적 회귀 테스트가 설정됐다면 실수를 줄일 수 있다.

  • reg-suit는 CI를 도입하지 않았다고 해도 유용하게 사용할 수 있다.

  • 수동으로 편리하게 실시할 수 있을 정도로만 환경을 세팅해놓고 미디어 쿼리로 반응형 대응을 하거나 리팩터링 가능

릴리스 직전의 리팩터링에 활용

  • 스타일 관련 전역 코드를 하나씩 삭제하면서 시각적 회귀 테스트를 통해 플젝에 영향을 미치는 영향을 확인 가능

  • 실제로 필요한 CSS 코드만 남기는 리팩터링은 시각적 회귀 테스트가 없다면 불가능

스토리 커밋 습관화로 시작하는 시각적 회귀 테스트

  • 스토리를 만들면 컴포넌트 단위로 시각적 회귀 테스트를 실시하는 것은 쉽다.

  • 등록된 스토리의 숫자만큼 세밀한 검증이 가능하기 때문에 습관적으로 스토리를 커밋할것을 권장

  • 릴리스 직전에 스토리를 커밋하면 반응형 디자인이 필요할 때 여유롭게 대응하는것이 힘들어 도입 자체가 좌절되기 쉽다.

  • 스토리가 정말로 필요하지 않다고 느껴지지 않는 한, 처음부터 커밋

Last updated