๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ‘จ‍๐Ÿ’ป web.dev/fe

ํ”„๋ก ํŠธ์—”๋“œ E2E ํ…Œ์ŠคํŒ… (with Cypress)

by HandHand 2023. 3. 19.

 

๐Ÿ’ก ์ด๋ฒˆ ํฌ์ŠคํŠธ๋Š” ์‚ฌ๋‚ด ์œ„ํด๋ฆฌ์—์„œ ๋ฐœํ‘œํ•œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ๋“ค์–ด๊ฐ€๋ฉฐ

 

์ž‘๋…„ 12์›”๋ถ€ํ„ฐ ์•ฝ 3๊ฐœ์›”๊ฐ„ ํ•ญ๊ณต ์›น ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ–ˆ๋Š”๋ฐ, ์ƒ๊ฐ๋ณด๋‹ค ๋‹ค์–‘ํ•œ ๋™์„ ์ด ์žˆ์–ด์„œ ๊ณ ์ƒ์„ ํ–ˆ๋˜ ๊ธฐ์–ต์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋”๊ตฐ๋‹ค๋‚˜ ์ œ๊ฐ€ ์ง„ํ–‰ํ–ˆ๋˜ ์—…๋ฌด(์›น ํ—ˆ๋ธŒ ์˜คํ”ˆ, TF v12 ์ „ํ™˜)๊ฐ€ ํ•ญ๊ณต์˜ ์ „์ฒด์ ์ธ ๋™์„ ์— ์ ์šฉ์ด ํ•„์š”ํ–ˆ๋˜ ๊ฒƒ๋“ค์ด์–ด์„œ ๋”์šฑ ๊ทธ๋žฌ๋˜ ๊ฒƒ ๊ฐ™์€๋ฐ์š”.

์•„์ง E2E ํ…Œ์ŠคํŠธ ์ž‘์„ฑ๊ฒฝํ—˜์ด ์—†์–ด์„œ ์ด๋ฒˆ ๊ธฐํšŒ์— ๊ณต๋ถ€๋„ ํ•ด๋ณผ ๊ฒธ ๊ตญ์ œ์„  ์˜ˆ์•ฝ๋™์„ ์˜ E2E ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด๋ดค์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ E2E ํ…Œ์ŠคํŠธ๋ž€?

์šฐ์„  ๊ฐ„๋‹จํ•˜๊ฒŒ E2E ํ…Œ์ŠคํŠธ๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

ํ”ํžˆ E2E (End to End) ํ…Œ์ŠคํŠธ๋Š” ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์—์„œ ์œ ์ €๊ฐ€ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฐ€์ •ํ•˜์—

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ „์ฒด์ ์ธ ๋™์ž‘์„ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ์„ ๋งํ•ฉ๋‹ˆ๋‹ค.

 

์œ ๋‹› ํ…Œ์ŠคํŠธ์™€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์—์„œ๋Š” ๊ฐ ๋ชจ๋“ˆ์˜ ๋ฌด๊ฒฐ์„ฑ์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด,

E2E ์—์„œ๋Š” ์ด๋Ÿฌํ•œ ๋ชจ๋“ˆ๋“ค์ด ์ž˜ ์„ž์—ฌ์„œ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์„ ๋ฌธ์ œ์—†์ด ์ œ๊ณตํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ถœ์ฒ˜ : https://www.headspin.io/blog/the-testing-pyramid-simplified-for-one-and-all

 

๋Œ€์‹  ๊ทธ๋งŒํผ ๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๊ฒ€์ฆํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•๋“ค์— ๋น„ํ•ด ํ…Œ์ŠคํŠธ ๋น„์šฉ์ด ๋น„์‹ผ ๊ฒƒ๋„ ์‚ฌ์‹ค์ž…๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ํ•ญ๊ณต์›น๊ณผ ๊ฐ™์ด ๋‹ค์–‘ํ•œ ๋™์„ ์„ ๊ฒ€์ฆํ•ด์•ผํ•˜๋Š” ๊ฒฝ์šฐ E2E ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•œ ๊ธฐ๋Šฅ์  ํšŒ๊ท€๋กœ

์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ ํ˜น์€ ๋ฒ„๊ทธ ์ˆ˜์ •์‹œ์— ์•ˆ์ •๊ฐ์„ ๊ฐ€์ ธ๋‹ค ์ค„ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ํ•ญ๊ณต์›น ๋™์„  ์ •๋ฆฌ

ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์ „์— ๋จผ์ € ํ˜„์žฌ ํ•ญ๊ณต์›น ๋™์„ ์„ ํŒŒ์•…ํ•ด๋ด…์‹œ๋‹ค.

์šฐ์„  ๊ฐ€์žฅ ๊ธฐ๋ณธ์ด ๋˜๋Š” ์˜ˆ์•ฝ๋™์„ ์„ ํฌ๊ฒŒ ๋‚˜๋ˆ„๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

๊ตญ๋‚ด์„ 

  • ์›น, ์•ฑ, ๋ฉ”ํƒ€(b2c - skyscanner, naver)

๊ตญ์ œ์„ 

  • ์›น, ์•ฑ, ๋ฉ”ํƒ€(b2c - skyscanner)

๊ทธ๋ฆฌ๊ณ  ๊ฐ๊ฐ์˜ ๋™์„ ์€ ๋‹ค์Œ ํ๋ฆ„์œผ๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค.

 

ํ—ˆ๋ธŒ → ํ•ญ๊ณตํŽธ ๊ฒ€์ƒ‰ ๋ชฉ๋ก → ํ•ญ๊ณตํŽธ ์ƒ์„ธ → ์˜ˆ์•ฝํ•˜๊ธฐ → ๊ฒฐ์ œํ•˜๊ธฐ → ๋ฐœ๊ถŒ์™„๋ฃŒ → (๋‚ด ์˜ˆ์•ฝ์ƒ์„ธ || ํ—ˆ๋ธŒ)

 

์ด์™ธ์—๋„ ์„ธ๋ถ€์ ์œผ๋กœ ์—ฌํ–‰์ž ๋ณดํ—˜, PLCC ๋“ฑ๋“ฑ ๋‹ค์–‘ํ•œ ๋™์„ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋™์„ ๋“ค์€ ๊ตญ๋‚ด์„ /๊ตญ์ œ์„  ๋“ฑ์— ๊ณตํ†ต์œผ๋กœ ๋…น์•„์ ธ ์žˆ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ข€ ๋” ์ง๊ด€์ ์ธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ์ด๋Ÿฌํ•œ ๋™์„ ๋“ค์€

๋ฉ”์ธ ๋™์„ ๊ณผ ๋™์ผํ•œ ์œ„๊ณ„๋กœ ๋ณด๊ณ  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ์ง„ํ–‰ํ•ด๋ดค์Šต๋‹ˆ๋‹ค.

 

integration
 ใ„ด intl
 ใ„ด domestic
 ใ„ด b2c
        ใ„ด naver
        ใ„ด skyscanner
 ใ„ด plcc
 ใ„ด ... ๊ทธ์™ธ cross-sub-domain ๊ธฐ๋Šฅ๋“ค

 

๊ตญ๋‚ด์„ ์€ ์ด์ „์— eeyore์ด ์ž‘์—…ํ•ด์ฃผ์‹  ๊ฒƒ์ด ์žˆ์–ด์„œ ์ด๋ฒˆ์—๋Š” ๊ตญ์ œ์„  ์˜ˆ์•ฝ ๋™์„ ์˜ e2e ํ…Œ์ŠคํŠธ๋ฅผ ๊ตฌ์„ฑํ•ด๋ณด๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ Cypress ์˜ ํŠน์ง• ์‚ดํŽด๋ณด๊ธฐ

