이전 포스팅에서는 Cypress를 설치하고 실행하는 명령어에 대해 간단하게 알아보았습니다.
이번에는 실제 테스트 코드를 작성하고 반복되는 테스트 코드는 어떻게 모듈화하여 관리할 수 있는지 그 방법에 대해 정리해보려고 합니다.
물론 공식 홈페이지에도 잘 나와있습니다.
절대적으로 양이 적은 한글문서.. 많은 개발자분들께 적게나마 도움이 되었으면 좋겠습니다.


✨ Test Structure


Cypress를 실행하고 테스트 코드가 없는 상태에서 GUI 앱을 실행해 보신 분들은 대충 눈치챘을 것입니다.
자동으로 생성된 예제 테스트 코드 모습이 jest 혹은 mocha와 많이 비슷하지 않나요?

실제로 Cypress 공식 문서 상에서도 다음과 같이 안내하고 있습니다.

💡 The test interface, borrowed from Mocha, provides describe(), context(), it() and specify().
context() is identical to describe() and specify() is identical to it(), so choose whatever terminology works best for you.


Structure and Hooks

실제로 예제 코드를 통해 구조를 알아보겠습니다.
예제 코드에서 사용하는 hook은 다음과 같습니다:

  • describe(name, config, fn) : 테스트 코드를 묶는 가장 큰 단위
  • context(name, config, fn) : describe() 내부에서 새로운 단위로 다시 묶는 hook
  • beforeEach(fn) : 각각의 테스트 코드 (it())보다 먼저 실행되는 코드를 묶는 hook
  • it(name, config, fn) : 실제 테스트 코드가 작성되는 hook
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 ...
    }); 
  });
});


✨ E2E Basic Codes


이미 Cypress 공식 문서에서 다양한 메서드와 플러그인을 제공하고 있습니다.
또 하나의 장점 중 하나로 문서화가 굉장히 잘 되어 있다는 점을 꼽을 수 있는데요.

Cypress를 처음 접하는 분들도 충분히 CSS Selector에 대한 감만 익히면 충분히 사용할 수 있을 것입니다.

이번에는 E2E Test Code를 작성할 때 가장 많이 사용될 것으로 보이는 메서드 몇가지만 알아보겠습니다.
주관이 개입되어 있습니다. 팀 혹은 프로젝트 진행헤 필요한 내용은 공식문서를 참고해주세요.🙏

cy.get(selector: string)

Cypress DOM Element 태그를 탐색하는 코드입니다.
JavaScript에서 사용하는 document.querySelector('Selector') 쿼리와 비슷한 역할을 합니다.

cy.get() 쿼리는 Selector에 해당하는 태그를 1개 혹은 1개 이상 찾아 반환해줍니다.

사용 방법은 아래와 같습니다:

cy.get('tagName');
cy.get('.className');
cy.get('#idName');

cy.get('[class^="button_"]'); // button_ 가 포함된 클래스 이름을 가진 태그를 찾아라.

cy.contains(textParams: string)

특정 문자열이 포함된 DOM Element 태그를 찾는 쿼리입니다.

사용 방법은 아래와 같습니다:

cy.get('h1').contains('제목');

우선, h1 태그(들)을 찾습니다. 이후 contains()의 인자로 특정 문자열을 입력하게 되면, 찾은 태그들 중 text로 해당 문자열을 가지고 있는 태그들만 다시 찾아 반환해줍니다.


cy.click()

DOM Element 태그를 클릭할 수 있는 쿼리입니다.

사용 방법은 아래와 같습니다.

cy.get('h1').contains('제목').click();

h1 태그(들)를 찾아 중 "제목"이라는 텍스트를 가진 태그를 찾아 클릭합니다.


cy.should(option, expectResult)

Cypress의 Assertion 쿼리입니다.
should() 쿼리의 첫번째 인자로 assertion을 하기 위한 옵션을 설정할 수 있습니다.
should() 쿼리의 첫번째 인자로 옵션에 대한 예상 결과를 작성할 수 있습니다.

많이 사용하고 있는 jest 모듈의 expect().to() 구문과 거의 유사합니다. (실제로 이 구문도 사용할 수 있으니, 참고하면 좋겠습니다.)

사용 방법은 아래와 같습니다:

