회사이야기

회사 이야기 - Visual Regression Test feat. Cypress

eddie0329 2022. 7. 23. 16:57
반응형

📌 목차

  • 서론
  • Visual Regression Test를 적용한 이유
  • Visual Regression Test란?
  • Visual Regression Test 핵심 용어 정리
  • 왜 Cypress를 선택하였는가?
  • 설치 및 환경 설정
  • script 명령어 소개
  • 테스트 소개
  • 서버 데이터 mocking
  • 결론

📌 서론

안녕하세요! 에디 입니다. 오늘은 회사에서 Visual Regression Test를 적용한 이유 그리고 과정을 한번 소개를 해보겠습니다.
그럼 출발! 🚀

📌 Visual Regression Test를 적용한 이유

일일이 변경을 확인하는 나...

기존 프로젝트에는 styled-component로 세팅이 되어있었는데 이걸 emotion으로 포팅을 하게 되었습니다. emotion의 더 다양한 기능을 사용하기 위해 변경 작업을 진행했지만 비슷한 듯 조금 씩 다른 문법 그리고 styled-component에서는 의도한 대로 화면이 그려졌지만 emotion으로 넘어가면서 레이아웃이 깨지는 문제가 발생을 했어요. 그래서 emotion으로 변경할 때 마다 일일이 확인해야하는 번거로움이 있었습니다.

📌 Visual Regression Test란?

Visual Regression Test란 가시적으로 보여지는 화면에서 기존과 다른 부분(레이아웃)을 체크하는 Testing 입니다.여기서 중요한 건 레이아웃을 확인 한다는 점 이예요. 단순하게 글자가 추가되었거나 하는 부분은 레이아웃으로 인식을 안해서 단순 통과가 됩니다. 전체적인 틀을 확인 하는 테스트가 포인트 입니다.

Visual Regression Test의 순서는 대략 이렇습니다.

1.시나리오 실행
2.브라우저에서 동작
3-1. Reference Screens가 없다면 Reference Screens를 저장
3-2. Reference Screens가 있다면 Test Screens와 비교
4-1. Reference Screens와 달라진 점이 없다면 테스트 통과
4-2. Reference Screens와 달라진 점이 있다면 Report 발행 및 테스트 실패

그림으로 나타내면 다음과 같아요.

Visual Regression Test 순서도

📌 Visual Regression Test 핵심 용어 정리

Visual Regression Test에서 가장 중요한 용어는 다음 3가지 이예요.

  • Scenarios
  • Reference Screens
  • Test Screens

1. Scenarios

Scenarios는 테스트 코드를 의미합니다. 어떤 페이지의 레이아웃을 테스트 할 것 인가를 정의합니다.

2. Reference Screens

Reference Screens는 기준이 되는 스크린 샷 입니다. 레이아웃을 고치기 전에 스크린 샷이라고 이해하시면 빠를 것 같아요.

3. Test Screens

Test Screens는 테스트를 할 스크린 샷 입니다. 변경이 이루어진 스크린 샷으로 Reference Screens와 비교를 하게 됩니다.

📌 왜 Cypress를 선택하였는가?

저는 회사에서 Visual Regression Test를 위해 Cypress를 선택하였습니다. 다른 툴들 말고 Cypress를 선택한 이유는 다음과 같아요.

  1. 추후 해당 프로젝트에서 e2e 테스트를 해야함으로 툴을 통합하기 위함
  2. 많은 사용자 수를 보유하고 있어 문서, 참고 문헌이 많이 쓰여있음
  3. 무료 ⭐️

📌 설치 및 환경 설정

설치

먼저 3가지 패키지를 다운 받습니다. (cypress, cypress-image-snapshot, @types/cypress-image-snapshot)

🚧 @types/cypress-image-snapshot은 typescript용 이기 때문에 javascript 사용자는 설치 할 필요가 없습니다.

yarn install cypress cypress-image-snapshot @types/cypress-image-snapshot -D