Cypress ์˜ ๋ช‡๊ฐ€์ง€ ๋Œ€ํ‘œ์ ์ธ ํŠน์ง•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ด…์‹œ๋‹ค.

1๏ธโƒฃ functional test runner

Cypress ๋Š” functional test runner ์ž…๋‹ˆ๋‹ค.

์•ฑ์ด ์‚ฌ์šฉ์ž ๊ด€์ ์—์„œ ์˜๋„ํ•œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

 

2๏ธโƒฃ ๋น„๋™๊ธฐ์ ์ธ element ํƒ์ƒ‰๋ฐฉ์‹

์ƒํ™ฉ์— ๋”ฐ๋ผ element ๊ฐ€ ๋ฐ”๋กœ ๋กœ๋“œ๋˜์ง€ ๋ชปํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋Š”๋ฐ,

cypress ์—์„œ๋Š” element query ๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ํ•˜๊ธฐ์— ๋‚ด๋ถ€์ ์œผ๋กœ timeout ์ด ๋˜๊ธฐ ์ „๊นŒ์ง€ ์žฌ์š”์ฒญ์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.

 

3๏ธโƒฃ retryability

๋ชจ๋˜ ์›น์•ฑ์€ ๋น„๋™๊ธฐ ๋™์ž‘์ด ๋งŽ์€๋ฐ, cypress ์˜ retryability ๋•๋ถ„์— ๋ณ„๋„์˜ ๋™๊ธฐ์ฒ˜๋ฆฌ์—†์ด ์ง๊ด€์ ์ธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ฐพ๋Š” query ๋ฅผ ํฌํ•จํ•˜์—ฌ ํŠน์ • ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜๋Š” action (ex. click) ๋“ค๋„

์ผ์ • timeout ๊นŒ์ง€ ์š”์ฒญ์„ ์žฌ๋ฐ˜๋ณตํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿฐ ์ฒ˜๋ฆฌ์— ์ผ์ผํžˆ ์‹ ๊ฒฝ์“ฐ์ง€ ์•Š๊ณ  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ์— ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

(๋ฌผ๋ก  ๊ทธ๋ ‡๊ธฐ์— ์ž˜๋ชป ์‚ฌ์šฉํ•˜๋ฉด ๋””๋ฒ„๊น…์— ์• ๋ฅผ ๋จน์„์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.)

 

4๏ธโƒฃ Promise ๊ธฐ๋ฐ˜์˜ ์ฒด์ด๋‹ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘

์ฝ๊ธฐ ์‰ฌ์šด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ๋™๊ธฐ์ ์ธ ๋กœ์ง์„ ์ž‘์„ฑํ•  ๋•Œ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

 

5๏ธโƒฃ record & screenshot

cypress ์—์„œ๋Š” ํ…Œ์ŠคํŠธ ์ดํ›„ ๊ฒฐ๊ณผ๋ฌผ์„ video, screenshot ํ˜•ํƒœ๋กœ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์ค๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๊ฒฐ๊ณผ๋ฌผ์€ headless ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ ์‹คํŒจ ์›์ธ์„ ์ฐพ๋Š”๋ฐ ๋„์›€์ด ๋˜๋ฉฐ

ํ˜„์žฌ ํ•ญ๊ณต์›น์—์„œ๋Š” github actions ์˜ artifacts ๋ฅผ ํ†ตํ•ด์„œ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฌผ์„ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์–ด๋–ป๊ฒŒ ์ž‘์„ฑํ• ๊นŒ

1๏ธโƒฃ ๊ธฐํš์„œ ๊ธฐ๋ฐ˜์˜ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ

E2E ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ๋ฐ˜๋ณต๋˜๋Š” ์ฃผ์š” ๋™์„ ์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๊ณ ,

์ด๋Š” ๊ณง ํ•ด๋‹น ํ”„๋กœ์ ํŠธ๋ฅผ ์„ค๋ช…ํ•˜๋Š” ํ•˜๋‚˜์˜ ๋ฌธ์„œ ์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ ‡๊ธฐ์— ํ•ด๋‹น ์„œ๋น„์Šค์˜ ๊ธฐํš์„œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํ”Œ๋กœ์šฐ๋ฅผ ์ •๋ฆฌํ•˜๋Š”๊ฒŒ ๊ฐ€์žฅ ์ •ํ™•ํ•˜๊ณ  ์œ ์˜๋ฏธํ•  ๊ฒƒ์ด๋ผ ์ƒ๊ฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“ ํ•ญ๊ณต ๊ตญ์ œ์„  ํ”Œ๋กœ์šฐ ์ •๋ฆฌ

์ œ๊ฐ€ ์•ฝ 3๊ฐœ์›”๊ฐ„ ํ•ญ๊ณต์ชฝ ๊ฐœ๋ฐœ์„ ํ•˜๋ฉฐ ํŒŒ์•…ํ–ˆ๋˜ ์ฃผ์š” ๋™์„ ๋“ค์„ ์œ„์ฃผ๋กœ ํ”Œ๋กœ์šฐ๋ฅผ ๋Ÿฌํ”„ํ•˜๊ฒŒ ์ •๋ฆฌํ•ด๋ดค์Šต๋‹ˆ๋‹ค.

์„ธ๋ถ€์ ์œผ๋กœ ์ •๋ง ๋‹ค์–‘ํ•ญ ๋™์„ ์ด ์žˆ์ง€๋งŒ, ์—ฌ๊ธฐ์„œ๋Š” ํ•ต์‹ฌ๋งŒ ์ •๋ฆฌํ•˜์—ฌ ๋‚˜ํƒ€๋ƒˆ์Šต๋‹ˆ๋‹ค.

1. ํ—ˆ๋ธŒ
๊ตญ์ œ์„  ๊ฒ€์ƒ‰ ์กฐ๊ฑด์„ ์ž…๋ ฅํ•˜๊ณ  "ํ•ญ๊ณต๊ถŒ ๊ฒ€์ƒ‰" ์„ ํด๋ฆญํ•˜๋ฉด ๊ตญ์ œ์„  [ํ•ญ๊ณตํŽธ ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€] ๋กœ ์ด๋™ํ•œ๋‹ค.

2. ํ•ญ๊ณตํŽธ ๊ฒ€์ƒ‰
- ํ•„ํ„ฐ๋ฅผ ํ†ตํ•œ ์žฌ๊ฒ€์ƒ‰์ด ๋˜์–ด์•ผํ•œ๋‹ค.
- ํ•ญ๊ณตํŽธ ์„ ํƒ ์‹œ <์นด๋“œ ํ”„๋กœ๋ชจ์…˜ ์•ก์…˜์‹œํŠธ> ๊ฐ€ ๋…ธ์ถœํ•œ๋‹ค.
  ใ„ด ์„ ํƒํ•œ ํ”„๋กœ๋ชจ์…˜์œผ๋กœ [ํ•ญ๊ณตํŽธ ์ƒ์„ธํŽ˜์ด์ง€] ๋กœ ์ด๋™ํ•œ๋‹ค.