cy.get('h1').should('have.text', '제목');

h1 태그를 찾아 그 태그가 가진 텍스트가 '제목'과 일치하는지 판단합니다.
Assertion 구문이 없어도 테스트를 진행하는데 무리가 없지만, Assertion 구문이 있는 경우, 예측 값과 다른 결과가 도출된다면 해당 테스트 케이스는 fail 결과를 반환해줍니다.

should() 쿼리의 옵션은 다양하니 필요한 옵션을 때에 맞게 사용하면 됩니다.


cy.visit(url: string)

특정 도메인 url로 접속할 수 있는 쿼리입니다.
E2E 테스트를 하면서 유저가 실제로 로그인하는 과정부터 서비스를 이용하는 마지막 단계까지의 플로우를 고려해야하는데요.

그 과정에서 버튼을 클릭하지 않고 메인 페이지에 접속하거나, 특정 페이지로 이동해야하는 경우가 있습니다. 예를 들어, 각 테스트 케이스 마다 테스트 시작 페이지를 초기화 하기 위해 beforeEach() 훅에 메인 페이지로 접속하는 코드를 작성하기 위해 아래와 같이 작성할 수 있습니다:

beforeEach(() => {
  cy.visit('https://example.com/');
})

만약 cypress.config.ts 파일에서 --config 플래그로 baseUrl 옵션을 설정해두었다면 아래와 같이 작성할 수도 있습니다:

beforeEach(() => {
  cy.visit('/');
})

.then(option, callback fn)

보통 JavaScript에서 가리키는 then은 Promise 객체를 resolve하기 위해 사용하는 키워드일텐데요.
Cypress에서는 then() 메서드를 다음과 같이 설명하고 있습니다:

Enables you to work with the subject yielded from the previous command.

즉, then() 쿼리를 사용하였을 때, 이전에 실행한 subject (=태그)

Cypress의 각각의 구문은 JavaScript에서 await 키워드를 사용했을 때와 동일하게 동작합니다.

코드로써 설명드리면 다음과 같습니다:
특정 모달창의 태그 내부의 버튼을 클릭하기 를 테스트하는 코드는 다음과 같이 두가지 방법으로 작성할 수 있습니다:

  • then()을 사용하지 않고 작성하기
cy.get('.modal') // .modal 클래스를 가진 태그를 찾는다.
cy.get('button').click() // button 태그를 찾아 클릭한다.

위와 같이 작성해도 Cypress 테스트가 실행되는데엔 전혀 문제가 없습니다.
다만 button 태그를 찾을 때 modal 창에서 찾지 않고 웹 브라우저 전역에 걸쳐서 찾게 되어 모든 button 태그들을 찾을 수 있다는 문제점이 있습니다.

  • then()을 사용하여 작성하기
cy.get('.modal').then(($modal) => {
  const button = $modal.find('button')
  button.click();
});

// then()을 쓰지 않았을 때의 위험요소가 존재하지만
// 구조를 파악하기 더 용이합니다.
cy.get('.modal').then(($modal) => {
  cy.get('button').click();
});


✨ Cypress Commands


Commands란?

게임에서 사용되는 커맨더와 유사하다고 생각합니다.
Cypress의 반복되는 테스트 코드를 커스터마이징하여 테스트 코드 작성의 효율을 높일 수 있습니다.

Cypress 테스트 코드를 작성하다 보면 이런 고민에 빠지게 됩니다.
이 코드.. 분명 어디서 썼던 것 같은데?
Cypress 테스트 코드를 작성하면서도 이런 의문과 모듈화에 대한 고민을 하는 것은 당연합니다.
하지만 Cypress 테스트 코드를 Commands, 즉 커스터마이즈 함수로 만들고 사용하여 이 문제를 해결할 수 있습니다.

아래와 같이 결제하기 버튼을 찾아 클릭하는 Cypress 코드가 있다고 가정해보겠습니다:

cy.get('button').contains('결제하기').click();

이런 함수를 100여개의 상품 페이지를 테스트할 때마다 똑같이 작성해야한다면 동일한 코드가 이미 수십, 수백번 반복될 것입니다.
commands에서 커스텀 함수를 만들어 사용한다면 불필요한 코드를 줄일 수 있을 것입니다.


Commands 사용하기

