1. E2E 테스트 도구 - Cypress
Cypress: 자동화된 E2E 테스트를 작성하고 실행할 수 있는 도구End To End Test: 실제 사용자의 동작을 흉내내어 시스템의 시작부터 끝까지 테스트하는 기법
1.1 테스트 기법 비교
Unit(개별) Test: 함수와 같은 작은 단위의 기능들의 동작 확인- e.g. getProducts 함수를 통해 상품 목록 데이터가 의도한대로 반환되는지 확인
Integration(통합) Test: 다양한 기능들의 동작 여부를 통합적으로 확인- e.g. ProductList 컴포넌트 내부에서 Product 컴포넌트/getProducts 함수가 의도한대로 동작하는지 확인
E2E Test: 사용자 시나리오에 맞추어 모든 기능들이 제대로 동작하는지 확인- e.g. ProductList 컴포넌트를 통해 상품 목록의 확인이 가능하고, 상품 클릭을 통해 해당 상품의 상세 페이지로 이동이 가능한지 확인
Unit → Intergration → E2E 순으로 복잡도와 난이도가 높아지지만, 더 현실적인 테스트 가능
1.2 설치
1cd /your/project/path23# yarn으로 설치 시4yarn add cypress --dev56# npm으로 설치 시7npm install cypress --save-dev
1.3 열기
1yarn cypress open
- 프로젝트 루트에서 Cypress 실행
- Launchpad에서 E2E Testing 클릭 → 크롬창에서 Cypress 실행
- cf. https://docs.cypress.io/guides/getting-started/opening-the-app
package.json
1"scripts": {2"dev": "next dev",3"build": "next build",4"start": "next start",5"dev:mock": "USE_MOCK_DATA=true next dev",6"build:mock": "USE_MOCK_DATA=true next build",7"start:mock": "USE_MOCK_DATA=true next start",8// Mock 데이터를 사용한다면, 각자 상황에 맞게 수정해서 사용9"cypress:ready": "npm run build:mock && npm run start:mock",10// 아래 문구 추가11"cypress:open": "cypress open",12},
cypress open로 시작하면, 폴더에 테스트 타입 선택화면 나온다.- E2E Testing → 크롬 브라우저 선택
2. e2e cypress 기본 문법
1describe('Test Suite', () => {2it('Test Case 1', () => {3// 테스트 로직 ...4})56cy.get('input[name=username]').type('exampleUser')78cy.visit('/login')9})
Cypress는 기본적으로 describe와 it으로 테스트 케이스 정의
describe: 테스트 스위트 정의it: 테스트 케이스 정의cy.visit: 애플리케이션의 특정 URL을 연다cy.get: DOM 요소를 선택하는데 사용됨. 선택한 요소에 대한 다양한 동작들을 수행할 수 있음- 선택기 또는 별칭으로 하나 이상의 DOM 요소를 가져옵니다.
- cf. https://docs.cypress.io/api/commands/get
2.1 기본 문법: Query
1cy.get('.list > li') // .list에서 <li>를 산출2cy.get('.dropdown-menu').click() // 찾아서 요소를 클릭3cy.get('input').should('be.disabled') // 찾아서 체크
Query: DOM 요소를 선택하는 데 사용되는 다양한 메서드들로, 특정 요소를 찾고 조작하기 위해 활용- e.g. 자주 사용하는 ‘get’ 메서드는 요소를 선택하는데 사용함
1cy.get('.item').as('listItem')23// 다른 곳에서 참조된 별칭을 사용할 수 있음4cy.get('@listItem').click()
as 메서드: 선택한 요소에 별칭을 부여하는 데 사용됨. 이를 통해 특정 요소에 대한 참조를 나중에 재사용할 수 있음
그 외 Query 함수들
closest: 특정 조건에 가장 가까운 부모 요소를 찾음- e.g.
cy.get('.element').closest('.parent-container')
- e.g.
contains: 특정 텍스트를 포함하는 요소를 찾음- e.g.
cy.contains('Submit').click() - cf. https://docs.cypress.io/api/commands/contains
- e.g.
eq: 특정 인덱스에 있는 요소를 선택- e.g.
cy.get('ul li').eq(2).should('have.text', 'Third item')
- e.g.
find: 특정 자식 요소를 찾음- e.g.
cy.get('.parent-element').find('.child-element')
- e.g.
first, last: 선택된 요소 집합에서 첫 번째/마지막 요소를 선택parent, children: 특정 요소의 부모 / 자식 요소를 선택not: 주어진 선택자와 일치하지 않는 요소를 선택- e.g.
cy.get('ul li').not('.special-item').should('have.length', 2)
- e.g.
next, prev: 특정 요소의 다음 / 이전 형제 요소 선택
2.2 기본 문법: Assertion
1// shoud & end 같이 사용2cy.get('.err').should('be.empty').and('be.hidden')34// and만 사용5cy.contains('Login').and('be.visible')67// should만 사용8cy.get('.error').should('be.empty')9cy.contains('Login').should('be.visible')
Assertion: 테스트 중에 특정 조건이 만족되는지 확인하기 위해 사용되는 메커니즘and, should메서드를 사용해서 다양한 조건을 확인할 수 있음. (e.g. 요소의 가시성, 텍스트 내용, 속성, 위치 등)
그 외 Assertion 함수들
and: 여러 개의 Assertion을 묶어서 사용할 때 주로 활용. (e.g. 한 요소에 대한 여러 가지 조건을 동시에 확인하고 싶을 때 사용)should: 특정 조건이 충족되는지 확인하는데 사용되는데, 이를 여러 번 연결하여 사용할 수 있음
2.3 기본 문법: Actions
Actions: 사용자 인터랙션을 시뮬레이션하는 데 사용되는 메서드- 실제 사용자의 행동을 모방하여 E2E 테스트를 작성하는 데 매우 유용
그 외 Actions 함수들
check: 체크박스나 라디오 버튼을 선택하는 데 사용- e.g.
cy.get('#checkbox').check()
- e.g.
clear: 텍스트 입력 필드를 비우는 데 사용- e.g.
cy.get('input[name=username]').clear() - cf. https://docs.cypress.io/api/commands/clear
- e.g.
click: 특정 요소를 클릭하는 데 사용- e.g.
cy.get('#submitButton').click() - cf. https://docs.cypress.io/api/commands/click#__docusaurus_skipToContent_fallback
- e.g.
trigger: DOM 이벤트를 강제로 발생시키는 데 사용- e.g.
cy.get('#targetElement').trigger('mouseover')
- e.g.
type: 텍스트 입력 필드에 값을 입력하는 데 사용- e.g.
cy.get(‘input[name=username]').type('hello') - cf. https://docs.cypress.io/api/commands/type
- e.g.
2.4 그 외 기타 함수들
wait(): 다음 명령으로 넘어가기 전에 몇 밀리초 동안 기다리거나 별칭이 지정된 리소스가 해결될 때까지 기다립니다.
3. 예시
3.1 방문 페이지 테스트
1const API_URL = 'http://localhost:3000'23describe('서비스의 주요 페이지들이 잘 열리는지 페이지를 확인', () => {4it('메인 페이지 방문', () => {5cy.visit(API_URL)6})78it('지도 페이지 방문', () => {9cy.visit(`${API_URL}/map`)10})1112it('로그인 페이지 방문', () => {13cy.visit(`${API_URL}/users/signin`)14})1516it('FAQ 페이지 방문', () => {17cy.visit(`${API_URL}/faqs`)18})1920it('상세 페이지 방문', () => {21cy.visit(`${API_URL}/rooms/159`)22})23})
3.2 지역 필터링 테스트
1const API_URL = 'http://localhost:3000'23describe('지역 필터 테스트를 진행한다.', () => {4beforeEach(() => {5cy.visit(API_URL)6cy.wait(500)7})89it('필터 열기 버튼을 확인한다.', () => {10cy.get('[data-cy="filter-open"]').should('have.attr', 'type', 'button')11})1213it('필터 열기 버튼을 눌러서 지역 상세 필터 열기 버튼 유무를 확인한다.', () => {14cy.get('[data-cy="filter-open"]').click()15cy.wait(500)16cy.get('[data-cy="filter-location"]').contains('여행지')17})1819it('지역 필터 열기 버튼을 클릭해서 지역 검색 필터를 확인한다.', () => {20cy.get('[data-cy="filter-open"]').click()21cy.wait(500)22cy.get('[data-cy="filter-location"]').click()23cy.wait(500)24cy.get('[data-cy="filter-wrapper"]').contains('지역으로 검색하기')25})2627it('서울 지역 필터가 잘 작동하는지 확인하다', () => {28// ...29})30})
3.3 카테고리 필터링 테스트
1const API_URL = 'http://localhost:3000'23describe('카테고리 필터 테스트를 진행한다.', () => {4beforeEach(() => {5cy.visit(API_URL)6cy.wait(500)7})89it('카테고리 전체 필터를 확인한다.', () => {10cy.get('[data-cy="category-filter-all"]').contains('전체')11})1213it('"자연" 카테고리 필터를 확인한다.', () => {14cy.get('[data-cy="category-filter-자연"]').contains('자연')1516it('"자연" 카테고리를 선택한다.', () => {17cy.get('[data-cy="category-filter-자연"]').click()18cy.wait(500)19it('선택한 "자연" 카테고리에 맞는 숙소가 보여지는지 확인한다.', () => {20cy.get('[data-cy="room-category"]').first().contains('자연')21})22})23})2425it('선택한 "전망좋은" 카테고리에 맞는 숙소가 보여지는지 확인한다.', () => {26cy.get('[data-cy="category-filter-전망좋은"]').contains('전망좋은')27cy.get('[data-cy="category-filter-전망좋은"]').click()28cy.wait(500)29cy.get('[data-cy="room-category"]').first().contains('전망좋은')30})31})
3.4 메인 페이지 예시
1describe('메인페이지', () => {2/*** (1)3* @see https://docs.cypress.io/api/commands/visit4* visit() : 원격 URL을 방문합니다.5*6* @see https://docs.cypress.io/api/commands/contains7* contains() : 텍스트가 포함된 DOM 요소를 가져옵니다8*/9it('각 항목들이 노출되어야 한다', () => {10cy.visit('http://localhost:3000') // localhost:3000으로 이동11cy.contains('중고장터') // '중고장터'가 존재하는지 확인12cy.contains('판매하기')13cy.contains('내 상점')14cy.contains('채팅')15cy.contains('찜한 상품')16cy.contains('최근본상품')17})1819/*** (2)20* @see https://docs.cypress.io/api/commands/wait21* wait() : 다음 명령으로 넘어가기 전에 몇 밀리초 동안 기다리거나 별칭이 지정된 리소스가 해결될 때까지 기다립니다.22*23* @see https://docs.cypress.io/api/commands/click#__docusaurus_skipToContent_fallback24* click() : DOM 요소를 클릭합니다.25*26* 2초 대기하는 이유는 페이지가 로딩되는 시간을 고려하기 위함27*/28it('판매 페이지로 이동할 수 있어야 한다', () => {29cy.visit('http://localhost:3000')30cy.wait(2000) // 2초 대기하는 이유는 페이지가 로딩되는 시간을 고려하기 위함31cy.contains('판매하기').click() // 판매하기 클릭32cy.contains('상품정보') // 상품정보가 존재하는지 확인33})3435/*** (3)36*37*/38it('내 상점 페이지로 이동할 수 있어야 한다', () => {39cy.visit('http://localhost:3000')40cy.wait(2000)41cy.contains('내 상점').click()42cy.contains('상점명 수정')43cy.contains('내 상점 관리')44cy.contains('소개글 수정')45})4647/*** (4)48*49*/50it('채팅 페이지로 이동할 수 있어야 한다', () => {51cy.visit('http://localhost:3000')52cy.wait(2000)53cy.contains('채팅').click()54cy.contains('대화를 선택해주세요')55})5657/*** (5)58* @see https://docs.cypress.io/api/commands/get59* get() : 선택기 또는 별칭으로 하나 이상의 DOM 요소를 가져옵니다.60*/61it('검색창 클릭시 최근 검색어가 나와야 한다', () => {62cy.visit('http://localhost:3000')63cy.wait(2000)64cy.get('input[placeholder="상품명, 상점명 입력"]').click() // 검색창 클릭65cy.contains('최근 검색어')66cy.contains('최근 검색어가 없습니다')67})6869/*** (6)70* @see https://docs.cypress.io/api/commands/type71* type() : DOM 요소에 입력합니다.72*73* @see https://docs.cypress.io/api/commands/url74* next() : DOM 요소 집합 내에서 각 DOM 요소의 바로 다음 형제 요소를 가져옵니다.75*76* @see https://docs.cypress.io/api/commands/should#__docusaurus_skipToContent_fallback77* should() : 어설션(의견)을 만듭니다. 어설션은 통과하거나 시간이 초과될 때까지 자동으로 다시 시도됨78*/79it('검색어 입력시 자동 완성이 되어야 한다', () => {80cy.visit('http://localhost:3000')81cy.wait(2000)82cy.get('input[placeholder="상품명, 상점명 입력"]').type('가위') // '가위' 입력83cy.contains('상점 검색 >').next().contains('가위') // '가위'가 자동완성 되는지 확인84cy.contains('가위 - 0').click() // '가위 - 0' 클릭85// URL이 정상적으로 변경되는지 확인86cy.url().should('eq', `http://localhost:3000/search?query=${encodeURIComponent('가위 - 0')}`)87})8889/*** (7)90* @see https://docs.cypress.io/api/commands/clear91* clear() : 입력 또는 텍스트 영역의 값을 지웁니다.92*93* @see https://docs.cypress.io/api/commands/parent94* parent() : DOM 요소의 부모를 가져옵니다.95*96* @sees https://docs.cypress.io/api/commands/children#__docusaurus_skipToContent_fallback97* children() : DOM 요소의 자식을 가져옵니다.98*/99it('상품 검색 이후 최근 검색어에 해당 검색어가 포함되어야 한다', () => {100cy.visit('http://localhost:3000')101cy.wait(2000)102cy.get('input[placeholder="상품명, 상점명 입력"]').type('가위') // '가위' 입력103cy.contains('가위 - 0').click()104cy.wait(2000)105cy.get('input[placeholder="상품명, 상점명 입력"]').clear() // 검색창 초기화106// 최근 검색어에 '가위 - 0'이 포함되는지 확인107cy.contains('최근 검색어').parent().next().children().contains('가위 - 0')108})109110/*** (8)111*112*/113it('상점 검색 클릭시 상점 검색이 되어야 한다', () => {114cy.visit('http://localhost:3000')115cy.wait(2000)116cy.get('input[placeholder="상품명, 상점명 입력"]').type('가위') // '가위' 입력117cy.contains('상점 검색 >').click() // 상점 검색 클릭118// URL이 정상적으로 변경되는지 확인119cy.url().should('eq', `http://localhost:3000/search/shop?query=${encodeURIComponent('가위')}`)120})121})
3.5 상점 탭 페이지 예시
1describe('상점 페이지', () => {2/*** (1)3*4*/5it('상품 탭 클릭시 상품 탭으로 이동해야 한다', () => {6cy.visit('http://localhost:3000/shops/mock-shop-id')7cy.get('a[data-cy="shops-products-tab"]').click() // 상품 탭 클릭8cy.url().should('eq', 'http://localhost:3000/shops/mock-shop-id/products')9})1011/*** (2)12*13*/14it('상점 후기 탭 클릭시 상점 후기 탭으로 이동해야 한다', () => {15cy.visit('http://localhost:3000/shops/mock-shop-id')16cy.get('a[data-cy="shops-reviews-tab"]').click() // 상점 후기 탭 클릭17cy.url().should('eq', 'http://localhost:3000/shops/mock-shop-id/reviews')18})1920/*** (3)21*22*/23it('찜 탭 클릭시 찜 탭으로 이동해야 한다', () => {24cy.visit('http://localhost:3000/shops/mock-shop-id')25cy.get('a[data-cy="shops-likes-tab"]').click() // 찜 탭 클릭26cy.url().should('eq', 'http://localhost:3000/shops/mock-shop-id/likes')27})2829/*** (4)30*31*/32it('팔로잉 탭 클릭시 팔로잉 탭으로 이동해야 한다', () => {33cy.visit('http://localhost:3000/shops/mock-shop-id')34cy.get('a[data-cy="shops-following-tab"]').click() // 팔로잉 탭 클릭35cy.url().should('eq', 'http://localhost:3000/shops/mock-shop-id/following')36})3738/*** (5)39*40*/41it('팔로워 탭 클릭시 팔로워 탭으로 이동해야 한다', () => {42cy.visit('http://localhost:3000/shops/mock-shop-id')43cy.get('a[data-cy="shops-follower-tab"]').click() // 팔로워 탭 클릭44cy.url().should('eq', 'http://localhost:3000/shops/mock-shop-id/follower')45})46})
4. e2e.ts
1// ***********************************************************2// This example support/e2e.ts is processed and3// loaded automatically before your test files.4//5// This is a great place to put global configuration and6// behavior that modifies Cypress.7//8// You can change the location of this file or turn off9// automatically serving support files with the10// 'supportFile' configuration option.11//12// You can read more here:13// https://on.cypress.io/configuration14// ***********************************************************1516// Import commands.js using ES2015 syntax:17import './commands'1819// Alternatively you can use CommonJS syntax:20// require('./commands')2122/***23* @description Cypress에서 발생하는 uncaught exception을 무시24* @see https://docs.cypress.io/api/cypress-api/catalog-of-events#Uncaught-Exceptions25*/26Cypress.on('uncaught:exception', (err, runnable) => {27// returning false here prevents Cypress from28// failing the test29return false30})