3. ํ•ญ๊ณตํŽธ ์ƒ์„ธ
- "์ƒ์„ธ์š”๊ธˆ" ์„ ํƒ ์‹œ <์š”๊ธˆ ์ƒ์„ธ ์ •๋ณด ํŒ์—…> ์ด ๋…ธ์ถœ๋œ๋‹ค.
- "ํ•ญ๊ณตํŽธ ๋ณ€๊ฒฝ" ์„ ํƒ ์‹œ ์ด์ „ [ํ•ญ๊ณตํŽธ ๊ฒ€์ƒ‰๋ชฉ๋ก ํŽ˜์ด์ง€] ๋กœ ์ด๋™ํ•œ๋‹ค.
- ์ธ์›๋ณ€๊ฒฝ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ <์ธ์› ์„ ํƒ ํŒ์—…> ์ด ๋…ธ์ถœ๋œ๋‹ค.
   ใ„ด ์ƒˆ๋กœ์šด ์ธ์›ํƒ€์ž…์ด ์ถ”๊ฐ€๋  ๊ฒฝ์šฐ ํ•ด๋‹น ์กฐ๊ฑด์œผ๋กœ [ํ•ญ๊ณตํŽธ ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€] ๋กœ ์ด๋™ํ•˜์—ฌ ์žฌ๊ฒ€์ƒ‰ํ•œ๋‹ค.
   ใ„ด ๋‹จ์ˆœ ์ธ์›์ถ”๊ฐ€ ๋ฐ ์ œ๊ฑฐ์ผ ๊ฒฝ์šฐ ๊ฐ€๊ฒฉ ์ •๋ณด๋งŒ ์—…๋ฐ์ดํŠธ๋œ๋‹ค.
- "์šด์ž„ ๋ฐ ์ˆ˜ํ•˜๋ฌผ ๊ทœ์ • ์ž์„ธํžˆ๋ณด๊ธฐ" ์„ ํƒ ์‹œ <ํ•ญ๊ณต์‚ฌ ์šด์ž„ ๋ฐ ์ˆ˜ํ•˜๋ฌผ ๊ทœ์ • ํŒ์—…> ์ด ๋…ธ์ถœ๋œ๋‹ค.
- "์˜ˆ์•ฝํ•˜๊ธฐ" ์„ ํƒ ์‹œ [ํ•ญ๊ณตํŽธ ์˜ˆ์•ฝํŽ˜์ด์ง€] ๋กœ ์ด๋™ํ•œ๋‹ค.

4. ํ•ญ๊ณตํŽธ ์˜ˆ์•ฝ
- ์˜ˆ์•ฝ์ž ์ •๋ณด์™€ ํƒ‘์Šน๊ฐ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ ๋’ค์— ์•ฝ๊ด€์— ์ „์ฒด๋™์˜ํ•˜๋ฉด ์˜ˆ์•ฝ์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค.
- "์˜ˆ์•ฝํ•˜๊ธฐ" ์„ ํƒ ์‹œ <์˜ˆ์•ฝ์ •๋ณด ์ตœ์ข…ํ™•์ธ ํŒ์—…> ์ด ๋…ธ์ถœ๋œ๋‹ค.
   ใ„ด"๋„ค. ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค." ์„ ํƒ ์‹œ [ํ•ญ๊ณตํŽธ ๊ฒฐ์ œํŽ˜์ด์ง€] ๋กœ ์ด๋™ํ•œ๋‹ค.

5. ํ•ญ๊ณตํŽธ ๊ฒฐ์ œ
- ์—ฌ์ •์ •๋ณด ๋ฐ ํƒ‘์Šน๊ฐ์ •๋ณด ์„ ํƒ ์‹œ <์—ฌ์ •์ •๋ณด, ํƒ‘์Šน๊ฐ ์ •๋ณด ํŒ์—…>์ด ๋…ธ์ถœ๋œ๋‹ค.
- ๊ฒฐ์ œ์กฐ๊ฑด ๋ณ€๊ฒฝ ์‹œ <๊ฒฐ์ œ ์กฐ๊ฑด ๋ณ€๊ฒฝ ์•ก์…˜์‹œํŠธ> ๊ฐ€ ๋…ธ์ถœ๋œ๋‹ค.
   ใ„ด ๊ฒฐ์ œ ์กฐ๊ฑด ์„ ํƒ ํ›„ "์„ ํƒํ•œ ์กฐ๊ฑด์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ธฐ" ์„ ํƒ ์‹œ ์„ ํƒ๋œ ์กฐ๊ฑด์œผ๋กœ ๊ธˆ์•ก์ด ์žฌ์กฐํšŒ๋œ๋‹ค.
- ์นด๋“œ๋ฒˆํ˜ธ, ์œ ํšจ๊ธฐ๊ฐ„, ๋น„๋ฐ€๋ฒˆํ˜ธ ์•ž ๋‘์ž๋ฆฌ, ์นด๋“œ ์†Œ์œ ์ฃผ ์ƒ๋…„์›”์ผ, ํ• ๋ถ€ ๊ฐœ์›” ์ˆ˜๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ๊ฒฐ์ œ๊ทœ์ •์— ๋™์˜ํ•˜๋ฉด ๊ฒฐ์ œ๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง„๋‹ค.
- "๊ฒฐ์ œํ•˜๊ธฐ" ์„ ํƒ ์‹œ ๊ฒฐ์ œ๊ฐ€ ์ด๋ฃจ์–ด์ง„๋‹ค.

6. ๋ฐœ๊ถŒ์™„๋ฃŒ
- "๋‚ด ์˜ˆ์•ฝ์—์„œ ํ™•์ธ" ์„ ํƒ ์‹œ [๊ตญ์ œ์„  ๋‚ด ์˜ˆ์•ฝ์ƒ์„ธ ํŽ˜์ด์ง€]๋กœ ์ด๋™ํ•œ๋‹ค.

 

 

2๏ธโƒฃ ํ…Œ์ŠคํŠธ ํ˜•์‹ & ๊ฒฉ๋ฆฌ

์ผ๋ฐ˜์ ์ธ ํ…Œ์ŠคํŠธ๋Š” ๋‹ค์Œ 3๊ฐ€์ง€ ๋‹จ๊ณ„๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

given
์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŠน์ • ์ƒํƒœ(state)์—์„œ

when 
ํŠน์ • ๋™์ž‘(action)์„ ์ˆ˜ํ–‰ํ•  ๋•Œ

then
์˜๋„๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆ(assert)ํ•œ๋‹ค.

 

Cypress ๋ฅผ ํ†ตํ•œ E2E ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•  ๋•Œ์—๋Š” ์—ฌ๊ธฐ์„œ ์ข€ ๋” ๊ตฌ์ฒด์ ์ธ ๋‹จ๊ณ„์˜ ์ •์˜๊ฐ€ ํ•„์š”ํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

visit
ํ…Œ์ŠคํŠธํ•  ํŠน์ • ํŽ˜์ด์ง€์— ๋ฐฉ๋ฌธํ•ด์„œ

query & interaction
ํŠน์ • ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฐพ์•„ ์ƒํ˜ธ์ž‘์šฉ์„ ํ†ตํ•ด

assertion
์˜๋„ํ•œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•œ๋‹ค.

 

์œ„ 3๋‹จ๊ณ„๋กœ ์ˆ˜ํ–‰๋˜๋Š” ๊ฐ๊ฐ์˜ ๋…๋ฆฝ๋œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

cypress ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ฐ๊ฐ์˜ ํ…Œ์ŠคํŠธ ์‚ฌ์ด์— test state ๋ฅผ ๋ชจ๋‘ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฅผ ํ†ตํ•ด ์ผ๊ด€๋œ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

 

3๏ธโƒฃ mocks-server ๋ฅผ ํ™œ์šฉํ•œ API ๋ชจํ‚น

ํ•ญ๊ณต์€ ๋‹จ์ˆœ ์กฐํšŒ API ์˜ ๊ฒฝ์šฐ์—๋„ ์บ์‹œํ‚ค ๋งŒ๋ฃŒ ๋“ฑ์˜ ๋‹ค์–‘ํ•œ ์ผ€์ด์Šค๋กœ ์ธํ•œ ๋™์„ ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

