๐ก ์ด๋ฒ ํฌ์คํธ๋ ์ฌ๋ด ์ํด๋ฆฌ์์ ๋ฐํํ ๋ด์ฉ์ ์ ๋ฆฌํ ๊ฒ์ ๋๋ค.
๐ ๋ค์ด๊ฐ๋ฉฐ
์๋ 12์๋ถํฐ ์ฝ 3๊ฐ์๊ฐ ํญ๊ณต ์น ๊ฐ๋ฐ์ ์งํํ๋๋ฐ, ์๊ฐ๋ณด๋ค ๋ค์ํ ๋์ ์ด ์์ด์ ๊ณ ์์ ํ๋ ๊ธฐ์ต์ด ์์ต๋๋ค.
๋๊ตฐ๋ค๋ ์ ๊ฐ ์งํํ๋ ์ ๋ฌด(์น ํ๋ธ ์คํ, TF v12 ์ ํ)๊ฐ ํญ๊ณต์ ์ ์ฒด์ ์ธ ๋์ ์ ์ ์ฉ์ด ํ์ํ๋ ๊ฒ๋ค์ด์ด์ ๋์ฑ ๊ทธ๋ฌ๋ ๊ฒ ๊ฐ์๋ฐ์.
์์ง E2E ํ ์คํธ ์์ฑ๊ฒฝํ์ด ์์ด์ ์ด๋ฒ ๊ธฐํ์ ๊ณต๋ถ๋ ํด๋ณผ ๊ฒธ ๊ตญ์ ์ ์์ฝ๋์ ์ E2E ํ ์คํธ๋ฅผ ์งํํด๋ดค์ต๋๋ค.
๐ E2E ํ ์คํธ๋?
์ฐ์ ๊ฐ๋จํ๊ฒ E2E ํ ์คํธ๊ฐ ๋ฌด์์ธ์ง ์ดํด๋ณด๊ฒ ์ต๋๋ค.
ํํ E2E (End to End) ํ ์คํธ๋ ๋ธ๋ผ์ฐ์ ํ๊ฒฝ์์ ์ ์ ๊ฐ ์ฌ์ฉํ๊ณ ์๋ค๋ ๊ฐ์ ํ์
์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ฒด์ ์ธ ๋์์ ๊ฒ์ฆํ๋ ๊ฒ์ ๋งํฉ๋๋ค.
์ ๋ ํ ์คํธ์ ํตํฉ ํ ์คํธ์์๋ ๊ฐ ๋ชจ๋์ ๋ฌด๊ฒฐ์ฑ์ ๊ฒ์ฆํ ์ ์๋ค๋ฉด,
E2E ์์๋ ์ด๋ฌํ ๋ชจ๋๋ค์ด ์ ์์ฌ์ ํ์ํ ๊ธฐ๋ฅ์ ๋ฌธ์ ์์ด ์ ๊ณตํ๋์ง ๊ฒ์ฆํ ์ ์์ต๋๋ค.
๋์ ๊ทธ๋งํผ ๋ค์ํ ์๋๋ฆฌ์ค๋ฅผ ๊ฒ์ฆํ๊ธฐ ๋๋ฌธ์ ๋ค๋ฅธ ๋ฐฉ๋ฒ๋ค์ ๋นํด ํ ์คํธ ๋น์ฉ์ด ๋น์ผ ๊ฒ๋ ์ฌ์ค์ ๋๋ค.
ํ์ง๋ง ํญ๊ณต์น๊ณผ ๊ฐ์ด ๋ค์ํ ๋์ ์ ๊ฒ์ฆํด์ผํ๋ ๊ฒฝ์ฐ 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 ๋ฅผ ํตํด ์ฑ๊ณต ๋ฐ ์คํจ ์ผ์ด์ค ํ ์คํธ์ ํ์ํ 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 ๋ช ๋ฑ์ ์์ฑ ์ ํ์๋ค์ ์กฐํฉํด์ ์ฌ์ฉํ์ต๋๋ค.
์ฃผ์ํ ์ ์, styled-component ๋ฑ์ ์ด์ฉํด์ ์์ฑํ ๋ด๋ถ ์ปดํฌ๋ํธ์ ๊ฒฝ์ฐ์๋
className ์ด production build ์์ ์ต์ ํ ๊ณผ์ ์์ ๋ณ๊ฒฝ๋๊ธฐ ๋๋ฌธ์ ์ฌ์ฉํ์ง ์๋ ๊ฒ์ด ์ข์ต๋๋ค.
element select ์ best practice ๋ ๊ณต์๋ฌธ์์ ๋์์์ต๋๋ค.
Best Practices | Cypress Documentation
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,
},
}
๐ ๋ณ๋ ฌ ํ ์คํธ
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 ์ ๋ชจ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ง๋ ์์ต๋๋ค.)
์๋๋ ๊ณต์ ํ๋ฌ๊ทธ์ธ์ผ๋ก ๋ค์ด๊ฐ๋ ค๊ณ ํ ์คํธ ๋จ๊ณ์ ๋ค์ด๊ฐ๋๋ฐ,
์ค ์ฌ์ฉ์๊ฐ ๋ง์ด ์์์ ๊ทธ๋ฐ์ง ์ถํ ์ง์์ผ๋ก ๋ณ๊ฒฝ๋์๋๋ผ๊ตฌ์.
โญ๏ธ ์ ์ฉ ๋ฐฉ๋ฒ
> 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 ํ ์คํธ๋ฅผ ํตํด์ ๋ฐ๋ณต๋๋ ์์ ์ผ๋ก ์ธํ ํธ๋ฆฌํ ๋น์ฆํ๋ก ํธ ๊ฐ๋ฐ์๋ค์ ํผ๋ก๋๊ฐ ์กฐ๊ธ์ด๋๋ง ์ค์ด๋ค์์ผ๋ฉด ํ๋ ํฌ๋ง๊ณผ ํจ๊ป ์ด๋ฒ ํฌ์คํธ๋ฅผ ๋ง์น๊ฒ ์ต๋๋ค.
๊ธด ๊ธ ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค.๐โ๏ธ
'๐จโ๐ป web.dev > fe' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Storybook ์์ ์ปดํฌ๋ํธ ๋จ์ ํ ์คํธํ๊ธฐ (0) | 2023.06.24 |
---|---|
React Framer Motion ํบ์๋ณด๊ธฐ (0) | 2023.03.27 |
Next.js styled-component ์ค์ ํ๊ธฐ (SSR FOUC ์ด์) (0) | 2022.11.27 |
AWS SDK ๋ก s3 ํ์ผ ์ฌ์ด์ฆ ์กฐํํ๊ธฐ (0) | 2022.11.26 |
React ์ด๋ฒคํธ ์ฒ๋ฆฌ๋ฐฉ์๊ณผ SyntheticEvent (0) | 2022.10.17 |
๐ฌ ๋๊ธ