프론트엔드 테스트의 종류

프론트엔드 개발 시 할 수 있는 테스트는 크게 4개가 있습니다.
첫 째로 구문 오류나 나쁜 코드 스타일 등을 검증하는 정적 테스트(ESLint, TypeScript),
작은 단위만 떼어내어 분리된 환경에서 알고리즘이 제대로 동작하는지 확인하는 유닛 테스트(Jest),
어플리케이션의 여러 부분을 통합해 올바른 상호작용을 하는지 테스트하는 통합 테스트(Jest, RTL, Enzyme),
API 서버, DB 등 모두 연결된 실제 사용자의 조건에서 전체 서비스(시스템)를 검증하는 E2E 테스트(Cypress, Selenium)가 있습니다.

Cypress 소개

오늘 글에서 다룰 테스트는 종단간 테스트라고도 불리는 End-to-End(E2E) 테스트입니다. 프론트엔드 E2E 테스트시 사용할 수 있는 Cypress는 오픈소스 모던 웹 앱 테스팅 툴이고, 리액트/뷰 등 상관없이 웹앱 브라우저에서 돌아가는 거면 뭐든 테스트가 가능합니다. 개발자가 테스트 코드를 작성해두면 Cypress가 자동으로 테스트를 진행합니다.

Cypress 기능

  • Cypress는 모든 테스트가 행해질때마다 DOM 스냅샷을 찍어, 시간 여행을 하듯 해당 단계에서 일어난 일을 다시 볼 수 있습니다.
  • 네트워크 트래픽을 조절해 테스트할 수 있습니다.
  • 크로스 브라우저 테스트가 가능합니다.

명령어 사용하기

Cypress가 제공하는 명령어들은 다양합니다. andshould로 DOM 요소의 클래스나 스타일을 확인(assertion)할 수도 있고, 브라우저 쿠키를 지우거나(clearCookie) 마우스 우클릭(rightClick)을 할 수도 있습니다.
예시 코드입니다.

cy.visit(firstUrl)
cy.get(nameTextInput).type('hailey')
cy.location(redirectedUrl)

위 코드는 firstUrl에 방문해서, nameTextInput에 'hailey'라는 값을 입력하고, redirect된 location이 redirectedUrl이 맞나 확인합니다.

cy.task 커맨드로 자바스크립트를 시스템 레벨에서 실행할 수도 있습니다.

const userSeed = 유저정보;

context('User setup', ()=> {
	beforeEach(()=> {
		cy.task('clear:db')
		cy.task('seed:db', userSeed.data)
	})

	it('login user', () => {
		cy.visit('localhost:8080/login')
		cy.login('hailey@cypress.io', '1234')
		cy.location('pathname').should('eq', '/board')
	})
})

위 코드는 task 커맨드를 사용해 모든 login user 테스트 전에 clear:db, seed:db를 수행합니다.

커스텀 커맨드

Cypress has direct programmatic access to my app.

Cypress 커스텀 커맨드를 사용하면 프로그램적으로 앱에 접근할 수 있습니다. UI를 통하지 않고, 프로그램 자체에서 액션을 실행하게 하는 겁니다. 예를 들자면 로그인 버튼을 눌러 로그인 함수가 실행되게 하는게 아니라, 프로그램 내 login() 함수를 직접 실행시키는 거죠! 예시 코드는 이렇습니다.