์กฐ๊ฑด์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ์–ป๊ฒŒ๋˜๋ฉด ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•œ ์‹ ๋ขฐ๋„๊ฐ€ ๋‚ฎ์•„์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋ ‡๊ธฐ์— ์šฐ๋ฆฌ๋Š” ์ฒ ์ €ํžˆ ํ†ต์ œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•œ๋ฐ, mocks-server ๊ฐ€ ๊ทธ ์—ญํ• ์„ ๋„์™€์ค๋‹ˆ๋‹ค.

 

 

mocks-server ์ปจ์…‰

 

 

mocks-server ๋ฅผ ํ†ตํ•ด ์„ฑ๊ณต ๋ฐ ์‹คํŒจ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ์— ํ•„์š”ํ•œ API ๋“ค์„ ๋ชจํ‚นํ•ด์„œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

ํ•ญ๊ณต์›น์€ ํ˜„์žฌ ์„œ๋ฒ„ API endpoint ๋ฅผ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์–ด ๊ฐ„ํŽธํ•˜๊ฒŒ ์œ„์ž„์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ๊ตญ์ œ์„  ํ•ญ๊ณต๊ถŒ ๊ฒ€์ƒ‰ API ๋ฅผ ๋ชจํ‚นํ•œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

 

// mocks/routes/intl-search.js

const SEL_NYC_RESULTS_FIXTURE = [/** ์„œ์šธ <-> ๋‰ด์š• ๊ฒ€์ƒ‰๊ฒฐ๊ณผ ๋ชจํ‚น๋ฐ์ดํ„ฐ */]

module.exports = [
    {
    id: 'search-intl-flights',
    url: '/api/air/intl/search/flights/:key',
    method: 'POST',
    variants: [
      {
        id: 'success',
        response: (req, res) => {
          const sortCriteria = req.body['sort'] ?? 'PRICE'
          const airlineCriteria = req.body['filter']['byAirline']

          let contents = SEL_NYC_RESULTS_FIXTURE.contents

          if (airlineCriteria) {
            contents = SEL_NYC_RESULTS_FIXTURE.contents.filter((content) =>
              content.schedules.some((schedule) =>
                airlineCriteria.includes(schedule.carrier.code),
              ),
            )
          }

          /** ๊ฐ€๊ฒฉ, ๊ฐ€๋Š”๋‚  ์ถœ๋ฐœ์‹œ๊ฐ„ ์ˆœ ์ •๋ ฌ๋งŒ ๋ชจํ‚น */
          if (sortCriteria === 'OUTBOUND_DEPARTURE_TIME') {
            contents.sort((a, b) =>
              moment(a.schedules[0].departureDateTime).isBefore(
                b.schedules[0].departureDateTime,
              )
                ? -1
                : 1,
            )
          } else {
            contents.sort((a, b) => a.totalPrice - b.totalPrice)
          }

          res.send({
            ...SEL_NYC_RESULTS_FIXTURE,
            contents,
            page: {
              currentPage: 1,
              pageSize: 20,
              totalCount: contents.length,
            },
            status: contents.length > 0 ? 'COMPLETE' : 'NO_DATA',
          })
        },
      },
    ],
  }
]

 

๐Ÿ“Œ Cypress ์‚ฌ์šฉ ์‹œ ์œ ์šฉํ•œ ํŒ๊ณผ ๊ธฐ๋Šฅ๋“ค

1๏ธโƒฃ default selector ์šฐ์„ ์ˆœ์œ„

data-cy
data-test
data-testid
data-qa
id
class
tag
attributes
nth-child

 

ํ…Œ์ŠคํŠธ๋ฅผ ํ• ๋•Œ ์ƒํ˜ธ์ž‘์šฉํ•  ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ฐพ์„ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” selector ๋Š” ์œ„์™€ ๊ฐ™์€ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

dom ์ด๋‚˜ css ์†์„ฑ์— ์˜์กดํ•  ๊ฒฝ์šฐ UI ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๊ด€๋ฆฌ ์ธก๋ฉด์—์„œ ์ทจ์•ฝํ•˜๊ธฐ ๋•Œ๋ฌธ์—

data- ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ปค์Šคํ…€ ํ…Œ์ŠคํŠธ id ๋ฅผ ๋ถ€์—ฌํ•˜๋Š” ๋ฐฉ์‹์ด ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค.

์ด๋ฒˆ ๊ตญ์ œ์„  E2E ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ๋„ ์šฐ์„ ์ ์œผ๋กœ data-cy ๋ฅผ ํ†ตํ•œ ์œ ์ผํ•œ test id ๋ฅผ ๋ถ€์—ฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๋‹ค๋งŒ ์ง์ ‘์ ์ธ ์ œ์–ด๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•œ ์™ธ๋ถ€ ํŒจํ‚ค์ง€ UI ์ปดํฌ๋„ŒํŠธ์˜ ์š”์†Œ๋ฅผ ๊ฒ€์ƒ‰ํ•ด์•ผํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š”

ํƒœ๊ทธ ํƒ€์ž…, class ๋ช… ๋“ฑ์˜ ์†์„ฑ ์„ ํƒ์ž๋“ค์„ ์กฐํ•ฉํ•ด์„œ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

 

์˜ˆ์‹œ- background image ๋กœ๋งŒ ์ฒ˜๋ฆฌ๋˜์–ด์žˆ๋Š” nav-item

 

์ฃผ์˜ํ•  ์ ์€, styled-component ๋“ฑ์„ ์ด์šฉํ•ด์„œ ์ƒ์„ฑํ•œ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์˜ ๊ฒฝ์šฐ์—๋Š”

className ์ด production build ์‹œ์— ์ตœ์ ํ™” ๊ณผ์ •์—์„œ ๋ณ€๊ฒฝ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

 

element select ์˜ best practice ๋Š” ๊ณต์‹๋ฌธ์„œ์— ๋‚˜์™€์žˆ์Šต๋‹ˆ๋‹ค.

Best Practices | Cypress Documentation

 

Best Practices | Cypress Documentation

<TOCInline

docs.cypress.io

 

2๏ธโƒฃ ์ž์ฃผ ์ฐธ์กฐ๋˜๋Š” ์—˜๋ฆฌ๋จผํŠธ๋Š” alias ์ง€์ •ํ•˜๊ธฐ

๋™์ผํ•œ ๋ฌธ๋งฅ์—์„œ ์ž์ฃผ ์ฐธ์กฐ๋˜๋Š” element ๋Š” alias ์ง€์ •์„ ํ†ตํ•ด query ๋ฌธ์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

cy.get('.DayPicker-Day--today').as('today')

cy.get('@today').next().click()
cy.get('@today').next().next().click()

 

3๏ธโƒฃ native clock override

์‹œ๊ฐ„๊ณผ ๊ด€๋ จ๋œ ๋™์ž‘์„ ์ œ์–ดํ•ด์•ผํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•ญ๊ณต ๋ฉ”์ธ(ํ—ˆ๋ธŒ)์—์„œ ์™•๋ณต ํ•ญ๊ณตํŽธ์„ ์„ ํƒํ•  ๋•Œ ์ถœ๊ตญ์ผ๊ณผ ์ž…๊ตญ์ผ์„ ๋‹ฌ๋ ฅ์—์„œ ์„ ํƒํ•ด์•ผํ•˜๋Š”๋ฐ,

์ด๋•Œ ํ…Œ์ŠคํŠธ์˜ ๊ฐ„ํŽธํ•จ์„ ์œ„ํ•ด ์˜ค๋Š˜๋‚ ์งœ๋กœ ํ˜„์žฌ timestamp๋ฅผ ๋ถ€์—ฌํ–ˆ์Šต๋‹ˆ๋‹ค.

