🎉 berenickt 블로그에 온 걸 환영합니다. 🎉
Front
Cypress
Cypress 개요

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 설치

1
cd /your/project/path
2
3
# yarn으로 설치 시
4
yarn add cypress --dev
5
6
# npm으로 설치 시
7
npm install cypress --save-dev

1.3 열기

1
yarn cypress open
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 기본 문법

1
describe('Test Suite', () => {
2
it('Test Case 1', () => {
3
// 테스트 로직 ...
4
})
5
6
cy.get('input[name=username]').type('exampleUser')
7
8
cy.visit('/login')
9
})

Cypress는 기본적으로 describe와 it으로 테스트 케이스 정의

  • describe: 테스트 스위트 정의
  • it: 테스트 케이스 정의
  • cy.visit: 애플리케이션의 특정 URL을 연다
  • cy.get: DOM 요소를 선택하는데 사용됨. 선택한 요소에 대한 다양한 동작들을 수행할 수 있음

2.1 기본 문법: Query

1
cy.get('.list > li') // .list에서 <li>를 산출
2
cy.get('.dropdown-menu').click() // 찾아서 요소를 클릭
3
cy.get('input').should('be.disabled') // 찾아서 체크
  • Query: DOM 요소를 선택하는 데 사용되는 다양한 메서드들로, 특정 요소를 찾고 조작하기 위해 활용
  • e.g. 자주 사용하는 ‘get’ 메서드는 요소를 선택하는데 사용함
1
cy.get('.item').as('listItem')
2
3
// 다른 곳에서 참조된 별칭을 사용할 수 있음
4
cy.get('@listItem').click()
  • as 메서드: 선택한 요소에 별칭을 부여하는 데 사용됨. 이를 통해 특정 요소에 대한 참조를 나중에 재사용할 수 있음

그 외 Query 함수들

  1. closest : 특정 조건에 가장 가까운 부모 요소를 찾음
    • e.g. cy.get('.element').closest('.parent-container')
  2. contains : 특정 텍스트를 포함하는 요소를 찾음
  3. eq : 특정 인덱스에 있는 요소를 선택
    • e.g. cy.get('ul li').eq(2).should('have.text', 'Third item')
  4. find : 특정 자식 요소를 찾음
    • e.g. cy.get('.parent-element').find('.child-element')
  5. first, last : 선택된 요소 집합에서 첫 번째/마지막 요소를 선택
  6. parent, children : 특정 요소의 부모 / 자식 요소를 선택
  7. not : 주어진 선택자와 일치하지 않는 요소를 선택
    • e.g. cy.get('ul li').not('.special-item').should('have.length', 2)
  8. next, prev : 특정 요소의 다음 / 이전 형제 요소 선택

2.2 기본 문법: Assertion

1
// shoud & end 같이 사용
2
cy.get('.err').should('be.empty').and('be.hidden')
3
4
// and만 사용
5
cy.contains('Login').and('be.visible')
6
7
// should만 사용
8
cy.get('.error').should('be.empty')
9
cy.contains('Login').should('be.visible')
  • Assertion: 테스트 중에 특정 조건이 만족되는지 확인하기 위해 사용되는 메커니즘
    • and, should 메서드를 사용해서 다양한 조건을 확인할 수 있음. (e.g. 요소의 가시성, 텍스트 내용, 속성, 위치 등)

그 외 Assertion 함수들

  1. and: 여러 개의 Assertion을 묶어서 사용할 때 주로 활용. (e.g. 한 요소에 대한 여러 가지 조건을 동시에 확인하고 싶을 때 사용)
  2. should: 특정 조건이 충족되는지 확인하는데 사용되는데, 이를 여러 번 연결하여 사용할 수 있음

2.3 기본 문법: Actions

  • Actions: 사용자 인터랙션을 시뮬레이션하는 데 사용되는 메서드
  • 실제 사용자의 행동을 모방하여 E2E 테스트를 작성하는 데 매우 유용

그 외 Actions 함수들

  1. check : 체크박스나 라디오 버튼을 선택하는 데 사용
    • e.g. cy.get('#checkbox').check()
  2. clear : 텍스트 입력 필드를 비우는 데 사용
  3. click : 특정 요소를 클릭하는 데 사용
  4. trigger : DOM 이벤트를 강제로 발생시키는 데 사용
    • e.g. cy.get('#targetElement').trigger('mouseover')
  5. type : 텍스트 입력 필드에 값을 입력하는 데 사용

