Cypress로 e2e 테스팅 하기

jiny·2023년 7월 10일
0

FE Testing

목록 보기
2/2
post-thumbnail
post-custom-banner

문벅스 앱에 대해 테스팅 라이브러리인 Cypress를 활용하여 E2E 테스팅을 해본 것에 대해 기록해보려고 한다.

소프트웨어 테스트란?

프로그램에 있는 오류를 찾기 위해 프로그램을 실행하여 실행 결과와 예상 결과 를 비교하고 검토하는 과정

대표적인 소프트웨어 설계의 과정을 이야기하면 요구사항 분석 - 시스템 설계 - 아키텍처 설계 - 모듈 설계 - 구현 - 테스트와 같이 이뤄지게 되며 테스트는 가장 마지막 단계에 위치해 있다.

테스트는 유저에게 제품을 공개하기 전 잘못된 부분이 있는지, 예외 처리는 깔끔하게 이뤄졌는지를 꼼꼼하게 검증하는 단계이다.

테스팅을 진행하면 프로그램의 잠재된 오류, 성능적인 이슈, 유저의 입장에서 제품을 확인할 수 있는 장점이 있어 대부분의 회사에서 필수적으로 거치는 단계 중 하나이다.

테스트의 종류로는 크게 유닛 테스트, 통합 테스트, UI 테스트, E2E 테스트로 구분 지을 수 있다.

테스트의 종류

유닛 테스트

컴퓨터 프로그래밍에서 소스 코드의 특정 모듈이 의도된 대로 정확히 작동하는지 검증하는 단계

유닛 테스트는 소프트웨어 중 가장 작은 단위인 모듈, 함수, 클래스를 테스트하는 방법으로, 개발자가 작성한 코드를 개발자가 코드로 테스팅하여 정확하고 빠르게 버그를 찾아낼 수 있다.

또한, 프로그램의 각 부분을 고립 시켜서 각각의 부분이 정확하게 동작하는지 확인함으로써 프로그램을 작은 단위로 쪼개어 각 단위가 정확하게 동작하는지 검사하고 이를 통해 문제 발생 시 정확하게 어느 부분이 잘못되었는지를 재빨리 확인할 수 있게 해준다.

통합 테스트

단위테스트로 검증이 끝난 모듈들을 결합한 후, 각 모듈간 상호작용이 잘 일어나는지 확인하는 단계

여러 개의 단위 테스트가 완료되면 통합 테스트를 통해 여러 모듈을 동시에 테스트 하며 각 모듈 간 상호 작용 시 이슈가 발생하는지 확인 하는 작업이라고 할 수 있다.

UI 테스트

UI의 사용성을 검증하기 위한 단계

UI 테스트는 흔히 컴포넌트 테스트로써, 프론트엔드에서 만들어진 컴포넌트들을 독립된 환경에서 요구사항 대로 동작하는지 테스트하는 과정이라고 볼 수 있다. 대표적인 툴로는 Storybook이 있다.

E2E 테스트

유저 입장에서 애플리케이션의 흐름을 처음부터 끝까지 테스트하는 단계

E2E는 End to End라는 뜻으로, 실제 사용자가 사용하는 흐름대로 소프트웨어를 테스트 하는 단계이다. 모든 요구사항들에 대해 앱이 예상대로 동작되는지 확인하는 말 그대로 처음부터 끝까지 앱의 모든 것을 확인해보는 단계라고 할 수 있다.

이번에 사용한 툴인 Cypress는 대표적인 JavaScript의 E2E 테스팅 라이브러리이다.

Cypress

JavaScript Component Testing and E2E Testing Framework - cypress.io -

브라우저와 브라우저에서 실행되는 풍부한 기능의 JavaScript 테스트 프레임워크인 Mocha를 기반으로 구축된 JavaScript 기반 엔드투엔드 테스트 프레임워크로, 비동기 테스트를 간단하고 편리하게 수행할 수 있다.

Cypress는 또한 BDD/TDD 어설션 라이브러리와 브라우저를 사용하여 모든 JavaScript 테스트 프레임워크와 페어링할 수 있다.

Core Concept

Real-time Reloading

테스트 파일을 저장하자마자 브라우저 옆에서 자동으로 실행을 트리거하는 특징을 가지고 있다.

이로 인해 수동으로 실행을 트리거 하거나 따로 대기할 필요가 없다.

Automatic Waiting

요소가 표시되고, 애니메이션이 완료되고, DOM이 로드되고, XHR 및 AJAX 호출이 완료될 때까지 기다리는 특징을 지니고 있다. 따라서 암시적 및 명시적 대기 시간을 정의할 필요가 없다.

Easy Debugging

테스트 시나리오에 대한 시각적 표현을 제공하며 편리한 디버깅이 가능하다.

또한, 실패한 테스트에 대한 스크린샷과 동영상을 캡처하여 문제를 찾고 해결할 수 있다.

Time-travel Debugging

테스트 실행과정에서 Cypress는 각 단계를 기록하며, 이를 통해 필요한 단계에 대한 디버깅을 따로 실시 할 수 있다.

Network Traffic Control