(๊ตญ์ œ์„ ์€ ์˜ค๋Š˜ + 1 ์ผ๋ถ€ํ„ฐ ์ถœ๊ตญ์ผ์„ ์„ ํƒํ•ด์•ผํ•˜๋Š” ์กฐ๊ฑด์ด ์žˆ์Œ)

cy.clock(new Date(2023, 2, 13), ['Date'])

/** ํ—ˆ๋ธŒ์— ์ง„์ž… */
cy.visit('/')

์ด๋ ‡๊ฒŒํ•˜๋ฉด ๋‹ฌ๋ ฅ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋‚ด๋ถ€์ ์œผ๋กœ Date ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ์‹œ์ ์ด 2023-02-13 ์ด ๋˜์–ด ์˜ค๋Š˜ ๋‚ ์งœ๋ฅผ ์›ํ•˜๋Š” ๋‚ ์งœ๋กœ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

4๏ธโƒฃ ๋งค ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ์ˆ˜ํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ํ›…์€ ๋ถ„๋ฆฌํ•˜๊ธฐ

๊ฐ๊ฐ์˜ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ๋˜๋„๋ก ์ž‘์„ฑํ•˜๋˜, ๋งค ํ…Œ์ŠคํŠธ ๋งˆ๋‹ค ๋™์ผํ•œ ๊ณผ์ •์„ ๋ฐ˜๋ณตํ•ด์•ผํ•œ๋‹ค๋ฉด ์ด๋ฅผ ๋ณ„๋„์˜ ํ›…์œผ๋กœ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

cypress ์—์„œ๋Š” Mocha ์˜ BDD ๋ฐฉ์‹๊ณผ ๋™์ผํ•˜๊ฒŒ ํ›…์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์—ฌ๋Ÿฌ ํ›…์„ ์ด์šฉํ•ด ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ์ˆ˜ํ–‰๋  ํ›…์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// ํŽ˜์ด์ง€ ๋ฐฉ๋ฌธ ์˜ˆ์‹œ (beforeEach)
describe('๊ตญ์ œ์„  ํ•ญ๊ณตํŽธ ๊ฒฐ์ œ ๋ฐ ๋ฐœ๊ถŒ์™„๋ฃŒ', () => {
  beforeEach(() => {
    cy.visit(
      '/payment?orderId=3950&id=AMADEUS_AC62G3AC726G3AC8997G3AC61G4&adult=1&child=0&infant=0&from=hub&requestTotalPrice=1067700&originDestination=c%3ASEL-c%3ANYC&inboundDate=2023-03-22&outboundDate=2023-03-15&cabinTypes=ECONOMY&cabinTypes=PREMIUM_ECONOMY&cabinTypes=BUSINESS&cabinTypes=FIRST&detailKey=087d07ff-9c99-4fc5-a166-d65bfcca769d%3A%3AAMADEUS_84e4d118-db54-4447-8aa8-e889af90e798%3A%3A1_1_2&cashRewardDefinitionId=f6938058-e534-4127-89e8-bbf73131635a',
    )
    cy.contains('๊ฒฐ์ œ๋‚ด์šฉ ํ™•์ธ', { timeout: 60000 }).should('be.visible')
  })

    it('๊ฒฐ์ œ์กฐ๊ฑด ๋ณ€๊ฒฝ ์‹œ ์„ ํƒํ•œ ์กฐ๊ฑด์œผ๋กœ ๊ฒฐ์ œ์ •๋ณด๊ฐ€ ์žฌ์กฐํšŒ๋œ๋‹ค', () => {
        // ...
    })

    it('์นด๋“œ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ๊ฒฐ์ œ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ๋ฐœ๊ถŒ์™„๋ฃŒ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•œ๋‹ค.', () => {
        // ...
  })
})

// ํŒ์—… ๋…ธ์ถœ ์˜ˆ์‹œ (afterEach)
describe('๊ฒฐ์ œ์ •๋ณด ๊ด€๋ จ ํŒ์—…์„ ๋…ธ์ถœํ•œ๋‹ค.', () => {
  it('์—ฌ์ •์ •๋ณด ํŒ์—…์„ ๋…ธ์ถœํ•œ๋‹ค.', () => {
    // ...
  })

  it('ํƒ‘์Šน๊ฐ์ •๋ณด ํŒ์—…์„ ๋…ธ์ถœํ•œ๋‹ค.', () => {
    // ...
  })

  afterEach(() => {
    /** ํŒ์—… ๋‹ซ๊ธฐ */
    cy.get('.-triple-fallback-action').click()
  })
})

 

5๏ธโƒฃ custom command ์ถ”๊ฐ€ํ•˜๊ธฐ

cypress ๋Š” custom command ๋ฅผ ํ†ตํ•ด ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๋กœ์ง์„ ๋ถ„๋ฆฌํ•ด์„œ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ๋Š” ์ƒˆ๋กœ์šด command ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ๊ธฐ์กด์˜ command ๋ฅผ overwrite ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋จผ์ € custom command ๋ฅผ ํ—ˆ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก supportFile ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•ด์ค๋‹ˆ๋‹ค.

 

// cypress.config.js

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    supportFile: 'cypress/support/index.ts',
        // ...
  },
  // ...
})

 

์ด์ œ ํŠธ๋ฆฌํ”Œ ๋กœ๊ทธ์ธ ์‹œ ์„ค์ •๋˜๋Š” ์ฟ ํ‚ค๊ฐ’์„ ์„ธํŒ…ํ•ด์ฃผ๋Š” ๊ฐ„๋‹จํ•œ command ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

command.ts ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์„œ index ์—์„œ import ๋ฅผ ํ•ด์˜ค๋ฉด,

๋ณ„๋„์˜ import ๋ฌธ ์—†์ด ๋งค ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰์‹œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ๋ถˆ๋Ÿฌ์™€์„œ ํ•ด๋‹น command ๋“ค์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

// cypress/support/index.ts

import './command'

// cypress/support/command.ts

Cypress.Commands.add('setupTripleUserCookie', () => {
  cy.setCookie('x-sample', 'test')
  cy.setCookie('x-session', 'test')
})

 

ํ•ด๋‹น command ์— ๋Œ€ํ•œ ๊ฐ„๋‹จํ•œ ์„ค๋ช…๋„ ์žˆ์œผ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋„ค์š”.

์ด๋ฅผ ์œ„ํ•ด Cypress ํƒ€์ž…์„ ํ™•์žฅํ•ฉ๋‹ˆ๋‹ค.

 

// cypress/types/types.d.ts

declare namespace Cypress {
  interface Chainable {
    /**
     * ํŠธ๋ฆฌํ”Œ ๋กœ๊ทธ์ธ ์‹œ ์„ค์ •๋˜๋Š” ์ฟ ํ‚ค๊ฐ’์„ ๋ชจํ‚นํ•ฉ๋‹ˆ๋‹ค.
     * @example cy.setupTripleUserCookie()
     */
    setupTripleUserCookie(): void
  }
}

 

6๏ธโƒฃ intercept ๋กœ API spying

cypress ์—์„œ ์ œ๊ณตํ•˜๋Š” network intercept ๋„ ์œ ์šฉํ•œ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.

ํ•ญ๊ณต ๊ตญ์ œ์„ ์—์„œ๋Š” ๊ฒฐ์ œ ํŽ˜์ด์ง€์—์„œ ์„ ํƒํ•œ ์นด๋“œ ํ”„๋กœ๋ชจ์…˜ ์ •๋ณด๊ฐ€ ๋ฐ”๋€Œ๋ฉด