이렇게 실행을 하시면 cypress라는 폴더가 생기셨을 거예요.
그럼 이제 config를 한번 건드려 볼까요?

환경 설정

cypress.config.ts파일을 하나 만들어 줍니다.

// cypress.config.ts
import { defineConfig } from 'cypress'
import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin'

export default defineConfig({
  e2e: {
    "baseUrl": "여기에 베이스 주소를 넣어주세요",
    env: {
      // 🌈 이건 아래서 설명 ! 
      "ALLOW_SNAPSHOT": process.env.ALLOW_SNAPSHOT === undefined ? false : true 
    },
    // ⭐️ 이걸 해주셔야 matchSnapshot이라는 cypress command를 사용하실 수 있습니다.
    setupNodeEvents(on, config) {
      addMatchImageSnapshotPlugin(on, config)
    }
  },
})

📌 script 명령어 소개

script 명령어는 총 4가지 명령어로 구성이 되어있습니다.

// 📌 콘솔에서만 보고 싶을 때
// --browser chrome 명령은 실행하는 환경을 정의합니다.
yarn test:cypress // "cypress run --browser chrome"
yarn test:snapshot // "ALLOW_SNAPSHOT=true cypress run -- browser chrome"

// 📌 실제 제대로 동작을 하는지 보고 싶을 때
yarn test:cypress-visual // "cypress open"
yarn test:snapshot-visual // "ALLOW_SNAPSHOT=true cypress open"

엥 왜 명령어가 4개야? 🤔

두가지 이유로 4개의 명령어가 탄생했습니다.

  • 추후에 있을 e2e 테스트와 Visual Regression Test를 분리 하기 위해
  • Visual based와 console based를 분리하기 위해

추후에 있을 e2e 테스트와 Visual Regression Test를 분리 하기 위해

ALLOW_SNAPSHOT을 통해 Visual Regression Test인지 e2e 테스트인지 분리를 합니다. 그리고 cypress.config.ts에서 env 설정 값으로 넘겨주게 되어있어요.

// cypress.config.ts
import { defineConfig } from 'cypress'
import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin'

export default defineConfig({
  e2e: {
    "baseUrl": "여기에 베이스 주소를 넣어주세요",
    env: {
      // cypress에 env 값을 넣어줍니다. ALLOW_SNAPSHOT이라면 true를 넣어주고 아니라면 false로 값을 저장합니다.
      "ALLOW_SNAPSHOT": process.env.ALLOW_SNAPSHOT === undefined ? false : true 
    },
    setupNodeEvents(on, config) {
      addMatchImageSnapshotPlugin(on, config)
    }
  },
})

그리고 위의 ALLOW_SNAPSHOT env를 통해 matchImageSnapShot 명령어를 재정의 해줍니다.

// cypress/support/commands
Cypress.Commands.overwrite(
  'matchImageSnapshot',
  (originalFn, snapshotName, options) => {
    if (Cypress.env('ALLOW_SNAPSHOT')) { // flag 값을 통해 변경이 가능함
      originalFn(snapshotName, options)
    } else {
      cy.log(`Screenshot comparison is disabled`)
    }
  }
)

ALLOW_SNAPSHOT이 true 라면 기존 cypress-image-snapshot의 명령어인 matchImageSnapshot을 실행하고 그렇지 않다면 그냥 로그를 찍는 형태로 재정의를 해줍니다.

Visual based와 console based를 분리하기 위해

Visual와 console을 분리하는 명령어는 run과 open으로 분리가 되었습니다.

Visual Console

📌 테스트 소개

그럼 이제 모든 준비가 끝났습니다. 이제 한번 테스트 코드를 작성해볼까요?

다음 테스트는 /notices/3rd-parties 라는 페이지로 들어가서 데스크톱 사이즈에서 한번 촬영을 하고 모바일 사이즈에서 다시 한번 촬영을 할 거예요.