테스트 중인 앱과 서버 간 네트워크 요청과 응답을 감지 및 제어할 수 있다. 이를 통해 서버와 클라이언트 간 통신 상태를 테스트하는데 도움을 줄 수 있다.

Cross browser Testing

여러 브라우저 (chrome, firefox, Electron, Edge, safari) 등에서 테스트가 가능하며 이를 통해 특정 브라우저 내 발생할 수 있는 문제를 찾아 해결 할 수 있다.

Extensibility

다양한 플러그인을 통해 기능 확장이 가능하며, 맞춤형 테스트 도구 구성이 가능하다.

이 밖에 GUI와 Cli로 다양한 환경에서 테스팅이 가능하다는 특징도 존재한다.

Cypress Setting

npm install -D cypress
yarn add -D cypress

해당 명령어를 통해 cypress를 설치 한 후 다음의 명령어 들을 통해 GUI 또는 CLI로 실행시킬 수 있다.

  • npx cypress open (GUI)
  • npx cypress run (CLI)

아니면 package.jsonscripts 에 다음과 같은 명령어를 통해 npm test로 간단하게 실행도 가능하다.

처음 Cypress를 실행 하면 다음과 같은 폴더들이 생성된다.

+--cypress.config.js
+--cypress
      +--e2e
      |   +-- test1.cy.js
      |   +-- test2.cy.js
      +--fixtures
      |   +-- data.json
      |   +-- image.png
      +--support
      |   +-- commands.js
      |   +-- e2e.js
      +--screenshots
      +--videos
      +--downloads

cypress.config.js

  • cypress의 실행 환경을 관리하는 config 파일
  • 기존 plugins 폴더를 대체하는 역할 이다.
const { defineConfig } = require("cypress");

module.exports = defineConfig({
  projectId: 'janjrc',
  e2e: {
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
  },
});

package.json 파일과 동일한 경로에 있으며, 테스트를 위한 환경 변수 및 viewport 등의 설정을 할 수 있다.

  • e2e - 테스트 간 필요한 plugin을 관리
  • env - 테스트 실행에 대한 환경 변수 설정
  • baseUrl - 테스트를 실행하는 url 주소의 기본 값을 설정

e2e

  • 실제 테스트 코드가 포함되어 있는 디렉토리
  • 해당 디렉토리 내 테스트 파일 들은*.cy.js 형식으로 파일을 작성해야 한다.

fixtures

  • 테스트에 필요한 mock data를 담고 있는 디렉토리
  • json, image, ts 등의 데이터 파일들을 저장 한다.

support

  • commands 및 plugins 등을 전역 관리 하는 디렉토리
  • 테스트 실행 전 최우선적으로 import 된다.

screenshots / videos

  • 테스트 실행 결과가 담긴 스크린 샷과 비디오 파일이 자동으로 생성되는 디렉토리
  • GUI와 Headless 실행 이후 실패한 결과값만 확인할 수 있다.

Structure

테스트 파일을 작성하게 되면 크게 다음과 같은 구조로 나눌 수 있다.

describe('Test E2E by Cypress', () => {
  context('Mobile Version', () => {
  	beforeEach(() => {
      // ... Codes executed before each test case inside it() hooks ...
    });
    
    it('Login Button Should be Somewhere', () => {
      // ... Real Executing test codes for some application ...
    }); 
  });
});
  • describe(name, config, fn) - 테스트 코드를 묶는 가장 큰 단위
  • context(name, config, fn) - describe 내부에서 새로운 단위로 다시 묶는 hook
  • beforeEach(fn) - 테스트 코드에서 먼저 실행되는 부분을 묶는 hook
  • it(name, config, fn) - 실제 테스트 코드가 작성되는 hook

이번에 테스팅 코드를 작성하면서 describe는 page 단위, context는 기능 단위, it은 각 요구사항 단위로 작성하였으며, 대표적인 예는 다음과 같다.

describe("main page testing", () => {
  beforeEach(() => {
    cy.visit("../../index.html");
  });
  context("동작 관련 테스팅", () => {
    it("각 카테고리를 클릭 후 디저트 카테고리까지 클릭 되어있는 상태에서 리로딩을 하게 되면 에스프레소 메뉴판이 보여져야 한다.", () => {
      const menuCategorys = ["frappuccino", "blended", "teavana", "desert"];
      menuCategorys.forEach((category) => {
        cy.get(`[data-category-name=${category}]`).click();
      });
      cy.reload();
      cy.get("#category-title").contains("☕ 에스프레소 메뉴 관리").should("be.visible");
    });
    it("초기 렌더링 시 로컬 스토리지에 있는 데이터를 가져와 화면에 보여줄 수 있어야 한다.", () => {
      cy.registerLocalStorage();
      cy.reload();
    });
  });
});

실제로 Cypress를 실행해보면 위 사진과 같이 나눠지는 것을 확인해 볼 수 있다.

개인적인 의견이지만 RTL이나 Storybook을 사용해봤지만 Cypress가 정말 가독성이 좋다고 느껴졌다.

API