2.4 그 외 기타 함수들

  1. wait() : 다음 명령으로 넘어가기 전에 몇 밀리초 동안 기다리거나 별칭이 지정된 리소스가 해결될 때까지 기다립니다.

3. 예시

3.1 방문 페이지 테스트

1
const API_URL = 'http://localhost:3000'
2
3
describe('서비스의 주요 페이지들이 잘 열리는지 페이지를 확인', () => {
4
it('메인 페이지 방문', () => {
5
cy.visit(API_URL)
6
})
7
8
it('지도 페이지 방문', () => {
9
cy.visit(`${API_URL}/map`)
10
})
11
12
it('로그인 페이지 방문', () => {
13
cy.visit(`${API_URL}/users/signin`)
14
})
15
16
it('FAQ 페이지 방문', () => {
17
cy.visit(`${API_URL}/faqs`)
18
})
19
20
it('상세 페이지 방문', () => {
21
cy.visit(`${API_URL}/rooms/159`)
22
})
23
})

3.2 지역 필터링 테스트

1
const API_URL = 'http://localhost:3000'
2
3
describe('지역 필터 테스트를 진행한다.', () => {
4
beforeEach(() => {
5
cy.visit(API_URL)
6
cy.wait(500)
7
})
8
9
it('필터 열기 버튼을 확인한다.', () => {
10
cy.get('[data-cy="filter-open"]').should('have.attr', 'type', 'button')
11
})
12
13
it('필터 열기 버튼을 눌러서 지역 상세 필터 열기 버튼 유무를 확인한다.', () => {
14
cy.get('[data-cy="filter-open"]').click()
15
cy.wait(500)
16
cy.get('[data-cy="filter-location"]').contains('여행지')
17
})
18
19
it('지역 필터 열기 버튼을 클릭해서 지역 검색 필터를 확인한다.', () => {
20
cy.get('[data-cy="filter-open"]').click()
21
cy.wait(500)
22
cy.get('[data-cy="filter-location"]').click()
23
cy.wait(500)
24
cy.get('[data-cy="filter-wrapper"]').contains('지역으로 검색하기')
25
})
26
27
it('서울 지역 필터가 잘 작동하는지 확인하다', () => {
28
// ...
29
})
30
})

3.3 카테고리 필터링 테스트

1
const API_URL = 'http://localhost:3000'
2
3
describe('카테고리 필터 테스트를 진행한다.', () => {
4
beforeEach(() => {
5
cy.visit(API_URL)
6
cy.wait(500)
7
})
8
9
it('카테고리 전체 필터를 확인한다.', () => {
10
cy.get('[data-cy="category-filter-all"]').contains('전체')
11
})
12
13
it('"자연" 카테고리 필터를 확인한다.', () => {
14
cy.get('[data-cy="category-filter-자연"]').contains('자연')
15
16
it('"자연" 카테고리를 선택한다.', () => {
17
cy.get('[data-cy="category-filter-자연"]').click()
18
cy.wait(500)
19
it('선택한 "자연" 카테고리에 맞는 숙소가 보여지는지 확인한다.', () => {
20
cy.get('[data-cy="room-category"]').first().contains('자연')
21
})
22
})
23
})
24
25
it('선택한 "전망좋은" 카테고리에 맞는 숙소가 보여지는지 확인한다.', () => {
26
cy.get('[data-cy="category-filter-전망좋은"]').contains('전망좋은')
27
cy.get('[data-cy="category-filter-전망좋은"]').click()
28
cy.wait(500)
29
cy.get('[data-cy="room-category"]').first().contains('전망좋은')
30
})
31
})

3.4 메인 페이지 예시

