본문 바로가기

Frontend/Test

[Cypress] Cypress 도입기 (feat. MSW)

반응형

개요

이번달 초, 회사에서 운영 중인 서비스에 E2E 테스트 도입을 위해 Cypress 공부를 시작했다. 그동안 테스트 코드에 대한 관심은 있었지만 막상 시작할 엄두를 못 냈는데, 이렇게 실제 서비스에 적용할 수 있는 기회가 주어져 좋았다! 새로운 것을 시작할 때의 설렘 때문인지, 퇴사 전 마지막 임무가 주어진 느낌이라 그런지 왠지 더 잘하고 싶었다.

 

아무튼 테스트 코드에 대해 무지한 상태로 약 20일 간 공부하면서 Cypress를 적용했고, 어느 정도 돌아가는(?) 코드가 나온 시점에서 그 과정을 정리해보려고 한다.

 

 

Cypress와 MSW

Cypress?

  • 모던 웹을 위한 프론트엔드 테스팅 툴로, E2E 테스트에 최적화되어 있다.
    • 테스트란 서비스가 요구사항에 맞는지 검증하는 행위이고,
    • E2E 테스트란 사용자 관점의 테스트를 말한다.
  • 빠르고 쉽게 작성할 수 있고, 보다 안정적인 테스트를 제공한다.
  • 테스트 자동화를 통해 개발자와 QA 엔지니어의 리소스를 줄일 수 있다.

 

참고) cypress는 오픈소스이다.

 

MSW?

  • Mock Service Worker의 준말로 백엔드 모킹 툴이다.
  • 효율적인 테스트를 위해 실제 API를 사용하지 않고, 모킹한 데이터로 테스트를 돌리는 경우가 많아 cypress관련 글에 msw 소개가 함께하는 경우가 많다.
    • 왜? 이슈 트래킹에 용이하기 때문이다.
    • 백엔드와의 의존성을 차단해 테스팅을 하면 프론트 이슈만 확인할 수 있기 때문이다.
  • 모킹 시 주의할 점은 실제 데이터와 API의 동작을 잘 이해하고 있어야 한다는 것이다. 실제 동작에 맞춰 작성해야 실제와 같은 환경을 테스트할 수 있기 때문이다.

 

MSW에 대해 알게된 점도 많지만, 이번 글에서는 Cypress에 집중하기 위해 소개만 하고 넘어갈 것이다.

나중에 기회가 되면 따로 정리를..

 

 

Cypress 설치 및 실행

1. 설치하기

npm install cypress --save-dev

위 명령을 통해 cypress를 설치하면, 루트 폴더 하위에 cypress 폴더가 자동으로 생성된다.

 

기본 폴더 구조는 아래와 같다.

  • cypress/e2e: 테스트 파일의 위치
    • 테스트 파일 → 파일명.cy.ts
  • cypress/support: 공통으로 이루어지는 작업 혹은 설정
    • commands.ts: 커스텀 커멘드 작성

 

 

 

2. 실행하기

package.json에 스크립트를 추가 후

{
  "scripts": {
    "cypress:open": "cypress open"
  }
}

 

아래 명령을 입력하면

npm run cypress:open

 

이런 창이 뜬다. 아래 화면이 뜨면 cypress가 잘 실행이 되는구나!를 알 수 있다.

E2E 테스트 작성 순서

E2E 테스트 작성은 유저의 흐름을 그대로 따라간다고 생각하면 된다.

페이지 접근 → 테스트할 요소 찾기 → 요구사항 검증의 순서로 테스트 코드를 작성한다.

 

페이지 접근

1. 로그인

테스트할 페이지에 접근하기 전 필수로 거쳐야 하는 부분인 로그인은 커스텀 커멘드로 작성한다. 커스텀 커멘드는 support/commands.ts에 작성하며, 각 테스트에서 공통적인 부분을 함수화시켜 재사용을 위해 정의하는 메서드로 볼 수 있다.

 

이때 토큰을 유지하는 쿠키 관리에 꽤 애를 먹었는데, 결국 대부분의 원인은 잘못된 MSW 설정 때문이였고, cypress 설정 파일에서 약간의 값만 추가하면 큰 문제는 해결할 수 있었다.

 

 

cypress/support/commands.ts

Cypress.Commands.add('login', () => {
  const { email, password } = Cypress.env('oauth');
  cy.visit(`http://url:3000`);
  cy.get('input[type="text"]').click().type(email);
  cy.get('input[type="password"]').click().type(password);
  cy.get('button').should('have.text', '로그인').click();
});

 

2. 테스트 페이지 접근

cy.visit()으로 접근하는 것이 아닌, 유저 흐름대로 클릭을 통해 각 테스트 페이지에 접근했다.

 

cypress/e2e/endpoint.cy.ts

//커스텀 커멘드로 작성된 로그인이 테스트 실행 전 실행되도록 한다
before(() => {
  cy.login();
});