이번에 문벅스 애플리케이션을 테스팅해보며 사용했던 API들을 위주로 작성하며, 만약 또 다시 Cypress를 이용한다면 업데이트 될 예정이다.

Queries

cy.get(selesctor : string)

DOM API 중 document.querySelector()와 유사한 메서드로써, DOM Element를 반환한다.

cy.get("#menu-name") // id
cy.get(".menu-count") // class
cy.get("#menu-list li") // child node
cy.get(`[data-category-name=${category}]`) // attribute

cy.contains(textParams : string)

특정 문자열이 포함된 DOM Element 태그를 반환한다.

cy.get("#menu-list li").contains(addMenuName)
cy.get(".menu-count").contains("총 1 개")
cy.get("#category-title").contains("☕ 에스프레소 메뉴 관리")

Queries

그 밖에도 아주 많은 API 들을 제공하고 있다.

Actions

.click()

특정 DOM Element에 대해 click event를 발생시키는 메서드이다.

cy.get("#menu-submit-button").click();
cy.get(`[data-category-name=${category}]`).click();

.type()

특정 DOM element로 부터 typing을 지원하는 메서드이다. 주로 input 태그로 부터 사용하는 편이다.

cy.get("#menu-name").type(addMenuName);
cy.get("#menu-name").type(addMenuName).type("{enter}"); // {enter}는 keypress 이벤트를 발생시킨다.

Actions

더 많은 Actions API는 여기서 찾아 볼 수 있다.

Assertions

.should()

.should(chainers)
.should(chainers, value)
.should(chainers, method, value)
.should(callbackFn)

should의 경우 Assertion 쿼리로써, 주로 첫번째 인자로 assertion 하기 위한 옵션, 두번째 인자로 예상 결과를 작성할 수 있다.

jest의 expect().to()와 비슷한 구문이다.

cy.get("#menu-list li").contains(addMenuName).should("be.visible"); // 요소가 존재하는지 확인
cy.get("#menu-name").should("have.value", ""); // 값이 특정 값인지 확인
cy.get("#menu-list li").should("have.length", 2); // 요소의 갯수가 특정 갯수인지 확인
cy.get("[data-menu-id='0'] .menu-name").should("be.class", "sold-out"); // 특정 class attribute를 가지는지 확인
cy.get("[data-menu-id='0'] .menu-name").should("not.have.class", "sold-out"); // 특정 class attribute를 가지지 않는지 확인
cy.get("#menu-list").should("be.empty"); // 요소가 하나도 없는지 확인

첫번째 인자의 경우 정말 많은 옵션을 제공하기 때문에 docs 내 assertion 부분을 반드시 참고하자.

etc

cy.visit()

주로 beforeEach 구문에서 작성되며 테스트를 실행할 주소를 의미한다. baseUrl을 설정했다면 "/"의 형태로 생략 가능하다.

cy.visit("../../index.html");
cy.visit("http://www.naver.com");

Cypress.Commands.add(fnName, callback)

주로 재사용 함수를 만들기 위한 메서드 이며 다음과 같이 사용이 가능하다.

// support/commands.js
Cypress.Commands.add("registerLocalStorage", () => {
  const mockMenu = {//...};

  cy.window().then((win) => {
    win.localStorage.setItem("menuName", JSON.stringify(mockMenu));
  });

  cy.reload();
});
// mainPage.cy.js
it("사용자 입력값이 빈 값이라면 추가되지 않는다.", () => {
  cy.registerLocalStorage();
});

cy.window()

Cypress에서 현재 열려있는 창(브라우저 윈도우)에 대한 참조를 가져오는 명령으로 Cypress가 제공하는 브라우저 API를 사용하여 현재 창에 대한 정보를 가져올 수 있다.

context("메뉴 수정 관련 테스팅", () => {
    it("등록 된 메뉴들에 대해 첫번째 메뉴 이름 수정이 가능해야 한다.", () => {
      cy.registerLocalStorage();
      cy.window().then(($win) => {
        cy.stub($win, "prompt").returns("카페 모카");
        cy.get("[data-menu-id='0'] .menu-edit-button").click();
      });
      cy.get("[data-menu-id='0'] .menu-name").contains("카페 모카");
    });
  });

cy.stub()

Cypress에서 테스트 도중에 호출되는 함수나 메서드를 대체하여 테스트 결과를 조작할 수 있도록 해주는 명령으로 이를 통해 미리 정의된 값을 반환하거나, 특정 동작을 수행하지 않도록 만들 수 있습니다.

cy.stub() 명령을 사용하여 함수 또는 메서드를 스텁(stub)으로 대체하면, 해당 함수 또는 메서드가 호출될 때마다 스텁으로 정의한 동작이 수행됩니다.

위 코드에서는 confirm()을 가로채어 "카페 모카"를 입력 후 ok 버튼을 눌러주는 역할을 수행한다.

cy.get("[data-menu-id='0'] .menu-edit-button").click()가 실행 되면 해당 코드가 비동기적으로 실행된다.

cy.reload()

테스트 중인 페이지를 새로 고침 하는 역할을 수행하는 메서드

레퍼런스

post-custom-banner

0개의 댓글