본문 바로가기

cypress를 써보자! (1)

cypress를 써보자! (2) >

cypress를 써보자! (3) >

 

 

프로젝트를 쭉 담당하면서 단위테스트만으로는 부족하다고 생각될 때가 왔다.

저번주에 운영환경을 체크하는 로직을 라우터 페이지에 넣어버리는 실수로 인해 리얼환경에서 잘 동작하는지 반드시 확인해야 할 필요가 생겼다. 또한 QA 과정(혹은 단위테스트)에서 발견이 어려웠던 이슈들이 속속들이 나오고 있었다.

따라서 새 서비스 개발에 착수하기 전에 작년부터 해보고 싶었던 cypress를 도입하여 통합 테스트를 작성하고자 했다.

 

이번주에는 cypress 첫 설치와 실행 그리고 간단한 테스트 코드 작성으로 사용하는 것까지 공부하면서 기록했다.

 

1. cypress 설치

글쓴이는 외부와 차단된 private 망에서 개발을 하고 있다. 따라서 외부 깃헙에서 라이브러리를 받을 수 없어 node-sass와 같이 install하는데 제한이 있는 라이브러리와 마찬가지로 cypress도 단순한 npm install 로는 받을 수 없었다.

 

따라서 cypress 바이너리 파일을 직접 환경변수로 export해주기로 했다.

 

먼저 원하는 cypress binary를 받는다. (다운 경로 안내)

 

글쓴이는 6.4.0으로 다운받았다.

최신버전은 5/10 기준 7.2.0이었는데 최신보다 stable한 버전을 쓰고 싶어서 6버전을 선택했다.

(글을 쓰는 시점에서는 7.3.0이 최신이다👀)

 

다운로드가 완료되면 CYPRESS_INSTALL_BINARY 환경변수로 binary 파일이 위치한 절대경로를 설정한다.

export CYPRESS_INSTALL_BINARY="/Users/workingnewjeong/Downloads/cypress.zip"

이제 프로젝트 디렉토리에서 설치할 버전을 명시하여 npm install한다.

npm install cypress@6.4.0 --save-dev

정상적으로 설치되었다면 이제 cypress install 한다.

공식 문서에 따르면 npm을 통해 install하면 프로젝트의 ./node_modules/.bin에 실행가능한 바이너리와 함께 cypress가 설치되어 아래 명령어들로 install할 수 있다.

./node_modules/.bin/cypress open

$(npm bin)/cypress open

npx cypress open

정상적으로 install되었다면 아래처럼 ${사용자 경로}/Library/Caches/Cypress/6.4.0 에 설치되었음을 확인할 수 있다.

 