Cypress.Commands.add('login', (email, password) => {
	return cy.window().then(win => {
		return win.app.$store.dispatch('login', {
			email: 'hailey@cypress.io',
			password: '1234'
		})
	})`
});

웹앱을 테스트할때, 로그인 이후의 페이지를 테스트하려면 테스트할때마다 로그인을 계속 거쳐야합니다. 위 코드처럼 로그인을 하는 커스텀 커맨드를 만들면 로그인처럼 반복되는 액션을 UI를 거치지 않고 더 빠르고, 덜 번거롭게 원하는 페이지를 테스트할 수 있습니다.

저는 테스트 코드를 좀 더 쉽게 작성하고 관리하기 위해 POM 디자인 패턴을 사용했습니다.

POM

POM이란 Page Object Model의 약자로, 테스트 자동화를 위해 사용하면 좋은 디자인 패턴입니다. 이 패턴에서 Page 객체는 객체 지향형 클래스고, 테스트하려는 페이지 하나가 한 Page 객체가 됩니다. 또 POM 클래스를 상속해 테스트 케이스의 길이를 줄일 수 있습니다. 그렇기에 POM은 여러 페이지가 있는 앱에서 사용하면 제일 좋습니다.

POM에서는 테스트와 DOM 요소를 찾는 Element Locator를 분리합니다. 한 페이지의 UI가 바뀐다고 해서 그 부분을 사용하는 모든 테스트를 바꿀 필요 없이, 페이지 객체 안의 Element Locator만 바꾸면 됩니다.

또 중요한 점은 페이지 객체 저장소(Page Object Repository)가 테스트로부터 독립적이라는 겁니다. 페이지 객체들을 담아두는 저장소를 분리함으로써 사용자가 원하는 툴, 프레임워크와 함께 사용할 수 있습니다.

Page 객체를 사용한 테스트 예시

Best Practice: Use data-* attributes to provide context to your selectors and isolate them from CSS or JS changes.

Cypress에서 제공하는 Best Practice 중 하나가 테스트할 HTML 요소를 지정할 때 data-cy 속성을 사용하는 겁니다. 동적 ID나 클래스, CSS 속성처럼 자주 변하는 값 말고 테스트할 HTML 태그에 data-cy 값을 넣어두는 거죠. 아래 예시를 보면 됩니다. (가독성을 위해 data-cy 속성을 제외하곤 생략합니다.)

<article data-cy='post'>
	<h1 data-cy='title'>페이지 객체를 만드는 방법<h1>
	<article>
		<p>내용</p>
	</article>
</article>
<button data-cy='write-new'>새 글 쓰기</button>

이 페이지는 렌더링 후 결과물이 위와 같이 만들어집니다. 글 하나를 상세조회할 수 있는 페이지고, 아래에는 '새 글 쓰기' 버튼이 있다고 합시다. 그러면 아래와 같이 PostPage 클래스를 만들 수 있습니다.

// PostPage.js
class PostPage {
  constructor() {}

  // 글의 제목 요소를 가져온다.
  getPostTitle() {
	return cy.get('[data-cy="title"]')
  }

  // 새로 글쓰기 버튼 요소를 가져온다.
  getWriteNewButton() {
	return cy.get('[data-cy="write-new"]')
  }
}
export const postPage = new PostPage();

PostPage 클래스로 만든 인스턴스 postPage를 cypress 테스트 코드인 post.cy.js에서 사용합니다.

// post.cy.js
describe('게시글 상세조회 테스트', () => {
	context('게시글 상세조회', () => {
		it('새 글 쓰기 버튼을 클릭하면 글쓰기 페이지로 이동한다.', () => {
			postPage.getWriteNewButton().click();
			cy.location('pathname').should('eq', '/new')
		})
	})
})

이렇게 글쓰기 버튼을 클릭하고, cy.location()으로 이동한 페이지가 글쓰기 페이지의 pathname과 동일한지 테스트할 수 있습니다.

짜잔~ 이렇게 POM 패턴으로 코드를 구성하면 이후에 로그인 버튼 마크업이 바뀌어도 모든 테스트코드에서 수정할 필요 없이 PostPage.js에서만 수정하면 됩니다.

POM 패턴에 대한 자세한 설명은 이 글을 참고하세요.

Cypress 테스트 구현 시 주의할 점

화살표함수와 this

테스트 코드에서 반복적으로 사용되는 데이터를 변수로 지정할 때 테스트 케이스 it(this)의 프로퍼티(this.data)로 넣을 경우 화살표 함수를 사용하면 this를 갖지 않는 화살표 함수의 특성상 An outer value of 'this' is shadowed by this container.라는 타입스크립트 에러가 뜰 수 있습니다.

이러한 에러 메시지를 만날 경우 화살표 함수를 function 함수로 바꿔주면 됩니다.

쿠키 초기화

Cypress는 각 테스트마다 브라우저의 상태를 초기화합니다. 모든 it()마다 쿠키가 깨끗히 지워집니다. 테스트중인 앱이 인증 토큰을 저장하는데 쿠키를 사용한다면 로그아웃이 되어버립니다.

이럴 땐 맨 처음 로그인을 하면서 SESSIONID 또는 기억해야할 데이터 값을 전역 변수에 할당해두고, beforeEach로 쿠키에 해당 데이터를 넣어주도록 하면 됩니다.

(추가) 다른 e2e 테스트 툴과 비교하기

사실 처음 e2e 테스트 프레임워크를 선택했을 때 깊게 찾아보지 않고 Cypress를 선택했었는데요. Cypress 말고도 Selenium WebDriver, Playwright 등 10개가 넘는 툴들이 있다는 걸 알게 되었습니다. 늦게나마 다른 툴들과 비교를 해본다면

Cypress의 장점

  1. 설치와 세팅이 간단한지: Selenium은 드라이버를 설치해야하고 브라우저별 설정을 관리해줘야하는 반면, Cypress는 세팅이 쉽고 설치가 간단합니다.
  2. 크로스 브라우징: Puppeteer 등의 툴은 브라우저의 제한이 있는 반면 Cypress는 Chrome, Firefox, Edge 등 다양한 브라우저를 지원합니다.
  3. watch mode: Cypress는 테스트 실행 이후 코드가 변경되면 자동으로 다시 수행되게 할 수 있습니다.
  4. 테스트가 동작하는 걸 눈으로 바로 볼 수 있는지: Cypress의 경우 GUI로 테스트가 실행되는 걸 눈으로 바로 볼 수 있습니다. 테스트 장면을 녹화해서 다른 사람한테 보여줘야할 때 유용했어요.

Cypress의 단점

  1. 유료 병렬 테스트: Cypress로 병렬 테스트를 하려면 유료 플랜에 가입해야합니다. Playwright의 경우, 무료로 가능하다고 합니다. 또 Cypress로 병렬 테스트를 하면 가상머신을 이용해 테스트하는데 가능은 하지만 리소스가 많이 필요해 추천하지 않는다고 하네요.
  2. IDE 익스텐션이 없습니다. 저는 인텔리제이를 주로 사용하지만, Playwright의 경우 VSCode 익스텐션이 있어서 spec 안에서 원하는 테스트만 돌릴 수도 있다고 합니다.

프레임워크가 아주 다양하니 잘 비교하셔서 선택하시길 바랍니다!

끝 🔎

References