describe('메인 페이지 테스트', () => {
  before(() => {
    cy.get('li').contains('제목').click(); //클릭을 통해 테스트 페이지로 이동
  });
});

export {};

 

 

테스트 요소 찾기

페이지에 들어갔으면 이제 테스트할 요소를 찾아야 한다.

 

강의를 통해 cypress를 접할 때는 태그랑 속성을 이용해서 cy.get()으로 요소를 가져오면 되는구나!라고 생각했었다. 쉽게 생각했는데.. 개발자 도구로 HTML을 확인해보니 전부 div 뿐이였다.

 

🔒 문제: div 지옥 → 각 요소를 특징할 수 있는 것이 없다.

 

🔐 해결 과정

공식문서에서 제안하는 요소를 가져오는 BEST 방법

<button
  id="main"
  class="btn btn-large"
  name="submission"
  role="button"
  data-cy="submit"
>
  Submit
</button>

공식 문서에 의하면, 변화의 가능성이 낮은 값을 통해 요소를 가져오는 것이 좋다고 한다.

  • 태그, 클래스, 아이디는 가변적이므로 사용을 지양하고,
  • contains를 통해 컨텐츠의 텍스트를 이용하거나,
  • data-cy 속성을 추가해 테스팅만을 위한 selector를 만드는 것이 좋다고 한다.
    • 이는 스타일이나 동작에 영향을 받지 않고,
    • 해당 요소가 테스트에 사용된다는 것을 분명히 할 수 있는 방법이기 때문이다.

 

두 가지의 선택권이 생겼다.

 

방법 1. data-cy 속성 추가하기

  • 장점: 불변 값으로 테스팅 코드 작성 가능
  • 단점: 기존 코드 리팩토링 필요 → 의존성 생김 + 충돌 우려 + 귀찮음

방법 2. contains 활용

  • 장점: 기존 코드 건들이지 않아도 됨 → 테스트와 기능의 관심사 분리
  • 단점: 어쩔 수 없이 가변적인 값

 

🔑 결론: contains 활용

처음에는 1번을 시도했다.

하지만 컴포넌트에 추가적인 속성 값을 직접 넣을 수가 없는 문제가 있었다. 아래와 같이 기본 태그에 속성 값 추가는 가능하지만, 컴포넌트에는 data-cy라는 속성을 별도로 추가할 수가 없어 html document에 반영이 되지 않는 것이다. (사실 컴포넌트 props에 냅다 넣으니 안되는 건 당연하다ㅠ)

<DetailGrid title="기본 정보" data={baseInfoData} data-cy="basic-info"/> // 반영X
<span data-cy="basic-info">test</span> //반영 O

 

참고) material UI에 data-cy 속성을 추가하려고 했는데 실패한 사례. 저자는 이 경우 그냥 id를 추가해서 쓰자라는 결론을 내림. 이 사람의 말이 data-cy에 미련을 버리는데 큰 역할을 했다.

 

 

따라서, 방법2 contains 활용하기로 결정!

  • contains을 쓰면서,
  • parent, children, sibilings 등을 활용해 계층 구조를 활용해 테스트 요소를 찾고
  • as 활용해 별칭을 정하면서 범주를 설정하고 요소를 재사용하기로 했다.
describe('상세 페이지 테스트', () => {
  ...

  it('기본 정보가 출력된다.', () => {
    cy.contains('기본 정보').parents('div[width="medium"]').siblings().as('basic-info');

    cy.get('@basic-info')
      .contains('엔드포인트')
      .parent()
      .parent()
      .siblings()
      .should('have.text', RESOURCE.publicEndpoint);

    cy.get('@basic-info')
      .contains('API Key')
      .parent()
      .parent()
      .siblings()
      .should('have.text', RESOURCE.APIKey);
  });
});

 

검증

검증은 cy.should()cy.contains()를 통해 이루어졌다. 사실 contains는 동작 검증 용도가 아니지만, READ 테스트에서 텍스트의 존재 여부를 파악하는 데는 용이해서 사용했다. (READ 한정 검증 용도)

 

스키마 상 결과(raw data)와 화면 출력 결과가 일치하면 should를, 화면 출력에 추가 텍스트가 존재해 일치하지 않으면 contains를 사용했다.

 

 

참고 사항 및 느낀 점

  • contains를 통해 요소를 가져오면서, 테스트가 현재 스타일 구조에 의존성이 생겼다. 로직 리팩토링 작업이 있다면 이후 테스트를 돌려서 깨지는 부분이 있는지 검증이 필요하다.
  • 테스트 코드도 짜다보니 중복이 많다. 함수화시켜 공통화 리팩토링 작업이 필요하다.
  • 테스트 케이스에 대한 정리가 필요함을 느꼈다.
    • 어디까지 테스트를 할 것인가?에 대한 고민
    • 테스트 코드 작성 시 QA와 테스트 케이스를 공유해 이를 기반으로 작성하는 것이 좋을 것 같다.

 

Reference

반응형