또, 프로젝트 디렉토리에 cypress/* 가 추가된 것을 확인할 수 있다.

 

기본적으로 아래 4가지 디렉토리를 갖고 있다.

 

- fixtures : 정적인 데이터 (HttpRequest용 mock응답 작성 등)

- integration : test 코드가 위치하는 곳

- plugins : 각 테스트파일마다 동적으로 실행되는 플러그인 설정

- support : 각 테스트파일마다 실행되며, 전체 테스트 코드에 적용가능한 커스텀 커맨드등을 작성하는 곳

2. Cypress 실행

테스트 코드 작성 전, Cypress를 실행해본다.

글쓴이는 package.json에 아래처럼 추가하여 사용하였다.

// in package.json
scripts: {
  "test:ci:local": "cross-env REACT_APP_PROFILES=local ./node_modules/.bin/cypress open",
  "test:ci:real": "cross-env REACT_APP_PROFILES=real ./node_modules/.bin/cypress open"
}
}

 local 환경과 real 환경 각각에서 실행해보고 싶어서 이렇게 두개를 추가하였다.

npm run test:ci:local 로 실행해보면 아직은 테스트가 없어서 목록이 아무것도 없을 것이다.

 

3. Cypress 환경 설정

cypress를 더 잘 사용하기 위해서 프로젝트 환경에 맞게 환경 설정을 해줄 필요가 있다.

 

3.1 jsconfig.json (tsconfig.json)에 cypress 절대 경로 추가

서비스 규모가 방대해지면서 상대경로만으로는 다른 파일 경로 작성이 번거로워 alias와 함께 추가된 jsconfig.json 설정이 되어있다. cypress 에서도 절대경로 사용을 위해 아래처럼 include에 추가하였다.

{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      // alias 설정
      "$cypress/*": ["../cypress/*"] // baseUrl이 src임을 고려
    }
  },
  "include": ["src", "./node_modules/cypress", "cypress"]
}

5/23 추가. Webpack resolve.alias 추가

 

7/11 추가. paths 추가

 

3.2 cypress/plugins/index.js 에서 webpack-preprocessor 설정

스타일 관리를 scss로 하다보니 웹팩 설정도 필요했다.

 

사내에서는 react-scripts에서 자동으로 설정되는 웹팩 설정을 쓰는데 cypress에서 같은 환경대로 설정할 수 있는 레퍼런스를 찾아서 추가하였다. (레퍼런스)

// @see https://github.com/cypress-io/cypress/blob/master/npm/webpack-preprocessor/examples/react-app/cypress/plugins/index.js
const webpackPreprocessor = require('@cypress/webpack-preprocessor')
const findWebpack = require('find-webpack')

module.exports = (on) => {
    const webpackOptions = findWebpack.getWebpackOptions()

    if (!webpackOptions) {
        throw new Error('Could not find Webpack in this project.')
    }

    const cleanOptions = {
        reactScripts: true,
    }

    findWebpack.cleanForCypress(cleanOptions, webpackOptions)

    const options = {
        webpackOptions,
        watchOptions: {},
    }

    on('file:preprocessor', webpackPreprocessor(options))
}

 

3.3 호스트 설정

cypress에서는 호스트를 설정할 수 있는 다양한 방법을 제공한다.

 

가장 쉬운 방법으로는 cypress.json의 baseUrl 로 고정 호스트를 설정한다.

// in cypress.json
{
  baseUrl: 'http://localhost:3000'
}

 

혹은 커맨드의 --config 옵션에서 추가하는 방식이 있다.

cypress open --config baseUrl=http://localhost:3000

 

마지막으로 plugin/index.js에서 동적으로 호스트를 설정하는 방법이 있다.

글쓴이는 local, real 환경에서 사용하는 port가 달라서 이 방법으로 호스트를 설정하였다.

// @see https://github.com/cypress-io/cypress/blob/master/npm/webpack-preprocessor/examples/react-app/cypress/plugins/index.js
const webpackPreprocessor = require('@cypress/webpack-preprocessor')
const findWebpack = require('find-webpack')

const getHostByProfiles = (profile) => { // 환경변수로 호스트 설정
    switch (profile) {
        case 'local':
            return 'http://localhost:3000'
        default:
            return 'http://localhost:5000'
    }
}

module.exports = (on, config) => {
    const webpackOptions = findWebpack.getWebpackOptions()

    if (!webpackOptions) {
        throw new Error('Could not find Webpack in this project.')
    }

    const cleanOptions = {
        reactScripts: true,
    }

    findWebpack.cleanForCypress(cleanOptions, webpackOptions)

    const options = {
        webpackOptions,
        watchOptions: {},
    }

    on('file:preprocessor', webpackPreprocessor(options))

    config.env.host = getHostByProfiles(process.env.REACT_APP_PROFILES)

    return config
}

 

플러그인에서 config.env에 host를 넣어주면, 테스트 코드단에서 Cypress.env('host') 로 사용가능하다.

cypress.visit(`${Cypress.env('host')}${ROUTES.MAIN}`)

 

4. 테스트 코드 작성 시작!

이제 테스트 코드를 작성해본다. 간단하게 메인페이지를 방문하여 버튼을 클릭하는 코드를 작성해보았다.

describe('demo test', () => {
  it('enter home and button click', () => {
    /** 1. Enter Home */
    cy.visit(`${Cypress.env('host')}${ROUTES.MAIN}`)

    cy.get(`${element}.${className}`)
      .click()
      .then(() => {
        cy.location().should(({pathname}) => {
          expect(pathname).to.eq(path)
        })
      })
  })
})

 