우선 Commandssupport 폴더의 commands.ts 파일 내부에서 다음과 같이 선언됩니다.
결제하기 버튼을 클릭하는 코드를 커맨드로 작성하면 다음과 같습니다:

// cypress/support/commands.ts
Cypress.Commands.add('payment', (parameter: type) => {
  cy.get('button').contains('결제하기').click();
});

이후 테스트코드를 실행하기에 앞서 가장 먼저 import가 되는 파일인 support 폴더의 index.ts 파일 내부에서 다음과 같이 작성합니다.
커맨드 함수가 반환하는 값에 따라 타입을 지정해주면 됩니다.

/// <reference types="cypress" />

import './commands';

declare global {
  // eslint-disable-next-line @typescript-eslint/no-namespace
  namespace Cypress {
    interface Chainable {
		 payment(parameter: type): Chainable<returnType>;
		}    
	}
}

커맨드 함수를 만든 이후 사용 방법은 다음과 같습니다:
기존보다 훨씬 더 간결한 코드로 불필요한 반복 작업을 줄일 수 있으며, 함수를 더 효율적으로 관리할 수 있게됩니다.

it('데스크탑 결제 테스트', () => {
  // 기존: cy.get('button').contains('결제하기').click();
  cy.payment();
});

it('태블릿pc 결제 테스트', () => {
  // 기존: cy.get('button').contains('결제하기').click();
  cy.payment();
});

it('모바일 결제 테스트', () => {
  // 기존: cy.get('button').contains('결제하기').click();
  cy.payment();
})


✨ Restrictions


Cypress 테스트 코드를 작성하면서 와 이런 부분까지 테스트를 할 수 있다고? 라는 생각을 많이했습니다. 다만 아쉬운 부분도 분명히 있었습니다.

iframe

어플리케이션 내부에 또 다른 html 파일, 즉 iframe 코드를 통해 다른 origin을 지니는 페이지 혹은 비디오 파일은 Cypress에서 테스트를 직접할 수는 없습니다.

  • 결제 API 모달 창 (토스 페이먼츠 API 등)
  • imbeded video (유튜브, 비메오 영상 렌더링 목적의 iframe 태그 등)

물론, iframe 태그의 요소를 테스트하기 위한 방법도 분명히 존재하기는 하지만, 내가 호스팅하고 있는 웹브라우저 플랫폼의 보안 정책에 따라 적용이 될 수도 있고 안될 수도 있습니다. (불확실하다.)

이미 Cypress 공식 문서 상에서도 iframe을 테스트할 수 없다고 선언이 된 만큼, iframe 태그가 존재하는 페이지에 대해서는 아직까지는 사람이 직접 테스트를 하거나, 웹 브라우저 플랫폼의 보안정책을 해제해도 무방한 웹 서버를 호스팅하는 방법이 차선책일 것이라 판단됩니다.


CAPTCHA

크롬을 이용하다 보면, 로그인을 할 때 특정 이미지르 고르거나 이미지에 적힌 숫자를 적게 하는 등 유저가 유저임을 증명하는 프로그램을 본 적이 있을 것입니다.

하지만,, Cypress를 이용하는 것 자체가 이미 봇이기 때문에 CAPTCHA 프로그램을 만났을 때 우회할 수 없습니다.

국내에서 서비스 중인 많은 웹 애플리케이션의 경우, 카카오 소셜 로그인 기능을 많이 활용할텐데요,
해당 부분은 Cypress에게 직접 맡기기 보단 아래의 선택지를 적용하면 좋을 것 같습니다:

  • 로그인이 필요한 기능은 직접 로그인하기.
  • 토큰을 사용하는 경우 before() 훅 내부에서 window.localstorage.setItem() 코드 이용하기.

만약, 버전이 업데이트 되면서 제한사항이 많이 해결될 수도 있고, 제가 테스트했던 서버의 문제로 인해 해결할 수 없는 경우가 있을 것입니다.
이미 수많은 개발자들이 Cypress 제한사항에 대한 해결책을 제시하고 있으니, 더 많은 소스를 통해 다양한 방법으로 시도해보면 좋을 것 같습니다.



References


Cypress 공식 홈페이지 :: 이동하기



profile
누구나 이해할 수 있도록

0개의 댓글