1
describe('메인페이지', () => {
2
/*** (1)
3
* @see https://docs.cypress.io/api/commands/visit
4
* visit() : 원격 URL을 방문합니다.
5
*
6
* @see https://docs.cypress.io/api/commands/contains
7
* contains() : 텍스트가 포함된 DOM 요소를 가져옵니다
8
*/
9
it('각 항목들이 노출되어야 한다', () => {
10
cy.visit('http://localhost:3000') // localhost:3000으로 이동
11
cy.contains('중고장터') // '중고장터'가 존재하는지 확인
12
cy.contains('판매하기')
13
cy.contains('내 상점')
14
cy.contains('채팅')
15
cy.contains('찜한 상품')
16
cy.contains('최근본상품')
17
})
18
19
/*** (2)
20
* @see https://docs.cypress.io/api/commands/wait
21
* wait() : 다음 명령으로 넘어가기 전에 몇 밀리초 동안 기다리거나 별칭이 지정된 리소스가 해결될 때까지 기다립니다.
22
*
23
* @see https://docs.cypress.io/api/commands/click#__docusaurus_skipToContent_fallback
24
* click() : DOM 요소를 클릭합니다.
25
*
26
* 2초 대기하는 이유는 페이지가 로딩되는 시간을 고려하기 위함
27
*/
28
it('판매 페이지로 이동할 수 있어야 한다', () => {
29
cy.visit('http://localhost:3000')
30
cy.wait(2000) // 2초 대기하는 이유는 페이지가 로딩되는 시간을 고려하기 위함
31
cy.contains('판매하기').click() // 판매하기 클릭
32
cy.contains('상품정보') // 상품정보가 존재하는지 확인
33
})
34
35
/*** (3)
36
*
37
*/
38
it('내 상점 페이지로 이동할 수 있어야 한다', () => {
39
cy.visit('http://localhost:3000')
40
cy.wait(2000)
41
cy.contains('내 상점').click()
42
cy.contains('상점명 수정')
43
cy.contains('내 상점 관리')
44
cy.contains('소개글 수정')
45
})
46
47
/*** (4)
48
*
49
*/
50
it('채팅 페이지로 이동할 수 있어야 한다', () => {
51
cy.visit('http://localhost:3000')
52
cy.wait(2000)
53
cy.contains('채팅').click()
54
cy.contains('대화를 선택해주세요')
55
})
56
57
/*** (5)
58
* @see https://docs.cypress.io/api/commands/get
59
* get() : 선택기 또는 별칭으로 하나 이상의 DOM 요소를 가져옵니다.
60
*/
61
it('검색창 클릭시 최근 검색어가 나와야 한다', () => {
62
cy.visit('http://localhost:3000')
63
cy.wait(2000)
64
cy.get('input[placeholder="상품명, 상점명 입력"]').click() // 검색창 클릭
65
cy.contains('최근 검색어')
66
cy.contains('최근 검색어가 없습니다')
67
})
68
69
/*** (6)
70
* @see https://docs.cypress.io/api/commands/type
71
* type() : DOM 요소에 입력합니다.
72
*
73
* @see https://docs.cypress.io/api/commands/url
74
* next() : DOM 요소 집합 내에서 각 DOM 요소의 바로 다음 형제 요소를 가져옵니다.
75
*
76
* @see https://docs.cypress.io/api/commands/should#__docusaurus_skipToContent_fallback
77
* should() : 어설션(의견)을 만듭니다. 어설션은 통과하거나 시간이 초과될 때까지 자동으로 다시 시도됨
78
*/
79
it('검색어 입력시 자동 완성이 되어야 한다', () => {
80
cy.visit('http://localhost:3000')
81
cy.wait(2000)
82
cy.get('input[placeholder="상품명, 상점명 입력"]').type('가위') // '가위' 입력
83
cy.contains('상점 검색 >').next().contains('가위') // '가위'가 자동완성 되는지 확인
84
cy.contains('가위 - 0').click() // '가위 - 0' 클릭
85
// URL이 정상적으로 변경되는지 확인
86
cy.url().should('eq', `http://localhost:3000/search?query=${encodeURIComponent('가위 - 0')}`)
87
})
88
89
/*** (7)
90
* @see https://docs.cypress.io/api/commands/clear
91
* clear() : 입력 또는 텍스트 영역의 값을 지웁니다.
92
*
93
* @see https://docs.cypress.io/api/commands/parent
94
* parent() : DOM 요소의 부모를 가져옵니다.
95
*
96
* @sees https://docs.cypress.io/api/commands/children#__docusaurus_skipToContent_fallback
97
* children() : DOM 요소의 자식을 가져옵니다.
98
*/
99
it('상품 검색 이후 최근 검색어에 해당 검색어가 포함되어야 한다', () => {
100
cy.visit('http://localhost:3000')
101
cy.wait(2000)
102
cy.get('input[placeholder="상품명, 상점명 입력"]').type('가위') // '가위' 입력
103
cy.contains('가위 - 0').click()
104
cy.wait(2000)
105
cy.get('input[placeholder="상품명, 상점명 입력"]').clear() // 검색창 초기화
106
// 최근 검색어에 '가위 - 0'이 포함되는지 확인
107
cy.contains('최근 검색어').parent().next().children().contains('가위 - 0')
108
})
109
110
/*** (8)
111
*
112
*/
113
it('상점 검색 클릭시 상점 검색이 되어야 한다', () => {
114
cy.visit('http://localhost:3000')
115
cy.wait(2000)
116
cy.get('input[placeholder="상품명, 상점명 입력"]').type('가위') // '가위' 입력
117
cy.contains('상점 검색 >').click() // 상점 검색 클릭
118
// URL이 정상적으로 변경되는지 확인
119
cy.url().should('eq', `http://localhost:3000/search/shop?query=${encodeURIComponent('가위')}`)
120
})
121
})