정말 간단하다! Cypress Core Concept에서도 강조하는 Simplicity가 잘 반영되었다!

 

중요한건 각 cy.* 커맨드들이 async로 동작한다는 것이다. 따라서 then을 통한 chaining이 가능하다. (참고)

 

Remember: Cypress commands are asynchronous and get queued for execution at a later time. During execution, subjects are yielded from one command to the next, and a lot of helpful Cypress code runs between each command to ensure everything is in order.

(Chains of Commands 부분은 읽으시는 것을 추천드립니다. Cypress 테스트 코드 작성에 큰 도움이 됩니다 👍)

 

5. 커스텀 커맨드 추가하기

그런데 스펙이 점점 늘어남에 따라 중복되는 코드가 너무 많이 보이기 시작했다. Cypress는 이러한 중복 코드를 제거하기 위해 사용자가 직접 커스텀 커맨드를 작성할 수 있는 방법을 제공한다. 새로운 커맨드를 추가할 수도 있고, 기존 커맨드를 overwitre할 수도 있다.

 

supports/command.js가 바로 그 공간인데, 글쓴이는 우선 bulkIntercept와 checkLocation을 추가하였다.

import queryString from 'query-string'

/** 
 * @description 다수의 HttpRequest를 리스트로 받아서 intercept
 * @example
 * cy.bulkIntercept([
 *   {
 *      method: 'GET',
 *      url: '/users',
 *      res: {
 *        fixture: 'users.json'
 *      }
 *   }
 * ])
*/
Cypress.Commands.add('bulkIntercept', (list) => {
    list.forEach(({method, url, res}) => {
        cy.intercept(method, `${Cypress.env('host')}${url}`, res)
    })
})

/** 
 * @description location 확인
*/
Cypress.Commands.add('checkLocation', ({path = '', query = {}} = {}) => {
    cy.location().should(({pathname, search}) => {
        expect(pathname).to.eq(path)

        if (Object.keys(query).length > 0) {
            expect(queryString.parse(search)).to.contains(query)
        }
    })
})

 

0
작성 결과

정리

이렇게 Cypress 설치부터 짧은 사용까지 하면서 일주일을 보냈다.

Cypress는 테스트 코드를 작성하는 것에 온전히 집중할 수 있게 해주는 것 같았다.

 

jest는 내가 입사하기 전에 팀원분들께서 미리 환경이나 mocking functions, 참고할 수 있는 많은 테스트 코드들을 잘 작성해주셔서 어렵게 느껴지지 않았지 처음부터 하라고 했으면 100% 헤맸을 것이다. 그만큼 setup하는 과정에서 크게 어려움이 있었을텐데 (너무 감사ㅠㅠ) Cypress는 상대적으로 어렵지 않았던 거 같다.*

* 물론 술술 풀렸던 건 아니다 😅 글은 setup 과정을 순서대로 나열해서 적었지만 사실은 뒤죽박죽으로 설정해서 이번에 글을 쓰면서 경험을 되새김질한 것이다.

 

다음주에는 CI에 대해서도 재설정해보려고 한다. 지금 구현된 상태로는 서버를 띄워야 테스트가 동작하는데 이를 한번에 제공하지 못하고 있기 때문이다.

 

아직 Cypress가 제공하는 수많은 기능의 반의 반도 못써보았지만 이제 1주차니까! e2e 테스트를 추가해나가면서 공부할 것이다.