ํ•ด๋‹น ํ”„๋กœ๋ชจ์…˜ id ๋กœ ๊ฒฐ์ œ์ •๋ณด๋ฅผ ์žฌ์กฐํšŒํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

 

์„œ๋กœ๋‹ค๋ฅธ ์นด๋“œ ํ”„๋กœ๋ชจ์…˜ mocking ๋ฐ์ดํ„ฐ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์—๋Š” ํ•œ๊ณ„๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—

๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ธํ•˜๋Š” ๋Œ€์‹  ์žฌ์กฐํšŒ API ๋ฅผ ๋‹ค์‹œ ํ˜ธ์ถœํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

 

it('๊ฒฐ์ œ์กฐ๊ฑด ๋ณ€๊ฒฝ ์‹œ ์„ ํƒํ•œ ์กฐ๊ฑด์œผ๋กœ ๊ฒฐ์ œ์ •๋ณด๊ฐ€ ์žฌ์กฐํšŒ๋œ๋‹ค.', () => {
    /** ๊ฒฐ์ œ์กฐ๊ฑด ๋ณ€๊ฒฝ์„ ์œ„ํ•œ ์•ก์…˜์‹œํŠธ ์„ ํƒ */
  cy.contains('๊ฒฐ์ œ ์กฐ๊ฑด ๋ณ€๊ฒฝ').click()
  cy.get('div[class^="action-sheet-body"]')
    .contains('๊ฒฐ์ œ ์กฐ๊ฑด ๋ณ€๊ฒฝ')
    .should('be.visible')
  cy.get('tr[class^=card-promotion-radios]').eq(0).click()

  cy.intercept({
    url: '/api/air/intl/booking/ticketing/ready/orders/*',
    method: 'GET',
    query: {
      cardPromotionPrincipleId: '*',
    },
  }).as('cardPromotionAppliedTicketingRequest')

  /** ์นด๋“œ ํ”„๋กœ๋ชจ์…˜์ด ์ ์šฉ๋œ API ๋ฅผ ํ˜ธ์ถœํ•˜๋Š”์ง€ ํ™•์ธ */
  cy.get('button').contains('์„ ํƒํ•œ ์กฐ๊ฑด์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ธฐ').click()
  cy.wait('@cardPromotionAppliedTicketingRequest')
})

 

๐Ÿ“Œ Cypress test id ์ œ๊ฑฐํ•˜๊ณ  ๋ฐฐํฌํ•˜๊ธฐ

 

ํ…Œ์ŠคํŒ…ํ• ๋•Œ test-id ๋ฅผ ๋ถ€์—ฌํ•˜๋ฉด ์šด์˜ํ™˜๊ฒฝ์—์„œ๋„ ์ด id ๋“ค์ด ์œ ์ €๋“ค์—๊ฒŒ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ๋นŒ๋“œ ๋‹จ๊ณ„์—์„œ ์ด๋ฅผ ์ œ๊ฑฐํ•ด์ค˜์•ผํ•˜๋Š”๋ฐ, Next 12 ๋ถ€ํ„ฐ babel ๋Œ€์‹  SWC ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ

๋ณ„๋„์˜ ํ”Œ๋Ÿฌ๊ทธ์ธ(babel-plugin-react-remove-properties) ์—†์ด config ์„ค์ •๋งŒ์œผ๋กœ ๊ฐ€๋Šฅํ•ด์กŒ์Šต๋‹ˆ๋‹ค.

 

ํ™˜๊ฒฝ๋ณ€์ˆ˜์™€ ์ •๊ทœ์‹์„ ์ด์šฉํ•ด์„œ ์•ž์„œ ์‚ดํŽด๋ณธ cypress ์šฉ test id ๋“ค์„ production ๋นŒ๋“œ์‹œ์— ๊ฑธ๋Ÿฌ๋‚ด์ค๋‹ˆ๋‹ค.

 

// next.config.js

module.exports = {
compiler: {
    reactRemoveProperties:
      NODE_ENV === 'production'
        ? {
            properties: ['^data-(cy|test|testid|qa)$'],
          }
        : undefined,
  },
}

 

์ด์ œ ๋…ธ์ถœ๋˜๋˜ ํ…Œ์ŠคํŠธ์šฉ id ๋“ค์ด ์‚ฌ๋ผ์กŒ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ๋ณ‘๋ ฌ ํ…Œ์ŠคํŠธ

Cypress ์—์„œ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋ ค๋ฉด Cypress Clound(์œ ๋ฃŒ) ๋ฅผ ์ด์šฉํ•˜๊ฑฐ๋‚˜

๋‹ค๋ฅธ ํŒจํ‚ค์ง€(sorry-cypress ๋“ฑ๋“ฑ)๋ฅผ ํ•จ๊ป˜ ์ด์šฉํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

์ €๋Š” ์ด๋ฒˆ ์œ„ํด๋ฆฌ์—์„œ๋Š” ๋กœ์ปฌ์—์„œ๋งŒ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋Š”๊ฑธ ๊ตฌํ˜„ํ•ด๋ณด๊ณ  ์‹ถ์–ด์„œ

Node.js ์˜ child_process ๋ฅผ ํ™œ์šฉํ•œ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ดค๋Š”๋ฐ.. ์•„์ง ์ œ๋Œ€๋กœ ๋™์ž‘์€ ์•ˆํ•˜๋„ค์š”.

(์˜คํžˆ๋ ค ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰์‹œ๊ฐ„์ด ๋” ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋„ค์š” ๐Ÿ˜…)

 

const CYPRESS_ROOT_PATH = './cypress/integration'
const WORKLOAD = 2
const SPEC_FILE_REGEX = /.*?\.spec\.(js|ts)/

async function main() {
  const testSuitePaths = deriveTestSuitePaths()
  const jobs = allocateJobs(testSuitePaths)

  await runTestGroup(jobs)
}

async function runTestGroup(jobs) {
  const label = '๐Ÿš€ total execution time'

  try {
    console.time(label)
    await Promise.all(jobs)
  } catch (error) {
    console.error(`โŒ test failed : ${error.message}`)
  } finally {
    console.timeEnd(label)
  }
}

function allocateJobs(testSuitePaths) {
  const tasks = new Map()

  const groupedJobs = chunk(testSuitePaths, WORKLOAD)
  groupedJobs.forEach((group, idx) => tasks.set(idx, group))

  return [...tasks.entries()].map(([key, task]) => process(key, task))
}

async function process(key, testSuites) {
  const testOption = {
    command: 'cypress:headless',
    option: '--spec',
    specs: testSuites.join(','),
  }

  return new Promise((resolve, reject) => {
    const child = spawn('npm', [
      'run',
      testOption.command,
      '--',
      testOption.option,
      testOption.specs,
    ])

    child.stdout.on('data', (data) => {
      // ํ˜„์žฌ ์ˆ˜ํ–‰์ค‘์ธ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ์ถœ๋ ฅ
    })

    child.on('exit', (code) => {
      const hasException = code > 0

      if (hasException) {
        reject(new Error(`exception occur at child process with code ${code}`))
      }
      resolve(true)
    })
  })
}

 

๊ทธ๋ž˜๋„ ์‹œ๊ฐ„๋ ๋•Œ ์กฐ๊ธˆ์”ฉ ๋งŒ๋“ค์–ด์„œ CI ํ™˜๊ฒฝ์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํŒจํ‚ค์ง€๋ฅผ ๋ฐฐํฌ๊นŒ์ง€ ํ•ด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. (์˜ฌํ•ด์•ˆ์—..?)

 

๐Ÿ“Œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€