describe('notices snapshot test', () => {
  beforeEach(() => {
    /** Init size of view port */
    cy.setDesktopViewport() // 화면을 desktop 사이즈로 변경
  })
  it('3rd parties snapshot test', () => {
    cy.visit('/notices/3rd-parties')
    /** Desktop size */
    cy.prepareSnapshot() // 🚨 이 명령어를 입력해야 스크린샷을 찍을 준비가 됌(스크롤이 가능해짐)
    cy.matchImageSnapshot('notices/3rd-parties-desktop', {
      capture: 'fullPage',
    })
    /** Mobile size */
    cy.setMobileViewport() // mobile 사이즈로 변경
    cy.matchImageSnapshot('notices/3rd-parties-mobile', { capture: 'fullPage' })
  })
})

위에서 사용 된 커스텀 명령어는 다음과 같습니다.

// cypress/support/commands

/**
 * @description Snapshot 을 촬영할 때 scroll 이 전체 영역을 다 찍을 수 있도록 변경
 */
Cypress.Commands.add('prepareSnapshot', () => {
  cy.get('div[id=root]')
    .invoke('css', 'position', 'absolute')
    .invoke('css', 'width', '100%')
})

/**
 * @description 페이지가 전부 랜딩을 되기 위해 wait(1000) 을 사용했지만 url을 통한 방법이 있어 visit 를 재활용
 * @see https://newdevzone.com/posts/how-to-wait-until-page-is-fully-loaded
 */
Cypress.Commands.overwrite('visit', (originalFn, args) => {
  originalFn(args)
  cy.url().should('include', 'https://local-ditto.devel.kakao.com/')
})

/**
 * @description Cypress 의 viewport 를 desktop size 로 조정
 */
Cypress.Commands.add('setDesktopViewport', () => {
  cy.viewport(1280, 800)
})

/**
 * @description Cypress 의 viewport 를 mobile size(IPHONE_XR) 로 조정
 */
Cypress.Commands.add('setMobileViewport', () => {
  cy.viewport(375, 667)
})

그리고 커스텀 명령어의 타입을 정의해줍니다.

// cypress/support/index.ts
declare namespace Cypress {
  interface Chainable {
    prepareSnapshot: () => void
    setDesktopViewport: () => void
    setMobileViewport: () => void
  }
}

그럼 이제 한번 실제로 돌려볼까요?

결과 이미지

가장 왼쪽에 있는 건 Reference Screens 입니다. 그리고 가장 오른쪽에 있는 건 Test Screens 입니다. Reference Screens가 Test Screens와 다르게 되면 중간에 보이는 것 처럼 Report가 발행이 됩니다. 어느 구간에서 어떻게 변경이 이루어 졌는지 나타내 줍니다.

📌 서버 데이터 mocking

만약 매번 새로운 데이터로 인해 테스트가 깨진다면 어떻게 대처를 할 수 있을까요? Cypress에서는 손 쉽게 intercept를 사용하여 server에서 내려주는 데이터를 mock 할 수 있습니다.

먼저 cypress/fixtures 폴더에 내려줄 데이터를 json으로 저장을 합니다.

  // apollo를 사용함으로 POST로 요청을 받아옵니다.
  cy.intercept('POST', '/api/v1', (req) => {
      if (req.body.operationName === 'QueryItem')
        req.reply({ fixture: 'shuttle-item.json' })
    }).as('data-fetch')
  // intercept 데이터가 다 내려올 때 까지 기다려줍니다.
  cy.wait('@data-fetch')

📌 결론

이렇게 styled-components에서 emotion으로 편하게 변경 작업을 진행 했습니다. 여러분도 혹시 화면에 대한 변경 사항을 일일이 쫓아가는 작업을 하시고 계시다면 Cypress snapshot test를 추천드릴게요. 그럼 긴 글 읽어주셔서 감사합니다! 🙇‍♂️ 그럼 또 만나요 안녕 ~

반응형