3.5 상점 탭 페이지 예시

1
describe('상점 페이지', () => {
2
/*** (1)
3
*
4
*/
5
it('상품 탭 클릭시 상품 탭으로 이동해야 한다', () => {
6
cy.visit('http://localhost:3000/shops/mock-shop-id')
7
cy.get('a[data-cy="shops-products-tab"]').click() // 상품 탭 클릭
8
cy.url().should('eq', 'http://localhost:3000/shops/mock-shop-id/products')
9
})
10
11
/*** (2)
12
*
13
*/
14
it('상점 후기 탭 클릭시 상점 후기 탭으로 이동해야 한다', () => {
15
cy.visit('http://localhost:3000/shops/mock-shop-id')
16
cy.get('a[data-cy="shops-reviews-tab"]').click() // 상점 후기 탭 클릭
17
cy.url().should('eq', 'http://localhost:3000/shops/mock-shop-id/reviews')
18
})
19
20
/*** (3)
21
*
22
*/
23
it('찜 탭 클릭시 찜 탭으로 이동해야 한다', () => {
24
cy.visit('http://localhost:3000/shops/mock-shop-id')
25
cy.get('a[data-cy="shops-likes-tab"]').click() // 찜 탭 클릭
26
cy.url().should('eq', 'http://localhost:3000/shops/mock-shop-id/likes')
27
})
28
29
/*** (4)
30
*
31
*/
32
it('팔로잉 탭 클릭시 팔로잉 탭으로 이동해야 한다', () => {
33
cy.visit('http://localhost:3000/shops/mock-shop-id')
34
cy.get('a[data-cy="shops-following-tab"]').click() // 팔로잉 탭 클릭
35
cy.url().should('eq', 'http://localhost:3000/shops/mock-shop-id/following')
36
})
37
38
/*** (5)
39
*
40
*/
41
it('팔로워 탭 클릭시 팔로워 탭으로 이동해야 한다', () => {
42
cy.visit('http://localhost:3000/shops/mock-shop-id')
43
cy.get('a[data-cy="shops-follower-tab"]').click() // 팔로워 탭 클릭
44
cy.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 and
3
// loaded automatically before your test files.
4
//
5
// This is a great place to put global configuration and
6
// behavior that modifies Cypress.
7
//
8
// You can change the location of this file or turn off
9
// automatically serving support files with the
10
// 'supportFile' configuration option.
11
//
12
// You can read more here:
13
// https://on.cypress.io/configuration
14
// ***********************************************************
15
16
// Import commands.js using ES2015 syntax:
17
import './commands'
18
19
// Alternatively you can use CommonJS syntax:
20
// require('./commands')
21
22
/***
23
* @description Cypress에서 발생하는 uncaught exception을 무시
24
* @see https://docs.cypress.io/api/cypress-api/catalog-of-events#Uncaught-Exceptions
25
*/
26
Cypress.on('uncaught:exception', (err, runnable) => {
27
// returning false here prevents Cypress from
28
// failing the test
29
return false
30
})