Cypress ๋ฅผ ํ™œ์šฉํ•œ E2E ํ…Œ์ŠคํŠธ์‹œ์—๋„ code coverage ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋•Œ code instrument(์ฝ”๋“œ ๊ณ„์ธก) ๊ณผ์ •์ด ์„ ํ–‰๋˜์–ด์•ผํ•˜๋Š”๋ฐ, ์ด๋Š” Cypress ์—์„œ ๋ณ„๋„๋กœ ์ œ๊ณตํ•ด์ฃผ์ง€ ์•Š๊ธฐ์— ๋ณ„๋„๋กœ ์ง์ ‘ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

๊ฐ€์žฅ ์œ ๋ช…ํ•œ ๊ฒƒ์€ istanbul ์ด๋ผ๋Š” ํŒจํ‚ค์ง€๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ธ๋ฐ, ์•„์‰ฝ๊ฒŒ๋„ ์•ˆ์ •ํ™”๋œ Babel ๋ฒ„์ „์€ Next 12 ์ด์ƒ๋ถ€ํ„ฐ๋Š” ๋”์ด์ƒ ์œ ํšจํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ ๋Œ€์‹  swc ์— ๋Œ€์‘ํ•ด์„œ ๋‚˜์˜จ ์‹คํ—˜์ ์ธ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

(๊ธฐ์กด babel-plugin-istanbul ์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค.)

 

์›๋ž˜๋Š” ๊ณต์‹ ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ๋“ค์–ด๊ฐ€๋ ค๊ณ  ํ…Œ์ŠคํŠธ ๋‹จ๊ณ„์— ๋“ค์–ด๊ฐ”๋Š”๋ฐ,

์‹ค ์‚ฌ์šฉ์ž๊ฐ€ ๋งŽ์ด ์•Š์•„์„œ ๊ทธ๋Ÿฐ์ง€ ์ถ”ํ›„ ์ง€์›์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ๋”๋ผ๊ตฌ์š”.

feat(next/swc): support experimental coverage instrument by kwonoj · Pull Request #36692 · vercel/next.js

 

feat(next/swc): support experimental coverage instrument by kwonoj · Pull Request #36692 · vercel/next.js

Related to #30529 , #30174 This PR enables an experimental next.js configuration to enable coverage instrumentation via next-swc. Currently configuration itself is not being used yet other than if ...

github.com

 

โญ๏ธ ์ ์šฉ ๋ฐฉ๋ฒ•

> npm i -D swc-plugin-coverage-instrument

 

npm script ์— COVERAGE ๋ฅผ true ๋กœ ๋„˜๊ฒจ์ค„ ๊ฒฝ์šฐ์—๋งŒ intrument ๊ณผ์ •์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํ• ๋‹นํ•ฉ๋‹ˆ๋‹ค.

 

// next.config.js

module.exports = {
    experimental:
    process.env.COVERAGE === 'true'
      ? {
          swcPlugins: [['swc-plugin-coverage-instrument', {}]],
        }
      : undefined,
}

instrument ๊ณผ์ •์„ ๋งˆ์น˜๋ฉด, ์ฝ”๋“œ ์ฐธ์กฐ ์ •๋ณด๊ฐ€ ์ „์—ญ window ๊ฐ์ฒด์— __coverage__ ๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค.

ํ•ด๋‹น ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ•˜๊ณ  dev ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•ด์„œ ๋นŒ๋“œ๋œ ๊ฒฐ๊ณผ๋ฌผ์„ ํ™•์ธํ•ด๋ด…์‹œ๋‹ค.

 

 

external module ๋ถ€ํ„ฐ app code ๊นŒ์ง€ ์ฐธ์กฐ ์ •๋ณด๊ฐ€ ์ถ”๊ฐ€๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

(babel plugin ์€ external module ์€ ์ œ์™ธํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋Š”๋ฐ, swc plugin ๋ฒ„์ „์€ ์ œ๊ณตํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์— ํ•œ๊ณ„๊ฐ€ ์žˆ๋„ค์š”)

 

ํ…Œ์ŠคํŠธ ์‹œ์— code coverage ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Cypress ์—์„œ ์ œ๊ณตํ•˜๋Š” @cypress/code-coverage ๋ผ๋Š” ํŒจํ‚ค์ง€๋ฅผ ์ถ”๊ฐ€ํ•ด์ค˜์•ผํ•ฉ๋‹ˆ๋‹ค.

 

npm install -D @cypress/code-coverage

 

cypress.config.js ํŒŒ์ผ์—์„œ ํ•ด๋‹น ํŒจํ‚ค์ง€๋ฅผ e2e ํ…Œ์ŠคํŠธ ์‹œ์— ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.

 

// support/e2e.ts
import '@cypress/code-coverage/support'

// support/index.ts
import './e2e'

// cypress.config.js
module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      require('@cypress/code-coverage/task')(on, config)
      // NOTE: include any other plugin code

      return config
    },
  },
})

 

๊ทธ๋ฆฌ๊ณ  cypress ์‹คํ–‰ ์‹œ coverage ์˜ต์…˜์„ ํ•จ๊ป˜ ๋„˜๊ฒจ์ฃผ๋ฉด plugin ์‚ฌ์šฉ ์—ฌ๋ถ€๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

"cypress:headless:coverage": "cypress run --env coverage=true"

 

์ด ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ…Œ์ŠคํŠธ ์ดํ›„ ์ƒ์„ฑ๋œ code coverage ์ •๋ณด๋ฅผ HTML ํŒŒ์ผ๋กœ ์ƒ์„ฑํ•ด์„œ

๊ฒ€์ƒ‰์„ ํ†ตํ•ด ์›ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋งŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ํ•ญ๊ณต ๊ตญ์ œ์„  E2E ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๊ฒฐ๊ณผ๋ฌผ์ž…๋‹ˆ๋‹ค.

 

 

๊ฐ๊ฐ์˜ ํŒŒ์ผ์— ๋Œ€ํ•ด์„œ ํ…Œ์ŠคํŠธ์ฝ”๋“œ ์ถ”๊ฐ€๊ฐ€ ํ•„์š”ํ•œ ๋ถ€๋ถ„์ด๋‚˜ ๋†“์นœ ๋ถ€๋ถ„์„ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

์ด๋Ÿฐ coverage ์ •๋ณด๋„ CI ์‹œ github artifact ์— ํ•จ๊ป˜ ์—…๋กœ๋“œํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์€๋ฐ..

์ด ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ํ˜„์žฌ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ํ™œ๋ฐœํ•˜์ง€ ์•Š๊ณ , ๋ณด๊ณ ๋œ ์ด์Šˆ ์ค‘์— Next 13 ์˜ /app ๋””๋ ‰ํ† ๋ฆฌ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ ์‹œ

์•ฑ ์‚ฌ์šฉ์— ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚จ ์‚ฌ๋ก€๊ฐ€ ๋ณด๊ณ ๋œ ๊ฒƒ์ด ์žˆ์–ด์„œ ์•ˆ์ •ํ™”๋˜๊ธฐ์ „๊นŒ์ง€๋Š” ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ๋งŒ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

1๏ธโƒฃ ๊ฐ€๋” ์˜ˆ์ƒ์น˜ ๋ชปํ•˜๊ฒŒ ํ…Œ์ŠคํŠธ ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ

๊ฐ€๋” ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์ด์œ  (๋„คํŠธ์›Œํฌ ์ด์Šˆ, API, ๋ Œ๋”๋ง ์†๋„ ๋“ฑ) ์œผ๋กœ ์ธํ•ด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ณ€์ˆ˜๋ฅผ ์ตœ์†Œํ•œ์œผ๋กœ ์ค„์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด ๋ช‡๊ฐ€์ง€ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด์„œ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

1. next-build ๋œ ํŒŒ์ผ๋กœ CI ์—์„œ ํ…Œ์ŠคํŠธ ์ง„ํ–‰ํ•˜๊ธฐ

๋กœ์ปฌ์—์„œ๋Š” ๊ฐœ๋ฐœ์„œ๋ฒ„์—์„œ ๋™์ž‘ํ•˜๋Š” ์•ฑ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜์ง€๋งŒ

CI ํ™˜๊ฒฝ์—์„œ๋Š” build ๋ฅผ ํ†ตํ•ด ์ตœ์ ํ™”๋œ ์ฝ”๋“œ๋กœ ์ˆ˜ํ–‰๋  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

"e2e:headless": "start-server-and-test testdev-withmocks http://localhost:3000/air cypress:headless",
"e2e:ci": "start-server-and-test start-withmocks http://localhost:3000/air cypress:headless",

 

2. ์ด๋ฒคํŠธ ๋กœ๊น… blocking

ํ…Œ์ŠคํŠธ ๋‹จ๊ณ„์—์„œ๋Š” ๋ถˆํ•„์š”ํ•œ network ์š”์ฒญ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ์ด๋ฒคํŠธ ๋กœ๊น… ๊ด€๋ จ ์š”์ฒญ์„ ๋ง‰๋„๋ก ์„ค์ •์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

// cypress.config.js

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  // ...
  blockHosts: '*.google-analytics.com',
})

 

3. visit, location ์ด๋™ ์‹œ timeout ๋ถ€์—ฌ

ํŽ˜์ด์ง€์— ์ง„์ž…ํ•˜๊ฑฐ๋‚˜ ์ด๋™ํ•  ๋•Œ API ํ˜ธ์ถœ ๋“ฑ์— ์‹œ๊ฐ„์ด ์†Œ์š”๋˜์–ด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๊ฐ„ํ—์ ์œผ๋กœ ์žˆ์–ด ๊ธฐ๋ณธ timeout ๊ฐ’์ธ 4s ๋ณด๋‹ค ํฌ๊ฒŒ ์„ค์ •ํ•ด์คฌ์Šต๋‹ˆ๋‹ค.

 

4. test-retries ์„ค์ • ์ถ”๊ฐ€

๊ธฐ๋ณธ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ์‹คํŒจ์‹œ์— ์žฌ์‹œ๋„๋ฅผ ํ•˜์ง€ ์•Š์ง€๋งŒ ์„ค์ •์„ ํ†ตํ•ด ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

runMode ์™€ openMode ์— ๊ฐ๊ฐ ์„ค์ •ํ•ด์ค„ ์ˆ˜ ์žˆ๋Š”๋ฐ,

CI ์‹œ headless ํ•˜๊ฒŒ ์ˆ˜ํ–‰๋˜๋Š” cypress run ์— ์ ์šฉ๋  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์คฌ์Šต๋‹ˆ๋‹ค.

 

// cypress.config.js

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  // ...
  retries: {
    runMode: 2,
  },
})

 

2๏ธโƒฃ ์„ธ์…˜ ์ฒ˜๋ฆฌ ์–ด๋–ป๊ฒŒ ํ•˜์ง€

ํ•ญ๊ณต์€ ๋ณ„๋„์˜ ํ…Œ์ŠคํŠธ์šฉ ๊ณ„์ •์ด ์—†๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์„ธ์…˜๊ณผ ์—ฎ์—ฌ์žˆ๋Š” ๋™์„ ์€ ์–ด๋–ป๊ฒŒ ํ• ์ง€ ๊ณ ๋ฏผํ•˜๋‹ค๊ฐ€ ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰ ์ „ ์ฟ ํ‚ค ์„ค์ •ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์šฐํšŒ๋ฅผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฟ ํ‚ค ์‚ฌ์šฉ์‹œ ์ฃผ์˜ํ• ์ ์€ cypress ๋Š” ์ผ๊ด€๋œ ์ƒํƒœ๋ฅผ ์œ„ํ•ด ๊ฐ๊ฐ์˜ test ์‚ฌ์ด์— ์ฟ ํ‚ค๋ฅผ ์ง€์šด๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์‚ฌ์ด์— ์„ธ์…˜์„ ์œ ์ง€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด cy.session ์„ ํ™œ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ๋” ์ƒ๊ฐํ•ด๋ณด๊ธฐ

mock ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์ƒ, ์„œ๋ฒ„ interface ๊ฐ€ ๋ฐ”๋€Œ๋ฉด mock ๋ฐ์ดํ„ฐ๋„ ๋ณ€๊ฒฝ์ด ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ•˜๋‚˜์˜ ์œ ์ง€๋ณด์ˆ˜ ํฌ์ธํŠธ๊ฐ€ ๋œ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ์ž๋™ํ™” ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์€ ์—†์„๊นŒ? ๊ณ ๋ฏผ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

→ ํ…Œ์ŠคํŠธ์˜ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด ๋ฐ์ดํ„ฐ์˜ ๋ฉฑ๋“ฑ์„ฑ์ด ๋ณด์žฅ๋˜์–ด์•ผํ• ํ…๋ฐ, side effect ๋Š” ์—†์„์ง€ ๊ฒ€์ฆ์ด ํ•„์š”ํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

์‚ฌ์‹ค ํ˜„์žฌ ํ•ญ๊ณต์›น์—์„œ ๊ฐ€์žฅ ๊ณจ์นซ๊ฑฐ๋ฆฌ๋Š” ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋™์„ ์ž…๋‹ˆ๋‹ค. (๊ฒ€์ƒ‰ํ‚ค ๋งŒ๋ฃŒ, ๊ฐ€๊ฒฉ ๋ณ€๋™, ์œ ํšจ๊ธฐ๊ฐ„ ๋งŒ๋ฃŒ ๋“ฑ๋“ฑ)

์ง€๊ธˆ์€ Happy Path ๋งŒ ์ถ”๊ฐ€ํ–ˆ๋Š”๋ฐ, ์ด๋Ÿฌํ•œ ์˜ˆ์™ธ ๋™์„ ๋„ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์— ํ•จ๊ป˜ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š”๊ฒŒ ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

→ network intercept ์ดํ›„ ๊ฐ•์ œ๋กœ ์‹คํŒจ์ฒ˜๋ฆฌ์‹œํ‚ค๋ฉด ๋™์„  ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ๋งˆ๋ฌด๋ฆฌ

 

 

๋Ÿฌํ”„ํ•œ E2E ํ…Œ์ŠคํŠธ ๋ฐฉ์‹์„ ๋„์ž…ํ•ด๋ดค๋Š”๋ฐ์š”, ์ด๋ฒˆ ๊ตญ์ œ์„  E2E ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด์„œ ๋ฐ˜๋ณต๋˜๋Š” ์ž‘์—…์œผ๋กœ ์ธํ•œ ํŠธ๋ฆฌํ”Œ ๋น„์ฆˆํ”„๋ก ํŠธ ๊ฐœ๋ฐœ์ž๋“ค์˜ ํ”ผ๋กœ๋„๊ฐ€ ์กฐ๊ธˆ์ด๋‚˜๋งˆ ์ค„์–ด๋“ค์—ˆ์œผ๋ฉด ํ•˜๋Š” ํฌ๋ง๊ณผ ํ•จ๊ป˜ ์ด๋ฒˆ ํฌ์ŠคํŠธ๋ฅผ ๋งˆ์น˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

๊ธด ๊ธ€ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.๐Ÿ™‡‍โ™‚๏ธ

๋ฐ˜์‘ํ˜•

๐Ÿ’ฌ ๋Œ“๊ธ€