๐Ÿ‘‡ ์ด ๊ฐ€์ด๋“œ๊ฐ€ ๋‹น์‹ ์˜ ํ…Œ์ŠคํŠธ ๊ธฐ์ˆ ์„ ํ•œ ๋‹จ๊ณ„ ๋Œ์–ด ์˜ฌ๋ฆฌ๋Š” ์ด์œ 

May 9, 2022 ยท View on GitHub


๐Ÿ‘‡ ์ด ๊ฐ€์ด๋“œ๊ฐ€ ๋‹น์‹ ์˜ ํ…Œ์ŠคํŠธ ๊ธฐ์ˆ ์„ ํ•œ ๋‹จ๊ณ„ ๋Œ์–ด ์˜ฌ๋ฆฌ๋Š” ์ด์œ 


๐Ÿ“— ์ฒ ์ €ํ•˜๊ณ  ๋งค์šฐ ํฌ๊ด„์ ์ธ 45๊ฐ€์ง€ ์ด์ƒ์˜ ๋ชจ๋ฒ” ์‚ฌ๋ก€

JavaScript ๋ฐ Node.js์— ๋Œ€ํ•œ A๋ถ€ํ„ฐ Z๊นŒ์ง€์˜ ๋ฏฟ์Œ์งํ•œ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. ์ˆ˜์‹ญ ๊ฐ€์ง€ ์ตœ๊ณ ์˜ ๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๋ฌผ, ์„œ์  ๋ฐ ๋„๊ตฌ๋ฅผ ์š”์•ฝํ•˜๊ณ  ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๐Ÿšข ๊ธฐ์ดˆ๋ฅผ ๋›ฐ์–ด๋„˜์–ด ๊ณ ๊ธ‰์œผ๋กœ

์šด์˜์ค‘์ธ ์ œํ’ˆ์˜ ํ…Œ์ŠคํŠธ, ๋Œ์—ฐ๋ณ€์ด ํ…Œ์ŠคํŠธ, ์†์„ฑ ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ ๋ฐ ๊ธฐํƒ€ ์—ฌ๋Ÿฌ ์ „๋žต์  & ์ „๋ฌธ ๋„๊ตฌ์™€ ๊ฐ™์€ ๊ณ ๊ธ‰ ์ฃผ์ œ๋กœ ๋„˜์–ด๊ฐ€๋Š” ์—ฌ์ •์„ ๊ฒฝํ—˜ํ•˜์‹ญ์‹œ์˜ค. ์ด ๊ฐ€์ด๋“œ์˜ ๋ชจ๋“  ๋‹จ์–ด๋ฅผ ์ฝ์œผ๋ฉด ๋‹น์‹ ์˜ ํ…Œ์ŠคํŠธ ๊ธฐ์ˆ ์ด ํ‰๊ท ๋ณด๋‹ค ๋†’์•„์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐ŸŒ Full-stack: ํ”„๋ก ํŠธ, ๋ฐฑ์—”๋“œ, CI, ๋ฌด์—‡์ด๋“ 

๋ชจ๋“  ์‘์šฉํ”„๋กœ๊ทธ๋žจ ๊ณ„์ธต์˜ ๊ธฐ์ดˆ๊ฐ€ ๋˜๋Š” ์œ ๋น„์ฟผํ„ฐ์Šค ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•์„ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์œผ๋กœ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์‹ญ์‹œ์˜ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ ํ”„๋ก ํŠธ์—”๋“œ/UI, ๋ฐฑ์—”๋“œ, CI ํ˜น์€ ์ด ๋ชจ๋“ ๊ฒƒ์„ ๊ณต๋ถ€ํ•˜์„ธ์š”.


Yoni Goldberg ์ž‘์„ฑ



๋ชฉ์ฐจ

์„น์…˜ 0: ํ™ฉ๊ธˆ๋ฅ 

๋ชจ๋“  ๋ชจ๋“  ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ์˜๊ฐ์„ ์ฃผ๋Š” ํ•˜๋‚˜์˜ ์กฐ์–ธ(ํ•˜๋‚˜์˜ ํŠน์ˆ˜ํ•œ ํ•ญ๋ชฉ)

์„น์…˜ 1: ํ…Œ์ŠคํŠธ ํ•ด๋ถ€

๊ธฐ์ดˆ - ๊น”๋”ํ•œ ํ…Œ์ŠคํŠธ ๊ตฌ์„ฑํ•˜๊ธฐ(12๊ฐœ)

์„น์…˜ 2: ๋ฐฑ์—”๋“œ

๋ฐฑ์—”๋“œ ๋ฐ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ…Œ์ŠคํŠธ ํšจ์œจ์ ์œผ๋กœ ์ž‘์„ฑํ•˜๊ธฐ(8๊ฐœ)

์„น์…˜ 3: ํ”„๋ก ํŠธ์—”๋“œ

์ปดํฌ๋„ŒํŠธ ๋ฐ E2E ํ…Œ์ŠคํŠธ๋ฅผ ํฌํ•จํ•œ ์›น UI์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ(11๊ฐœ)

์„น์…˜ 4: ํ…Œ์ŠคํŠธ ํšจ๊ณผ ์ธก์ •

๊ฐ์‹œ์ž๋ฅผ ๊ฐ์‹œํ•˜๊ธฐ - ํ…Œ์ŠคํŠธ ํ’ˆ์งˆ ์ธก์ •(4๊ฐœ)

์„น์…˜ 5: ์ง€์†์ ์ธ ํ†ตํ•ฉ

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์„ธ๊ณ„์—์„œ CI์— ๋Œ€ํ•œ ์ง€์นจ(9๊ฐœ)



์„น์…˜ 0๏ธโƒฃ: ํ™ฉ๊ธˆ๋ฅ 


โšช ๏ธ 0 ํ™ฉ๊ธˆ๋ฅ : ๋ฆฐ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์„ค๊ณ„

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ์ œํ’ˆ ์ฝ”๋“œ์™€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ๋‹จ์ˆœํ•˜๊ณ , ์งง๊ณ , ์ถ”์ƒํ™”๊ฐ€ ์—†๊ณ , ๋ฌด๋‚œํ•˜๊ณ , ์ž‘์—…ํ•˜๊ธฐ์— ํŽธ๋ฆฌํ•˜๊ณ , ๋ฆฐํ•˜๊ฒŒ ๋””์ž์ธ ํ•˜์‹ญ์‹œ์˜ค. ํ…Œ์ŠคํŠธ๋ฅผ ๋ณด๊ณ  ์ฆ‰์‹œ ์˜๋ฏธ๋ฅผ ์•Œ์•„์ฑŒ ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์šฐ๋ฆฌ ๋จธ๋ฆฌ์†์€ ์ œํ’ˆ ์ฝ”๋“œ๋กœ ๊ฐ€๋“ํ•˜๊ณ  ๋ถ€๊ฐ€์ ์ธ ๋ณต์žกํ•œ ๊ฒƒ๋“ค์„ ์ƒ๊ฐํ•  ์—ฌ์œ ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋˜ ๋‹ค๋ฅธ ์–ด๋ ค์šด ์ฝ”๋“œ๋ฅผ ์–ต์ง€๋กœ ์ƒ๊ฐํ•ด๋‚ด๋ ค๊ณ  ํ•œ๋‹ค๋ฉด, ํŒ€์˜ ์†๋„๋ฅผ ๋Šฆ์ถ”๊ฒŒ ๋˜์–ด ์šฐ๋ฆฌ๊ฐ€ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š” ์ด์œ ๊ฐ€ ๋ฌด์ƒ‰ํ•ด ์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ๋งŽ์€ ํŒ€๋“ค์ด ์ด๋Ÿฐ ์ด์œ ๋ฅผ ํ…Œ์ŠคํŠธ๋ฅผ ํฌ๊ธฐํ•ฉ๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ๋Š” ์นœ์ ˆํ•˜๊ณ  ์›ƒ๋Š” ๋™๋ฃŒ์™€ ํ•จ๊ป˜ ์ผํ•˜๋Š” ๊ฒƒ์ด ์ฆ๊ฑฐ์šธ ์ˆ˜ ์žˆ๋Š” ๊ธฐํšŒ์ด๊ณ , ์ ์€ ํˆฌ์ž๋กœ ํฐ ๊ฐ€์น˜๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ณผํ•™์€ ์šฐ๋ฆฌ์—๊ฒŒ ๋‘ ๊ฐœ์˜ ๋‡Œ ์‹œ์Šคํ…œ์ด ์žˆ๋‹ค๊ณ  ๋งํ•ฉ๋‹ˆ๋‹ค. ๋นˆ ๋„๋กœ์—์„œ ์ž๋™์ฐจ๋ฅผ ์šด์ „ํ•˜๋Š” ๋“ฑ์˜ ๊ฐ„ํŽธํ•œ ํ™œ๋™์— ์‚ฌ์šฉ๋˜๋Š” ์‹œ์Šคํ…œ 1, ๊ทธ๋ฆฌ๊ณ  ์ˆ˜ํ•™ ๋ฐฉ์ •์‹์„ ํ‘ธ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์ด ๋ณต์žกํ•˜๊ณ  ์˜์‹์ ์ธ ์—ฐ์‚ฐ์„ ์œ„ํ•œ ์‹œ์Šคํ…œ 2. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋ณผ ๋•Œ ์ˆ˜ํ•™ ๋ฌธ์ œ๋ฅผ ํ‘ธ๋Š” ๊ฒƒ ๊ฐ™์€๊ฒŒ ์•„๋‹Œ, HTML ๋ฌธ์„œ๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ๋งŒ ํผ ์‰ฌ์›Œ์•ผํ•˜๋Š” ์‹œ์Šคํ…œ 1์— ๋งž๊ฒŒ ํ…Œ์ŠคํŠธ๋ฅผ ์„ค๊ณ„ํ•˜์‹ญ์‹œ์˜ค.

์„ ํƒ์ ์ธ ์ฒด๋ฆฌํ”ฝ ๊ธฐ์ˆ , ํˆด ๊ทธ๋ฆฌ๊ณ  ๋น„์šฉ-ํšจ์œจ์ ์ด๊ณ  ๋›ฐ์–ด๋‚œ ROI๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ…Œ์ŠคํŠธ ๋Œ€์ƒ ์„ ์ •์œผ๋กœ ์ด๋Ÿฌํ•œ ๋ชฉ์ ์„ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•„์š”ํ•œ ๋งŒํผ์˜ ํ…Œ์ŠคํŠธ, ์œตํ†ต์„ฑ ์žˆ๊ฒŒ ์œ ์ง€ํ•˜๋ ค๋Š” ๋…ธ๋ ฅ, ๋•Œ๋กœ๋Š” ์• ์ž์ผํ•จ๊ณผ ๋‹จ์ˆœ์„ฑ์„ ์œ„ํ•ด ์ผ๋ถ€ ํ…Œ์ŠคํŠธ์™€ ์‹ ๋ขฐ์„ฑ์„ ํฌ๊ธฐํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ€์น˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

alt text

์•„๋ž˜ ๋Œ€๋ถ€๋ถ„์˜ ์กฐ์–ธ์€ ์ด ์›์น™์˜ ํŒŒ์ƒ์ž…๋‹ˆ๋‹ค.

์‹œ์ž‘ํ•  ์ค€๋น„ ๋˜์…จ๋‚˜์š”?



์„น์…˜ 1: ํ…Œ์ŠคํŠธ ํ•ด๋ถ€


โšช ๏ธ 1.1 ๊ฐ ํ…Œ์ŠคํŠธ ์ด๋ฆ„์€ ์„ธ ๋ถ€๋ถ„์œผ๋กœ ๊ตฌ์„ฑ๋œ๋‹ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ํ…Œ์ŠคํŠธ๋Š” ํ˜„์žฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ฐœ์ •ํŒ์ด ์š”๊ตฌ ์‚ฌํ•ญ์„ ์ถฉ์กฑํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ์•Œ๋ ค์•ผํ•ฉ๋‹ˆ๋‹ค: ๋ฐฐํฌ๋ฅผ ํ•  ํ…Œ์Šคํ„ฐ, DevOps ์—”์ง€๋‹ˆ์–ด, 2๋…„ ํ›„์˜ ๋ฏธ๋ž˜์— ์ฝ”๋“œ๊ฐ€ ์ต์ˆ™ํ•˜์ง€ ์•Š์€ ์‚ฌ๋žŒ. ํ…Œ์ŠคํŠธ๊ฐ€ ์š”๊ตฌ ์‚ฌํ•ญ ์ˆ˜์ค€์—์„œ ์ž‘์„ฑ๋˜์–ด ์žˆ๊ณ  ์„ธ ๋ถ€๋ถ„์œผ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋‹ค๋ฉด, ๋ชฉ์ ์„ ์ด๋ฃฐ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

(1) ๋ฌด์—‡์„ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์žˆ๋Š”๊ฐ€? ์˜ˆ) ์ œํ’ˆ์„œ๋น„์Šค.์ƒˆ์ œํ’ˆ์ถ”๊ฐ€ ๋ฉ”์„œ๋“œ

(2) ์–ด๋–ค ์ƒํ™ฉ๊ณผ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ? ์˜ˆ) ๋ฉ”์„œ๋“œ์— ๊ฐ€๊ฒฉ์ด ์ „๋‹ฌ๋˜์ง€ ์•Š๋Š”๋‹ค.

(3) ์˜ˆ์ƒ๋˜๋Š” ๊ฒฐ๊ณผ๋Š” ๋ฌด์—‡์ธ๊ฐ€? ์˜ˆ) ์‹ ์ œํ’ˆ์€ ์Šน์ธ๋˜์ง€ ์•Š๋Š”๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ๋ฐฐํฌ์— ์‹คํŒจํ•˜์˜€๊ณ  "์ œํ’ˆ ์ถ”๊ฐ€" ๋ผ๋Š” ํ…Œ์ŠคํŠธ์— ์‹คํŒจํ•˜์˜€๋‹ค. ์ด๊ฒƒ์ด ์ •ํ™•ํžˆ ์–ด๋–ค ์˜ค์ž‘๋™ ์ธ์ง€๋ฅผ ์•Œ๋ ค์ฃผ๋‚˜์š”?


๐Ÿ‘‡ ์ฃผ์˜: ๊ฐ ๊ธ€์—๋Š” ์˜ˆ์ œ ์ฝ”๋“œ๊ฐ€ ์žˆ์œผ๋ฉฐ ๋•Œ๋กœ๋Š” ์ด๋ฏธ์ง€๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ํด๋ฆญํ•˜์—ฌ ํ™•์žฅ

โœ ์˜ˆ์ œ ์ฝ”๋“œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ์„ธ ๋ถ€๋ถ„์œผ๋กœ ๊ตฌ์„ฑ๋œ ํ…Œ์ŠคํŠธ ์ด๋ฆ„

//1. ๋‹จ์œ„ ํ…Œ์ŠคํŠธ
describe('์ œํ’ˆ ์„œ๋น„์Šค', function() {
  describe('์ƒˆ ์ œํ’ˆ ์ถ”๊ฐ€', function() {
    //2. ์‹œ๋‚˜๋ฆฌ์˜ค 3. ์˜ˆ์ƒ
    it('๊ฐ€๊ฒฉ์„ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ์ œํ’ˆ ์ƒํƒœ๋Š” ์Šน์ธ ๋Œ€๊ธฐ์ค‘์ด๋‹ค.', ()=> {
      const newProduct = new ProductService().add(...);
      expect(newProduct.status).to.equal('์Šน์ธ ๋Œ€๊ธฐ');
    });
  });
});

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ์„ธ ๋ถ€๋ถ„์œผ๋กœ ๊ตฌ์„ฑ๋œ ํ…Œ์ŠคํŠธ ์ด๋ฆ„

alt text



โšช ๏ธ 1.2 AAA ํŒจํ„ด์— ์˜ํ•œ ํ…Œ์ŠคํŠธ ๊ตฌ์กฐ

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: 3๊ฐœ์˜ ์ž˜ ์ž˜ ๊ตฌ๋ถ„๋œ ์„น์…˜ AAA(Arrange, Act, Assert)์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ๊ตฌ์„ฑํ•˜์‹ญ์‹œ์˜ค. ์ด ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ฅด๋ฉด ํ…Œ์ŠคํŠธ๋ฅผ ์‰ฝ๊ฒŒ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

์ฒซ๋ฒˆ์งธ A - Arrange(์ค€๋น„): ํ…Œ์ŠคํŠธ๊ฐ€ ๋ชฉํ‘œ๋กœ ํ•˜๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค์— ํ•„์š”ํ•œ ์‹œ์Šคํ…œ์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•œ ๋ชจ๋“  ์„ค์ • ์ฝ”๋“œ. ์—ฌ๊ธฐ์—๋Š” ํ…Œ์ŠคํŠธ ์ƒ์„ฑ์ž์˜ ๋‹จ์œ„ ์ธ์Šคํ„ด์Šคํ™”, DB ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€, ๊ฐ์ฒด์— ๋Œ€ํ•œ mock/stub ๋ฐ ๊ธฐํƒ€ ์ค€๋น„ ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‘๋ฒˆ์งธ A - Act(ํ–‰๋™): ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰. ์ผ๋ฐ˜์ ์œผ๋กœ ์ฝ”๋“œ ํ•œ์ค„

์„ธ๋ฒˆ์งธ A - Assert(์ฃผ์žฅ, ์˜ˆ์ƒ): ๋ฐ›์€ ์˜ˆ์ƒ๊ฐ’์ด ์ถฉ์กฑํ•˜๋Š”์ง€ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค. ์ผ๋ฐ˜์ ์œผ๋กœ ์ฝ”๋“œ ํ•œ์ค„


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ํ…Œ์ŠคํŠธ๋Š” ์˜ค๋Š˜ ์ผ์˜ ์•„์ฃผ ๋‹จ์ˆœํ•œ ๋ถ€๋ถ„์— ๋ถˆ๊ณผํ•˜์ง€๋งŒ, ๋ฉ”์ธ ์ฝ”๋“œ๋ฅผ ์ดํ•ดํ•˜๋Š”๋ฐ ๋งŽ์€ ์‹œ๊ฐ„์„ ๋‚ญ๋น„ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: AAA ํŒจํ„ด์œผ๋กœ ๊ตฌ์„ฑ๋œ ํ…Œ์ŠคํŠธ

describe('๊ณ ๊ฐ ๋ถ„๋ฅ˜๊ธฐ', () => {
    test('๊ณ ๊ฐ์ด 500๋‹ฌ๋Ÿฌ ์ด์ƒ์„ ์†Œ๋น„ํ•œ ๊ฒฝ์šฐ ํ”„๋ฆฌ๋ฏธ์—„์œผ๋กœ ๋ถ„๋ฅ˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', () => {
        //Arrange
        const customerToClassify = {spent:505, joined: new Date(), id:1}
        const DBStub = sinon.stub(dataAccess, "getCustomer")
            .reply({id:1, classification: 'regular'});

        //Act
        const receivedClassification = customerClassifier.classifyCustomer(customerToClassify);

        //Assert
        expect(receivedClassification).toMatch('premium');
    });
});

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: ๋ถ„๋ฆฌ ๋˜์–ด์žˆ์ง€ ์•Š๊ณ  ํ•œ ๋ฒŒ๋กœ ์ž‘์„ฑ๋˜์–ด ์žˆ์–ด ํ•ด์„ํ•˜๊ธฐ ์–ด๋ ต๋‹ค.

test('ํ”„๋ฆฌ๋ฏธ์—„์œผ๋กœ ๋ถ„๋ฅ˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', () => {
    const customerToClassify = {spent:505, joined: new Date(), id:1}
    const DBStub = sinon.stub(dataAccess, "getCustomer")
        .reply({id:1, classification: 'regular'});
    const receivedClassification = customerClassifier.classifyCustomer(customerToClassify);
    expect(receivedClassification).toMatch('premium');
});



โšช ๏ธ 1.3 ์ œํ’ˆ์˜ ์–ธ์–ด๋กœ ์˜ˆ์ƒ๊ฐ’์„ ์„ค๋ช…: BDD ์Šคํƒ€์ผ์˜ Assertion์„ ์‚ฌ์šฉ

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ํ…Œ์ŠคํŠธ๋ฅผ ์„ ์–ธ์  ์Šคํƒ€์ผ๋กœ ์ž‘์„ฑํ•˜๋ฉด ์ฝ๋Š” ์‚ฌ๋žŒ์ด ์ฆ‰์‹œ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์กฐ๊ฑด๋ถ€ ๋…ผ๋ฆฌ๋กœ ์ฑ„์›Œ์ง„ ๋ช…๋ นํ˜• ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•˜๋ฉด ํ…Œ์ŠคํŠธ๋ฅผ ์ฝ๊ธฐ๊ฐ€ ์‰ฝ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ์˜๋ฏธ์—์„œ ์ž„์˜์˜ ์‚ฌ์šฉ์ž ์ •์˜ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ๋ง๊ณ , ์„ ์–ธ์  BDD ์Šคํƒ€์ผ์˜ expect ๋˜๋Š” should๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ธ๊ฐ„๊ณผ ๊ฐ™์€ ์–ธ์–ด๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜์‹ญ์‹œ์˜ค. Chai & Jest์— ์›ํ•˜๋Š” Assertion์ด ํฌํ•จ๋˜์–ด ์žˆ์ง€ ์•Š๊ณ  ๋ฐ˜๋ณต์„ฑ์ด ๋†’์€ ๊ฒฝ์šฐ extending Jest matcher (Jest) ํ˜น์€ custom Chai plugin ์ž‘์„ฑ์„ ๊ณ ๋ คํ•˜์‹ญ์‹œ์˜ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ํŒ€์€ ํ…Œ์ŠคํŠธ๋ฅผ ๋œ ์ž‘์„ฑํ•˜๊ณ  ์„ฑ๊ฐ€์‹  ๊ฒƒ๋“ค์„ .skip() ์œผ๋กœ ์žฅ์‹ํ•ฉ๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: ์ฝ๋Š” ์‚ฌ๋žŒ์€ ํ…Œ์ŠคํŠธ ์Šคํ† ๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•ด ์งง์ง€์•Š์€ ๋ช…๋ นํ˜• ์ฝ”๋“œ๋ฅผ ํ›‘์–ด๋ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

test("๊ด€๋ฆฌ์ž ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด ์ •๋ ฌ๋œ ๊ด€๋ฆฌ์ž ๋ชฉ๋ก๋งŒ ๊ฒฐ๊ณผ์— ํฌํ•จ๋œ๋‹ค." , () => {
    // ์—ฌ๊ธฐ์— ๋‘ ๋ช…์˜ ๊ด€๋ฆฌ์ž "admin1", "admin2" ๋ฐ "user1" ์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค.
    const allAdmins = getUsers({adminOnly:true});
    const admin1Found, adming2Found = false;
    allAdmins.forEach(aSingleUser => {
        if(aSingleUser === "user1"){
            assert.notEqual(aSingleUser, "user1", "๊ด€๋ฆฌ์ž๊ฐ€ ์•„๋‹Œ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์•˜๋‹ค.");
        }
        if(aSingleUser==="admin1"){
            admin1Found = true;
        }
        if(aSingleUser==="admin2"){
            admin2Found = true;
        }
    });
    if(!admin1Found || !admin2Found ){
        throw new Error("๋ชจ๋“  ๊ด€๋ฆฌ์ž๊ฐ€ ๋ฐ˜ํ™˜๋˜์ง€ ์•Š์•˜๋‹ค.");
    }
});

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์„ ์–ธ์  ํ…Œ์ŠคํŠธ๋Š” ์ดํ•ดํ•˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค.

it("๊ด€๋ฆฌ์ž ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด ์ •๋ ฌ๋œ ๊ด€๋ฆฌ์ž ๋ชฉ๋ก๋งŒ ๊ฒฐ๊ณผ์— ํฌํ•จ๋œ๋‹ค." , () => {
    // ์—ฌ๊ธฐ์— ๋‘ ๋ช…์˜ ๊ด€๋ฆฌ์ž๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค.
    const allAdmins = getUsers({adminOnly:true});
    expect(allAdmins).to.include.ordered.members(["admin1" , "admin2"])
                     .but.not.include.ordered.members(["user1"]);
});



โšช ๏ธ 1.4 ๋ธ”๋ž™๋ฐ•์Šค ํ…Œ์ŠคํŠธ์— ์ถฉ์‹ค: public method๋งŒ ํ…Œ์ŠคํŠธ

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ๋‚ด๋ถ€ํ…Œ์ŠคํŠธ๋Š” ๊ฑฐ์˜ ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š๋Š” ์—„์ฒญ๋‚œ ์˜ค๋ฒ„ํ—ค๋“œ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋‹น์‹ ์˜ ์ฝ”๋“œ ํ˜น์€ API๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค๋ฉด, ๋‚ด๋ถ€์ ์œผ๋กœ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ–ˆ๋Š”์ง€์˜ ํ…Œ์ŠคํŠธ์— 3์‹œ๊ฐ„์„ ํˆฌ์žํ•ด์•ผ ํ•ฉ๋‹ˆ๊นŒ? ๊นจ์ง€๊ธฐ ์‰ฌ์šด ํ…Œ์ŠคํŠธ๋ฅผ ์œ ์ง€ํ•ด์•ผ ํ•ฉ๋‹ˆ๊นŒ? public method๊ฐ€ ์ž˜ ๋™์ž‘ํ•  ๋•Œ๋งˆ๋‹ค private method ๋˜ํ•œ ์•”์‹œ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ๊ฐ€ ๋˜๊ณ , ํŠน์ • ๋ฌธ์ œ(์˜ˆ. ์ž˜๋ชป๋œ ์ถœ๋ ฅ)๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ํ…Œ์ŠคํŠธ๊ฐ€ ๊นจ์ง‘๋‹ˆ๋‹ค. ์ด ์ ‘๊ทผ๋ฒ•์€ ํ–‰๋™ ํ…Œ์ŠคํŠธ๋ผ๊ณ ๋„ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ•œํŽธ์œผ๋กœ ๋‹น์‹ ์€ ๋‚ด๋ถ€ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด์•ผํ•ฉ๋‹ˆ๊นŒ?(ํ™”์ดํŠธ๋ฐ•์Šค ์ ‘๊ทผ) - ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ค๊ณ„ํ•˜๋Š” ๊ฒƒ์—์„œ ํ•ต์‹ฌ ์„ธ๋ถ€ ์‚ฌํ•ญ์œผ๋กœ ์ดˆ์ ์ด ์ด๋™ํ•˜๊ฑฐ๋‚˜ ์ž‘์€ ์ฝ”๋“œ์˜ ๋ฆฌํŽ™ํ† ๋ง์œผ๋กœ ์ธํ•ด ํ…Œ์ŠคํŠธ๊ฐ€ ์ค‘๋‹จ ๋  ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ฒฐ๊ณผ๋Š” ํ›Œ๋ฅญํ•ฉ๋‹ˆ๋‹ค. - ์ด๋Š” ์œ ์ง€๋ณด์ˆ˜ ๋ถ€๋‹ด์„ ํฌ๊ฒŒ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ๋‹น์‹ ์˜ ํ…Œ์ŠคํŠธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ์–‘์น˜๊ธฐ ์†Œ๋…„: ๋Š‘๋Œ€๊ฐ€ ๋‚˜ํƒ€๋‚ฌ๋‹ค!(์˜ˆ. private ๋ณ€์ˆ˜๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด ํ…Œ์ŠคํŠธ์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค). ๋‹น์—ฐํžˆ ์‚ฌ๋žŒ๋“ค์€, ์–ธ์  ๊ฐ€ ์ง„์งœ ๋ฒ„๊ทธ๊ฐ€ ๋ฌด์‹œ๋  ๋•Œ ๊นŒ์ง€ CI ์•Œ๋žŒ์„ ๋ฌด์‹œํ•˜๊ธฐ ์‹œ์ž‘ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค...


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์ด์œ ์—†์ด ๋‚ด๋ถ€๋ฅผ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.

class ProductService{
    // ์ด method ๋Š” ๋‚ด๋ถ€์—์„œ๋งŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
    // ์ด ์ด๋ฆ„์„ ๋ณ€๊ฒฝํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.
    calculateVAT(priceWithoutVAT){
        return {finalPrice: priceWithoutVAT * 1.2};
        // ๊ฒฐ๊ณผ ํ˜•์‹์ด๋‚˜ ํ‚ค ์ด๋ฆ„์„ ๋ณ€๊ฒฝํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.
    }
    // public method
    getPrice(productId){
        const desiredProduct= DB.getProduct(productId);
        finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice;
    }
}

it("ํ™”์ดํŠธ๋ฐ•์Šค ํ…Œ์ŠคํŠธ: ๋‚ด๋ถ€ method๊ฐ€ VAT 0์„ ๋ฐ›์œผ๋ฉด 0์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.", async () => {
    // ์‚ฌ์šฉ์ž๊ฐ€ VAT๋ฅผ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ์š”๊ตฌ์‚ฌํ•ญ์€ ์—†์œผ๋ฉฐ, ์ตœ์ข… ๊ฐ€๊ฒฉ๋งŒ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
    // ๊ทธ๋Ÿผ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์—ฌ๊ธฐ์—์„œ ๋‚ด๋ถ€ ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰
    expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0);
});



โšช ๏ธ 1.5 ์˜ฌ๋ฐ”๋ฅธ ํ…Œ์ŠคํŠธ ๋”๋ธ” ์„ ํƒ: Stub๊ณผ Spy๋ฅผ ์œ„ํ•œ Mock์„ ํ”ผํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ํ…Œ์ŠคํŠธ ๋”๋ธ”์€ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๊ธฐ๋•Œ๋ฌธ์— ํ•„์š”์•…์ด์ง€๋งŒ ์ผ๋ถ€๋Š” ์—„์ฒญ๋‚œ ๊ฐ€์น˜๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค(ํ…Œ์ŠคํŠธ ๋”๋ธ”์— ๋Œ€ํ•œ ์•Œ๋ฆผ: mocks vs stubs vs spies).

ํ…Œ์ŠคํŠธ ๋”๋ธ”์„ ์‚ฌ์šฉํ•˜๊ธฐ ์ „์— ๊ฐ„๋‹จํ•œ ์งˆ๋ฌธ: ์š”๊ตฌ์‚ฌํ•ญ ๋ฌธ์„œ์— ์žˆ๊ฑฐ๋‚˜ ์žˆ์„ ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐ ํ…Œ์ŠคํŠธ ๋”๋ธ”์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๊นŒ? ๋งŒ์•ฝ ์•„๋‹ˆ๋ผ๋ฉด ํ™”์ดํŠธ๋ฐ•์Šค ํ…Œ์ŠคํŠธ ๋‚Œ์ƒˆ๊ฐ€ ๋ณด์ž…๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ๊ฒฐ์ œ ์„œ๋น„์Šค๊ฐ€ ์ค‘๋‹จ๋˜์—ˆ์„ ๋•Œ ์•ฑ์ด ์ ์ ˆํ•˜๊ฒŒ ์ž‘๋™ํ•˜๋Š” ๊ฒƒ์„ ํ…Œ์ŠคํŠธํ•˜๋ ค๋Š” ๊ฒฝ์šฐ, ํ…Œ์ŠคํŠธ์ค‘์ธ ๋‹จ์œ„๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก, ๊ฒฐ์ œ ์„œ๋น„์Šค๋ฅผ stubํ•˜๊ณ  '์‘๋‹ต ์—†์Œ' ๋ฐ˜ํ™˜์„ ํŠธ๋ฆฌ๊ฑฐ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ํŠน์ • ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋™์ž‘/์‘๋‹ต/๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  spy๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๋‹น ์„œ๋น„์Šค๊ฐ€ ์ค‘๋‹จ๋˜์—ˆ์„ ๋•Œ ๋ฉ”์ผ์ด ๋ณด๋‚ด์ง€๋Š”์ง€๋ฅผ assert ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ๋‹ค์‹œ ์š”๊ตฌ์‚ฌํ•ญ ๋ฌธ์„œ์— ์žˆ์„ ์ˆ˜ ์žˆ๋Š” ํ–‰๋™์— ๋Œ€ํ•œ ์ ๊ฒ€์ž…๋‹ˆ๋‹ค(๊ฒฐ์ œ๊ฐ€ ์ €์žฅ๋˜์ง€ ์•Š์œผ๋ฉด ๋ฉ”์ผ์€ ๋ณด๋‚ธ๋‹ค). ๋ฐ˜๋Œ€๋กœ, ๊ฒฐ์ œ ์„œ๋น„์Šค๋ฅผ mock ํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅธ JavaScript ํƒ€์ž…์œผ๋กœ ํ˜ธ์ถœ ๋˜์—ˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•œ๋‹ค๋ฉด - ๋‹น์‹ ์˜ ํ…Œ์ŠคํŠธ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ธฐ๋Šฅ์— ์ „ํ˜€ ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š๊ณ  ์ž์ฃผ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ๋Š” ๋‚ด๋ถ€ ๊ตฌํ˜„์— ์ดˆ์ ์„ ๋‘” ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.

โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ์ฝ”๋“œ๋ฅผ ๋ฆฌํŽ™ํ† ๋ง ํ•  ๋•Œ, ๋ชจ๋“  mock์„ ์ฐพ์•„์„œ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๊ฐ€ ๋„์›€์ด ์•„๋‹Œ ๋ถ€๋‹ด์ด ๋ฉ๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: ๋‚ด๋ถ€์— ์ดˆ์ ์„ ๋‘” mock

it("์œ ํšจํ•œ ์ œํ’ˆ์„ ์‚ญ์ œํ•˜๋ ค๊ณ  ํ•  ๋•Œ, ์˜ฌ๋ฐ”๋ฅธ ์ œํ’ˆ๊ณผ ์˜ฌ๋ฐ”๋ฅธ ๊ตฌ์„ฑ ์ •๋ณด๋กœ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค DAL์„ ํ•œ ๋ฒˆ ํ˜ธ์ถœํ–ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค", async () => {
    // ์ด๋ฏธ ์ œํ’ˆ์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •
    const dataAccessMock = sinon.mock(DAL);
    // ์ข‹์ง€ ์•Š์Œ: ๋‚ด๋ถ€ ํ…Œ์ŠคํŠธ๋Š” side-effect๋ฅผ ์œ„ํ•ด์„œ๊ฐ€ ์ฃผ์š” ๋ชฉ์ ์„ ์œ„ํ•ด์„œ ์ž…๋‹ˆ๋‹ค.
    dataAccessMock.expects("deleteProduct").once().withArgs(DBConfig, theProductWeJustAdded, true, false);
    new ProductService().deletePrice(theProductWeJustAdded);
    dataAccessMock.verify();
});

:clap:์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: spy๋Š” ์š”๊ตฌ์‚ฌํ•ญ์„ ํ…Œ์ŠคํŠธํ•˜๋Š”๋ฐ ์ดˆ์ ์„ ๋‘๊ณ ์žˆ์ง€๋งŒ, ๋‚ด๋ถ€๋ฅผ ๊ฑด๋“œ๋ฆฌ๋Š” side-effect๋ฅผ ํ”ผํ•  ์ˆœ ์—†์Šต๋‹ˆ๋‹ค.

it("์œ ํšจํ•œ ์ œํ’ˆ์„ ์‚ญ์ œํ•˜๋ ค๊ณ  ํ•  ๋•Œ, ๋ฉ”์ผ์„ ๋ณด๋‚ธ๋‹ค", async () => {
    // ์ด๋ฏธ ์ œํ’ˆ์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •
    const spy = sinon.spy(Emailer.prototype, "sendEmail");
    new ProductService().deletePrice(theProductWeJustAdded);
    // ์ข‹์Œ: ์šฐ๋ฆฌ๋Š” ๋‚ด๋ถ€๋ฅผ ๋‹ค๋ฃจ๋Š”๊ฐ€? ๊ทธ๋ ‡๋‹ค, ๊ทธ๋Ÿฌ๋‚˜ ์š”๊ตฌ์‚ฌํ•ญ(์ด๋ฉ”์ผ์„ ๋ณด๋‚ธ๋‹ค)์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ์˜ side-effect์ด๋‹ค.
});



โšช ๏ธ 1.6 ์˜๋ฏธ์—†๋Š” ์ธํ’‹ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ๋ง๊ณ , ์‹ค์ œ์™€ ๊ฐ™์€ ์ธํ’‹ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด๋ผ

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ํ”ํžˆ ์ œํ’ˆ์˜ ๋ฒ„๊ทธ๋“ค์€ ๋งค์šฐ ํŠน์ˆ˜ํ•œ ์ธํ’‹๋ฐ์ดํ„ฐ๋ฅผ ํ†ตํ•ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค - ํ…Œ์ŠคํŠธ ์ธํ’‹์ด ํ˜ˆ์‹ค์ ์ผ ์ˆ˜๋ก ๋ฒ„๊ทธ๋ฅผ ์กฐ๊ธฐ์— ๋ฐœ๊ฒฌํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„์ง‘๋‹ˆ๋‹ค. ์‹ค์ œ ๋ฐ์ดํ„ฐ์™€ ๋‹ค์–‘์„ฑ ๋ฐ ํ˜•ํƒœ๊ฐ€ ์œ ์‚ฌํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•ด ์ฃผ๋Š” Faker ๊ฐ™์€ ์ „์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ์ด๋Ÿฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์€ ์‹ค์ œ๊ฐ™์€ ์ „ํ™”๋ฒˆํ˜ธ, ์‚ฌ์šฉ์ž ์ด๋ฆ„, ์‹ ์šฉ์นด๋“œ, ํšŒ์‚ฌ๋ช… ๊ทธ๋ฆฌ๊ณ  ์‹ฌ์ง€์–ด 'lorem ipsum'๊ฐ™์€ ๋ฌธ์ž๋“ฑ์„ ์ƒ์„ฑํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹น์‹ ์€ ๊ฐ€์ƒ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ(๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์œ„์—์„œ)๋ฅผ ๋ฌด์ž‘์œ„ํ™” ํ•˜๊ฑฐ๋‚˜ ์‹ฌ์ง€์–ด ์‹ค์ œ ํ™˜๊ฒฝ์œผ๋กœ๋ถ€ํ„ฐ์˜ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์ž„ํฌํŠธ ํ• ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ์–ป๊ธฐ๋ฅผ ์›ํ•˜์‹ญ๋‹ˆ๊นŒ? ๊ทธ๋ ‡๋‹ค๋ฉด ์•„๋ž˜๋กœ ๊ฐ€์‹ญ์‹œ์˜ค (property-based testing).


โŒ ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด: "Foo"์™€ ๊ฐ™์€ ์ธํ’‹์„ ์‚ฌ์šฉํ•˜๋ฉด ๋‹น์‹ ์˜ ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ๋ชจ๋‘ ํ†ต๊ณผํ•œ๊ฒƒ ์ฒ˜๋Ÿผ ํ‘œ์‹œ๋˜์ง€๋งŒ, ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ๋Š” ํ•ด์ปค๊ฐ€ โ€œ@3e2ddsf . ##โ€™ 1 fdsfds . fds432 AAAAโ€ ๊ฐ™์€ ์ธํ’‹์„ ์ „๋‹ฌํ•ด ์‹คํŒจ ํ• ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: ํ˜„์‹ค์ ์ด์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ ๋•Œ๋ฌธ์— ํ†ต๊ณผํ•˜๋Š” ํ…Œ์ŠคํŠธ

const addProduct = (name, price) =>{
  const productNameRegexNoSpace = /^\S*$/;// ๊ณต๋ฐฑ์€ ํ—ˆ์šฉ๋˜์ง€ ์•Š์Œ

  if(!productNameRegexNoSpace.test(name))
    return false;//๋„๋‹ฌํ•˜์ง€ ์•Š๋Š” ๊ณณ

    //some logic here
    return true;
};

test("์ž˜๋ชป๋œ ์˜ˆ์ œ: ์œ ํšจํ•œ ์†์„ฑ๊ณผ ํ•จ๊ป˜ ์ œํ’ˆ์„ ์ถ”๊ฐ€ํ•œ๋‹ค๋ฉด, ์„ฑ๊ณต์„ ์–ป๋Š”๋‹ค.", async () => {
    //๋ชจ๋“  ํ…Œ์ŠคํŠธ์—์„œ false ๊ฐ€ ๋ฆฌํ„ด๋˜์ง€ ์•Š๋Š” "Foo" ์ธํ’‹์„ ์‚ฌ์šฉ
    const addProductResult = addProduct("Foo", 5);
    expect(addProductResult).toBe(true);
    //๊ฑฐ์ง“๋œ ์„ฑ๊ณต: ๊ณต๋ฐฑ์„ ํฌํ•จํ•˜๋Š” ๋ฌธ์ž์—ด์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ๋Š” ์„ฑ๊ณตํ•œ๋‹ค.
});

:clap:์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ๋ฌด์ž‘์œ„ํ•œ ํ˜„์‹ค์ ์ธ ์ธํ’‹

it("๋” ๋‚˜์€ ๊ฒƒ: ์œ ํšจํ•œ ์ œํ’ˆ์ด ์ถ”๊ฐ€๋œ๋‹ค๋ฉด, ์„ฑ๊ณต์„ ์–ป๋Š”๋‹ค.", async () => {
    const addProductResult = addProduct(faker.commerce.productName(), faker.random.number());
    //์ƒ์„ฑ๋œ ๋ฌด์ž‘์œ„ ์ธํ’‹: {'Sleek Cotton Computer',  85481}
    expect(addProductResult).to.be.true;
    //ํ…Œ์ŠคํŠธ๋Š” ์‹คํŒจํ•œ๋‹ค, ๋ฌด์ž‘์œ„ ์ธํ’‹์€ ์šฐ๋ฆฌ๊ฐ€ ๊ณ„ํšํ•˜์ง€ ์•Š์€ ์ผ์ด ์ผ์–ด๋‚˜๋„๋ก ๋งŒ๋“ ๋‹ค.
    //์šฐ๋ฆฌ๋Š” ์กฐ๊ธฐ์— ๋ฒ„๊ทธ๋ฅผ ๋ฐœ๊ฒฌํ–ˆ๋‹ค!
});



โšช ๏ธ 1.7 ย ํ”„๋กœํผํ‹ฐ ๊ธฐ๋ฐ˜(Property-based) ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ๋‹ค์–‘ํ•œ ์ธํ’‹ ๊ฐ’ ์กฐํ•ฉ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์šฐ๋ฆฌ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์ ์€ ์ˆ˜์˜ ์ธํ’‹ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ํ•ฉ๋‹ˆ๋‹ค. ์‹ฌ์ง€์–ด ์ธํ’‹ ๋ฐ์ดํ„ฐ ํ˜•์‹์ด ์‹ค์ œ ๋ฐ์ดํ„ฐ์™€ ๋น„์Šทํ•  ๋•Œ์—๋„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ œํ•œ๋œ ์ธํ’‹ ์กฐํ•ฉ์œผ๋กœ๋งŒ ํ…Œ์ŠคํŠธ๋ฅผ ์ปค๋ฒ„ํ•ฉ๋‹ˆ๋‹ค.(method(โ€˜โ€™, true, 1), method(โ€œstringโ€ , falseโ€ , 0)) ํ•˜์ง€๋งŒ, ์šด์˜์‹œ์—๋Š” 5๊ฐœ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๊ฐ€์ง€๋Š” API๋Š” ์ˆ˜ ์ฒœ ๊ฐœ์˜ ๋‹ค๋ฅธ ์กฐํ•ฉ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ˜ธ์ถœ ๋  ์ˆ˜ ์žˆ๊ณ , ์ด ์ค‘ ํ•˜๋‚˜๊ฐ€ ์šฐ๋ฆฌ์˜ ์‹œ์Šคํ…œ์„ ๋‹ค์šด์‹œํ‚ฌ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ๋งŒ์•ฝ 1000 ๊ฐ€์ง€ ์กฐํ•ฉ์˜ ์ธํ’‹๊ฐ’์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅธ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜์ง€ ๋ชปํ•˜๋Š” ์ธํ’‹๊ฐ’์„ ์ฐพ์•„๋‚ด๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ์–ด๋–จ๊นŒ์š”? ํ”„๋กœํผํ‹ฐ ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์— ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ์ธํ’‹ ์กฐํ•ฉ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ๊ฐํ•˜์ง€ ๋ชป ํ•œ ๋ฒ„๊ทธ๋ฅผ ์ฐพ์„ ํ™•๋ฅ ์„ ๋†’์—ฌ์ค๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ๋“ค์–ด, ๋‹ค์Œ์˜ ๋ฉ”์†Œ๋“œ๊ฐ€ ์ฃผ์–ด์กŒ์„ ๋•Œ โ€”โ€ŠaddNewProduct(id, name, isDiscount)โ€Šโ€” ํ”„๋กœํผํ‹ฐ ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์€ ๋‹ค์–‘ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ (number, string, boolean) ์กฐํ•ฉ์œผ๋กœ - (1, โ€œiPhoneโ€, false), (2, โ€œGalaxyโ€, true) - ์ด ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. js-verify ๋‚˜ testcheck (much better documentation) ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ง€์›ํ•˜๋Š” ํ…Œ์ŠคํŠธ ๋Ÿฌ๋„ˆ๋“ค (Mocha, Jest, etc) ์ค‘ ๋‹น์‹ ์ด ๊ฐ€์žฅ ์„ ํ˜ธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ํ†ตํ•ด ํ”„๋กœํผํ‹ฐ ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—…๋ฐ์ดํŠธ : Nicolas Dubien๊ฐ€ ์ฝ”๋ฉ˜ํŠธ๋ฅผ ํ†ตํ•ด ๋” ๋งŽ์€ ๋ถ€๊ฐ€์ ์ธ ๊ธฐ๋Šฅ๋“ค์„ ์ œ๊ณตํ•˜๊ณ  ํ™œ๋ฐœํ•˜๊ฒŒ ์œ ์ง€๋ณด์ˆ˜๋˜๊ณ  ์žˆ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ fast-check๋ฅผ ์ถ”์ฒœํ•ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ์˜์‹ฌํ•  ์—ฌ์ง€ ์—†์ด ๋‹น์‹ ์€ ์˜ค์ง ์ฝ”๋“œ๊ฐ€ ์ž˜ ๋™์ž‘ํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ธํ’‹์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ถˆํ–‰ํ•˜๊ฒŒ๋„ ์ด๋Ÿฌํ•œ ๋ฐฉ์‹์€ ๋ฒ„๊ทธ๋ฅผ ์ฐพ๋Š” ๋„๊ตฌ๋กœ์จ์˜ ํ…Œ์ŠคํŠธ ํšจ์œจ์„ฑ์„ ๋–จ์–ด๋œจ๋ฆด ๊ฒƒ ์ž…๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: โ€œfast-checkโ€๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์–‘ํ•œ ์ธํ’‹ ์กฐํ•ฉ์œผ๋กœ ํ…Œ์ŠคํŠธ ํ•˜์‹ญ์‹œ์˜ค.

import fc from "fast-check";

describe("Product service", () => {
  describe("Adding new", () => {
    //์„œ๋กœ ๋‹ค๋ฅธ ๋ฌด์ž‘์œ„ ๊ฐ’์œผ๋กœ 100ํšŒ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
    it("Add new product with random yet valid properties, always successful", () =>
      fc.assert(
        fc.property(fc.integer(), fc.string(), (id, name) => {
          expect(addNewProduct(id, name).status).toEqual("approved");
        })
      ));
  });
});



โšช ๏ธ 1.8 ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์งง๊ฑฐ๋‚˜ ์ธ๋ผ์ธ ์Šค๋ƒ…์ƒท๋งŒ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์™ธ๋ถ€ ํŒŒ์ผ์ด ์•„๋‹Œ ํ…Œ์ŠคํŠธ์˜ ์ผ๋ถ€ (์ธ๋ผ์ธ ์Šค๋ƒ…์ƒท)์— ํฌํ•จ ๋œ ์งง๊ณ  ์ง‘์ค‘๋œ ์Šค๋ƒ…์ƒท(3~7 ๋ผ์ธ)๋งŒ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ์ด ์ง€์นจ์„ ๋”ฐ๋ฅด๋ฉด ๋”ฐ๋กœ ์„ค๋ช…์ด ํ•„์š”์—†๊ณ  ์ž˜ ๊นจ์ง€์ง€ ์•Š๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

๋ฐ˜๋ฉด์—, '๊ณ ์ „์ ์ธ ์Šค๋ƒ…์ƒท' ํŠœํ† ๋ฆฌ์–ผ ๋ฐ ๋„๊ตฌ๋Š” ์™ธ๋ถ€์— ํฐ ํŒŒ์ผ(์˜ˆ: ๊ตฌ์„ฑ ์š”์†Œ ๋žœ๋”๋ง ๋งˆํฌ์—…, API JSON ๊ฒฐ๊ณผ)๋ฅผ ์ €์žฅํ•˜๊ณ , ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•  ๋•Œ ๋งˆ๋‹ค ์ˆ˜์‹ ๋œ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅ๋œ ๋ฒ„์ „๊ณผ ๋น„๊ตํ•˜๊ธฐ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ด๊ฒƒ์€ 1,000 ๋ผ์ธ(์šฐ๋ฆฌ๊ฐ€ ์ ˆ๋Œ€ ์ฝ์ง€ ์•Š๊ณ  ์ถ”๋ก ํ•˜์ง€ ์•Š์„ 3,000๊ฐœ์˜ ๋ฐ์ดํ„ฐ ๊ฐ’์„ ๊ฐ€์ง„)์˜ ์ฝ”๋“œ๋ฅผ ์šฐ๋ฆฌ ํ…Œ์ŠคํŠธ์— ์•”์‹œ์ ์œผ๋กœ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์™œ ์ด๊ฒƒ์ด ์ž˜๋ชป ๋˜์—ˆ์„๊นŒ์š”? ์ด๋ ‡๊ฒŒํ•˜๋ฉด ํ…Œ์ŠคํŠธ์— ์‹คํŒจํ•  1,000 ๊ฐ€์ง€ ์ด์œ ๊ฐ€ ์ƒ๊น๋‹ˆ๋‹ค. ํ•œ์ค„๋งŒ ๋ณ€๊ฒฝ๋˜์–ด๋„ ์Šค๋ƒ…์ƒท์ด ์œ ํšจํ•˜์ง€ ์•Š๊ฒŒ ๋˜๊ณ , ์ด๋Ÿฐ์ผ์ด ์ผ์–ด๋‚  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค. ์–ผ๋งˆ๋‚˜ ์ž์ฃผ? ๋ชจ๋“  ๊ณต๋ฐฑ, ์ฃผ์„์—์„œ ํ˜น์€ ์‚ฌ์†Œํ•œ CSS/HTML ๋ณ€๊ฒฝ์— ๋Œ€ํ•ด์„œ. ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ํ…Œ์ŠคํŠธ ์ด๋ฆ„์€ 1,000 ๋ผ์ธ์ด ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์•˜๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๊ธฐ ๋•Œ๋ถ„์—, ์‹คํŒจ์— ๋Œ€ํ•œ ๋‹จ์„œ๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์ž๊ฐ€ ๊ธด ๋ฌธ์„œ(๊ฒ€์‚ฌํ•˜๊ณ  ํ™•์ธํ•  ์ˆ˜ ์—†๋Š”)๋ฅผ ๋ฐ›์•„๋“ค์ด๊ฒŒ๋” ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ชจ๋“  ๊ฒƒ์€ ์ดˆ์ ์ด ๋งž์ง€์•Š๊ณ  ๋„ˆ๋ฌด ๋งŽ์€ ๊ฒƒ์„ ๋‹ฌ์„ฑํ•˜๋ ค๋Š” ๋ชจํ˜ธํ•˜๊ณ  ๊ฐ„์ ˆํ•œ ํ…Œ์ŠคํŠธ ์ฆ์ƒ์ž…๋‹ˆ๋‹ค.

๊ธด ์™ธ๋ถ€ ์Šค๋ƒ…์ƒท์ด ํ—ˆ์šฉ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๊ฑฐ์˜ ์—†๋‹ค๋Š” ์ ์€ ์ฃผ๋ชฉํ•  ๊ฐ€์น˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค - ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋‹Œ ์Šคํ‚ค๋งˆ๋ฅผ assert ํ•  ๋•Œ(๊ฐ’ ์ถ”์ถœ ๋ฐ ํ•„๋“œ์— ์ง‘์ค‘) ๋˜๋Š” ์ˆ˜์‹ ๋œ ๋ฌธ์„œ๊ฐ€ ๊ฑฐ์˜ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ


โŒ ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด: UI ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ์ฝ”๋“œ๊ฐ€ ๋ฌธ์ œ์—†์–ด ๋ณด์ด๊ณ  ํ™”๋ฉด์ด ์™„๋ฒฝํ•œ ํ”ฝ์…€์„ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. ์–ด๋–ป๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๊นŒ? ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ์—์„œ ์›๋ณธ ๋ฌธ์„œ์™€ ํ˜„์žฌ ์ˆ˜์‹ ๋œ ๋ฌธ์„œ์™€์˜ ์ฐจ์ด์ ์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๋นˆ์นธ ํ•˜๋‚˜๊ฐ€ ๋งˆํฌ ๋‹ค์šด์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค...


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: ๋ณด์ด์ง€ ์•Š๋Š” 2,000 ๋ผ์ธ์˜ ์ฝ”๋“œ๋ฅผ ์šฐ๋ฆฌ ํ…Œ์ŠคํŠธ์— ์—ฐ๊ฒฐ

it("TestJavaScript.com ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋žœ๋”๋ง ๋œ๋‹ค.", () => {
  //Arrange

  //Act
  const receivedPage = renderer
    .create(<DisplayPage page="http://www.testjavascript.com"> Test JavaScript </DisplayPage>)
    .toJSON();

  //Assert
  expect(receivedPage).toMatchSnapshot();
  // ์ด์ œ 2,000 ๋ผ์ธ์˜ ๋ฌธ์„œ๋ฅผ ์•”๋ฌต์ ์œผ๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  // ๋ชจ๋“  ์ค„๋ฐ”๊ฟˆ ๋˜๋Š” ์ฃผ์„์ด ํ…Œ์ŠคํŠธ๋ฅผ ๋ง๊ฐ€๋œจ๋ฆฝ๋‹ˆ๋‹ค.
});

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: expectation์ด ์ž˜ ๋ณด์ด๊ณ  ์ง‘์ค‘๋œ๋‹ค.

it("TestJavaScript.com ํ™ˆํŽ˜์ด์ง€๋ฅผ ๋ฐฉ๋ฌธํ•˜๋ฉด, ๋ฉ”๋‰ด๊ฐ€ ๋ณด์ธ๋‹ค.", () => {
  //Arrange

  //Act
  const receivedPage = renderer
    .create(<DisplayPage page="http://www.testjavascript.com"> Test JavaScript </DisplayPage>)
    .toJSON();

  //Assert

  const menu = receivedPage.content.menu;
  expect(menu).toMatchInlineSnapshot(`
<ul>
<li>Home</li>
<li> About </li>
<li> Contact </li>
</ul>
`);
});



โšช ๏ธ 1.9 ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธ€๋กœ๋ฒŒ๋กœ ํ•˜์ง€๋ง๊ณ  ํ…Œ์ŠคํŠธ๋ณ„๋กœ ๋”ฐ๋กœ ์ถ”๊ฐ€ํ•˜๋ผ.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ํ™ฉ๊ธˆ๋ฅ ์— ๋”ฐ๋ฅด๋ฉด(์„น์…˜ 0), ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ์ปคํ”Œ๋ง์„ ๋ฐฉ์ง€ํ•˜๊ณ  ํ…Œ์ŠคํŠธ ํ๋ฆ„์„ ์‰ฝ๊ฒŒ ์ถ”๋ก ํ•˜๊ธฐ ์œ„ํ•ด ์ž์ฒด DB ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ(ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์ „์— DB ๋ฐ์ดํ„ฐ๋ฅผ ์ค€๋น„('ํ…Œ์ŠคํŠธ ํ”ฝ์Šค์ณ'๋ผ๊ณ ๋„ ํ•ฉ๋‹ˆ๋‹ค))์„ ์œ„ํ•ด ์ด๋ฅผ ์œ„๋ฐ˜ํ•˜๋Š” ํ…Œ์Šคํ„ฐ๋“ค์ด ๋งŽ์Šต๋‹ˆ๋‹ค. ์„ฑ๋Šฅ์€ ์‹ค์ œ๋กœ ์œ ํšจํ•œ ๋ฌธ์ œ์ด์ง€๋งŒ ์™„ํ™”๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(2.2 ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ ์ฐธ๊ณ ). ๊ทธ๋Ÿฌ๋‚˜ ํ…Œ์ŠคํŠธ ๋ณต์žก์„ฑ์€ ๋Œ€๋ถ€๋ถ„์˜ ๋‹ค๋ฅธ ๊ณ ๋ ค์‚ฌํ•ญ๋“ค์„ ํ†ต์ œํ•ด์•ผ ํ•˜๋Š” ๊ณ ํ†ต์„ ์ˆ˜๋ฐ˜ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ํ…Œ์ŠคํŠธ์— ํ•„์š”ํ•œ DB ๋ ˆ์ฝ”๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ถ”๊ฐ€ํ•˜๊ณ , ํ•ด๋‹น ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด์„œ๋งŒ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜์‹ญ์‹œ์˜ค. ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•œ ๋ฌธ์ œ๊ฐ€ ๋˜๋Š” ๊ฒฝ์šฐ - ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š” ํ…Œ์ŠคํŠธ ๋ชจ์Œ(์˜ˆ: ์ฟผ๋ฆฌ)์— ๋Œ€ํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ค€๋น„ํ•˜๋Š” ํ˜•ํƒœ๋กœ ํƒ€ํ˜‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ํ…Œ์ŠคํŠธ ์‹คํŒจ, ๋ฐฐํฌ ์ค‘๋‹จ์œผ๋กœ ํŒ€์›๋“ค์ด ๊ท€์ค‘ํ•œ ์‹œ๊ฐ„์„ ์†Œ๋น„ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ฒ„๊ทธ๊ฐ€ ์žˆ์Šต๋‹ˆ๊นŒ? ์กฐ์‚ฌํ•ด๋ณด๋‹ˆ '์—†์Šต๋‹ˆ๋‹ค' - ๋‘ ํ…Œ์ŠคํŠธ์—์„œ ๋™์ผํ•œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒจ์•ˆ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์ด์ง€ ์•Š์œผ๋ฉฐ ๊ธ€๋กœ๋ฒŒ ํ›…์— ์˜ํ•œ DB ๋ฐ์ดํ„ฐ์— ์˜์กด

before(() => {
  // ์‚ฌ์ดํŠธ ๋ฐ ๊ด€๋ฆฌ์ž ๋ฐ์ดํ„ฐ๋ฅผ DB์— ์ถ”๊ฐ€. ๋ฐ์ดํ„ฐ๋Š” ์–ด๋””์— ์žˆ์Šต๋‹ˆ๊นŒ? ์™ธ๋ถ€์—. ์™ธ๋ถ€ JSON ๋˜๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ”„๋ ˆ์ž„์›Œํฌ์—
  await DB.AddSeedDataFromJson('seed.json');
});
it("์‚ฌ์ดํŠธ ์ด๋ฆ„์„ ์—…๋ฐ์ดํŠธ ํ•  ๋•Œ, ์„ฑ๊ณต์„ ํ™•์ธํ•œ๋‹ค.", async () => {
  // ์‚ฌ์ดํŠธ ์ด๋ฆ„ "portal"์ด ์กด์žฌํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ณ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹œ๋“œํŒŒ์ผ์—์„œ ๋ดค์Šต๋‹ˆ๋‹ค.
  const siteToUpdate = await SiteService.getSiteByName("Portal");
  const updateNameResult = await SiteService.changeName(siteToUpdate, "newName");
  expect(updateNameResult).to.be(true);
});
it("์‚ฌ์ดํŠธ ์ด๋ฆ„์„ ์ฟผ๋ฆฌํ•  ๋•Œ, ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์ดํŠธ ์ด๋ฆ„์„ ์–ป๋Š”๋‹ค.", async () => {
  // ์‚ฌ์ดํŠธ ์ด๋ฆ„ "portal"์ด ์กด์žฌํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ณ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹œ๋“œํŒŒ์ผ์—์„œ ๋ดค์Šต๋‹ˆ๋‹ค.
  const siteToCheck = await SiteService.getSiteByName("Portal");
  expect(siteToCheck.name).to.be.equal("Portal"); // ์‹คํŒจ! ์ด์ „ ํ…Œ์ŠคํŠธ์—์„œ ์ด๋ฆ„์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ใ… ใ… 
});

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ์šฐ๋ฆฌ๋Š” ํ…Œ์ŠคํŠธ ๋‚ด๋ถ€์—๋งŒ ๋จธ๋ฌผ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ์ž์ฒด ๋ฐ์ดํ„ฐ ์„ธํŠธ์—์„œ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

it("์‚ฌ์ดํŠธ ์ด๋ฆ„์„ ์—…๋ฐ์ดํŠธ ํ•  ๋•Œ, ์„ฑ๊ณต์„ ํ™•์ธํ•œ๋‹ค.", async () => {
  // ํ…Œ์ŠคํŠธ๋Š” ์ƒˆ๋กœ์šด ๋ ˆ์ฝ”๋“œ๋ฅผ ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•˜๊ณ  ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ์— ๋Œ€ํ•ด์„œ๋งŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
  const siteUnderTest = await SiteService.addSite({
    name: "siteForUpdateTest"
  });
  
  const updateNameResult = await SiteService.changeName(siteUnderTest, "newName");
  
  expect(updateNameResult).to.be(true);
});



โšช ๏ธ 1.10 ์˜ค๋ฅ˜๋ฅผ catch ํ•˜์ง€๋ง๊ณ  expect ํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ์ž…๋ ฅ๊ฐ’์„ assert ํ•  ๋•Œ, try-catch-finally๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  catch ๋ธ”๋Ÿญ์—์„œ assert ํ•˜๋Š”๊ฒŒ ๋งž์•„ ๋ณด์ผ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜ ์˜ˆ๋Š” ํ…Œ์ŠคํŠธ ์˜๋„์™€ ๊ฒฐ๊ณผ expectation์„ ์ˆจ๊ธฐ๋Š” ์–ด์ƒ‰ํ•˜๊ณ  ์žฅํ™ฉํ•œ ํ…Œ์ŠคํŠธ ์‚ฌ๋ก€์ž…๋‹ˆ๋‹ค.

๋ณด๋‹ค ์šฐ์•„ํ•œ ๋Œ€์•ˆ์€ ํ•œ์ค„์งœ๋ฆฌ Chai assertion์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ ์ž…๋‹ˆ๋‹ค: expect(method).to.throw (ํ˜น์€ Jest: expect(method).toThrow()). ์˜ค๋ฅ˜ ์œ ํ˜•์„ ์•Œ๋ ค์ฃผ๋Š” ์†์„ฑ์ด ์˜ˆ์™ธ์— ํฌํ•จ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š๊ณ  ์ผ๋ฐ˜์ ์ธ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฉด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ์‹ค๋ง์Šค๋Ÿฌ์šด ๋ฉ”์‹œ์ง€๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๊ฒƒ ๋ฐ–์— ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ๋ฌด์—‡์ด ์ž˜๋ชป๋˜์—ˆ๋Š”์ง€ ํ…Œ์ŠคํŠธ ๋ณด๊ณ ์„œ(์˜ˆ: CI ๋ณด๊ณ ์„œ)์—์„œ ์ถ”๋ก ํ•˜๊ธฐ ์–ด๋ ค์šธ ๊ฒƒ์ž…๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: try-catch๋กœ ์˜ค๋ฅ˜๊ฐ€ ์กด์žฌํ•œ๋‹ค๊ณ  assert ํ•˜๋Š” ๊ธด ํ…Œ์ŠคํŠธ ์‚ฌ๋ก€

it("์ œํ’ˆ๋ช…์ด ์—†์œผ๋ฉด, 400 ์˜ค๋ฅ˜๋ฅผ ๋˜์ง„๋‹ค.", async() => {
  let errorWeExceptFor = null;
  try {
    const result = await addNewProduct({});
  } catch (error) {
    expect(error.code).to.equal("InvalidInput");
    errorWeExceptFor = error;
  }
  expect(errorWeExceptFor).not.to.be.null;
  // ์ด asserting์ด ์‹คํŒจํ•˜๋ฉด, ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ์—์„œ ๋ˆ„๋ฝ๋œ ์ž…๋ ฅ๊ฐ’์— ๋Œ€ํ•œ ๋‹จ์–ด๋Š” ์•Œ ์ˆ˜ ์—†๊ณ 
  // ์ž…๋ ฅ๊ฐ’์ด null ์ด๋ผ๋Š” ๊ฒƒ๋งŒ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
});

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: QA๋‚˜ PM์ด๋ผ๋„ ์‰ฝ๊ฒŒ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ณ  ์ฝ๊ธฐ ์‰ฌ์šด expectation

it("์ œํ’ˆ๋ช…์ด ์—†์œผ๋ฉด, 400 ์˜ค๋ฅ˜๋ฅผ ๋˜์ง„๋‹ค.", async () => {
  await expect(addNewProduct({}))
    .to.eventually.throw(AppError)
    .with.property("code", "InvalidInput");
});



โšช ๏ธ 1.11 ํ…Œ์ŠคํŠธ์— ํƒœ๊น…ํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ๋Š” ๊ผญ ๋‹ค๋ฅธ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: ๊ฐœ๋ฐœ์ž๊ฐ€ ํŒŒ์ผ์„ ์ €์žฅํ•˜๊ฑฐ๋‚˜ ์ปค๋ฐ‹์„ ํ•  ๋•Œ ๋น ๋ฅด๊ณ , IO๊ฐ€ ๋งŽ์ด ์—†๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ „์ฒด end-to-end ํ…Œ์ŠคํŠธ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์ƒˆ๋กœ์šด Pull Request๊ฐ€ ์ œ์ถœ๋˜์—ˆ์„ ๋•Œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๋“ฑ.. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ์— #cold #api #sanity์™€ ๊ฐ™์€ ํ‚ค์›Œ๋“œ๋กœ ํ…Œ์ŠคํŠธ์— ํƒœ๊น…ํ•˜๋ฉด ํ…Œ์ŠคํŠธ๋ฅผ ํšจ์œจ์ ์œผ๋กœ grep ํ•  ์ˆ˜ ์žˆ๊ณ , ์›ํ•˜๋Š” ํ•˜์œ„์„ธํŠธ๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ) Mocha๋ฅผ ์ด์šฉํ•ด์„œ sanity ํ…Œ์ŠคํŠธ ๊ทธ๋ฃน๋งŒ ์‹คํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค: mocha - grep 'sanity'


โŒ ๊ทธ๋ ‡์ง€ ์•Ÿ์œผ๋ฉด: ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž‘์€ ๋ณ€๊ฒฝ์„ ํ•  ๋•Œ๋งˆ๋‹ค ์ˆ˜์‹ญ ๊ฐœ์˜ DB ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ํฌํ•จํ•œ ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค๋ฉด, ์†๋„๊ฐ€ ๋งค์šฐ ๋А๋ ค์ ธ ๊ฐœ๋ฐœ์ž๊ฐ€ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๊ฒŒ ๋งŒ๋“ค ๊ฒƒ์ž…๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ํ…Œ์ŠคํŠธ๋ฅผ '#cold-test'๋กœ ํƒœ๊น…ํ•˜๋ฉด ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ์‚ฌ๋žŒ์ด ๋น ๋ฅธ ํ…Œ์ŠคํŠธ๋งŒ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(IO๋ฅผ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๊ณ  ๊ฐœ๋ฐœ์ž๊ฐ€ ์ฝ”๋”ฉํ•˜๋Š” ์ค‘์—๋„ ์ž์ฃผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ํ…Œ์ŠคํŠธ cold === quick).

// ์ด ํ…Œ์ŠคํŠธ๋Š” ๋น ๋ฅด๊ณ (DB ์—†์Œ) ํ˜„์žฌ ์‚ฌ์šฉ์ž/CI๊ฐ€ ์ž์ฃผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ํƒœ๊ทธ๋ฅผ ์ง€์ •ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
describe('์ฃผ๋ฌธ ์„œ๋น„์Šค', function() {
  describe('์ƒˆ ์ฃผ๋ฌธ ์ถ”๊ฐ€ #cold-test #sanity', function() {
    test('์‹œ๋‚˜๋ฆฌ์˜ค - ํ†ตํ™”๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์Œ. ์˜ˆ์™ธ - ๊ธฐ๋ณธ ํ†ตํ™” ์‚ฌ์šฉ #sanity', function() {
      // code logic here
    });
  });
});



โšช ๏ธ 1.12 ์ผ๋ฐ˜์ ์ธ ์ข‹์€ ํ…Œ์ŠคํŠธ ๊ธฐ๋ฒ•๋“ค

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์ด ๊ธ€์€ Node.js์™€ ๊ด€๋ จ์ด ์žˆ๊ฑฐ๋‚˜ ์ตœ์†Œํ•œ Node.js๋กœ ์˜ˆ๋ฅผ ๋“ค ์ˆ˜ ์žˆ๋Š” ํ…Œ์ŠคํŠธ ์กฐ์–ธ์— ์ค‘์ ์„๋‘๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๋ฒˆ์—๋Š” Node.js๊ฐ€ ์•„๋‹ˆ์ง€๋งŒ ์ž˜ ์•Œ๋ ค์ง„ ํŒ๋“ค์„ ํฌํ•จํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

TDD ์›์น™์„ ๋ฐฐ์šฐ๊ณ  ์—ฐ์Šตํ•˜์‹ญ์‹œ์˜ค - ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ๋งค์šฐ ๊ฐ€์น˜๊ฐ€ ์žˆ์ง€๋งŒ, ์ž์‹ ์˜ ์Šคํƒ€์ผ์— ๋งž์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹คํŒจ-์„ฑ๊ณต-๋ฆฌํŽ˜ํ† ๋ง ์Šคํƒ€์ผ๋กœ ์ฝ”๋“œ ์ž‘์„ฑ ์ „์— ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์‹ญ์‹œ์˜ค. ๋ฒ„๊ทธ๋ฅผ ๋ฐœ๊ฒฌํ•˜๋ฉด ๊ฐ ํ…Œ์ŠคํŠธ์—์„œ ์ •ํ™•ํžˆ ํ•œ ๊ฐ€์ง€๋งŒ ํ™•์ธํ•˜๋„๋ก ํ•˜์‹ญ์‹œ์˜ค. ์ˆ˜์ •ํ•˜๊ธฐ ์ „์— ์•ž์œผ๋กœ ์ด ๋ฒ„๊ทธ๋ฅผ ๋ฐœ๊ฒฌ ํ•  ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜์‹ญ์‹œ์˜ค. ํ…Œ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณตํ•˜๊ธฐ ์ „์— ๊ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ํ•œ๋ฒˆ ์ด์ƒ ์‹คํŒจํ•˜๋„๋ก ํ•˜์‹ญ์‹œ์˜ค. ํ…Œ์ŠคํŠธ๋ฅผ ๋งŒ์กฑ์‹œํ‚ค๋Š” ๊ฐ„๋‹จํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ๋น ๋ฅด๊ฒŒ ๋ชจ๋“ˆ์„ ์‹œ์ž‘ํ•˜์‹ญ์‹œ์˜ค - ์ ์‹ ์ ์œผ๋กœ ๋ฆฌํŽ™ํ† ๋งํ•˜์—ฌ ํ”„๋กœ๋•์…˜ ๋“ฑ๊ธ‰์˜ ์ˆ˜์ค€์œผ๋กœ ๊ฐ€์ ธ๊ฐ€์‹ญ์‹œ์˜ค. ํ™˜๊ฒฝ(๊ฒฝ๋กœ, OS ๋“ฑ)์— ๋Œ€ํ•œ ์ข…์†์„ฑ์„ ํ”ผํ•˜์‹ญ์‹œ์˜ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ์ˆ˜์‹ญ ๋…„ ๋™์•ˆ ์ˆ˜์ง‘ ๋œ ์•„์ฃผ ์†Œ์ค‘ํ•œ ์กฐ์–ธ์„ ๋†“์น˜๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.



์„น์…˜ 2๏ธโƒฃ: ๋ฐฑ์—”๋“œ ํ…Œ์ŠคํŠธ

โšช ๏ธ 2.1 ๋‹น์‹ ์˜ ํ…Œ์ŠคํŠธ ํฌํŠธํด๋ฆฌ์˜ค๋ฅผ ํ’๋ถ€ํ•˜๊ฒŒ ํ•˜์‹ญ์‹œ์˜ค: ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์™€ ํ”ผ๋ผ๋ฏธ๋“œ๋ฅผ ๋„˜์–ด์„œ์„ธ์š”.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: 10๋…„์ด ๋„˜์€ ๋ชจ๋ธ์ธ ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ๋Š” ์„ธ ๊ฐ€์ง€ ํ…Œ์ŠคํŠธ ์œ ํ˜•์„ ์ œ์‹œํ•˜๊ณ  ๋Œ€๋‹ค์ˆ˜ ๊ฐœ๋ฐœ์ž์˜ ํ…Œ์ŠคํŠธ ์ „๋žต์— ์˜ํ–ฅ์„ ์ฃผ๋Š” ํ›Œ๋ฅญํ•œ ๋ชจ๋ธ์ž…๋‹ˆ๋‹ค. ๋™์‹œ์—, ๋ช‡ ๊ฐ€์ง€ ๋ฐ˜์ง์ด๋Š” ์ƒˆ๋กœ์šด ํ…Œ์ŠคํŠธ ๊ธฐ์ˆ ๋“ค์ด ๋“ฑ์žฅํ•˜์˜€์ง€๋งŒ ๋ชจ๋‘ ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ์˜ ๊ทธ๋ฆผ์ž ๋’ค๋กœ ์‚ฌ๋ผ์กŒ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ์ตœ๊ทผ 10๋…„๊ฐ„ ๋ณด์•„ ์˜จ ๊ทน์ ์ธ ๊ธฐ์ˆ ์˜ ๋ณ€ํ™”๋“ค(Microservices, cloud, serverless)์„ ๊ณ ๋ คํ•  ๋•Œ, ์•„์ฃผ ์˜ค๋ž˜๋œ ๋ชจ๋ธ ํ•˜๋‚˜๊ฐ€ ๋ชจ๋“  ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์œ ํ˜•์— ์ ํ•ฉํ•˜๋‹ค๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•œ๊ฐ€์š”? ํ…Œ์ŠคํŠธ ์„ธ๊ณ„๋Š” ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ์„ ๋ฐ›์•„๋“ค์ด๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์ง€ ์•Š๋‚˜์š”?

์˜คํ•ด๋Š” ํ•˜์ง€ ๋งˆ์„ธ์š”. 2019 ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ์—์„œ TDD์™€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋Š” ์—ฌ์ „ํžˆ ๊ฐ•๋ ฅํ•œ ๊ธฐ์ˆ ์ด๊ณ  ์•„๋งˆ๋„ ๋งŽ์€ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๊ฐ€์žฅ ์–ด์šธ๋ฆฌ๋Š” ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋ชจ๋ธ๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, ํ…Œ์ŠคํŠธ ๋ฏธ๋ผ๋ฏธ๋“œ๋Š” ์œ ์šฉํ•˜์ง€๋งŒ ๊ทธ๊ฒƒ์ด ํ•ญ์ƒ ๋งž๋Š” ๊ฒƒ์€ ์•„๋‹™๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์–ด๋–ค IOT ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ƒ๊ฐํ•ด ๋ด…์‹œ๋‹ค. ์ด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ๋‹ค์ˆ˜์˜ ์ด๋ฒคํŠธ๋ฅผ Kafka/RabbitMQ ๊ฐ™์€ ๋ฉ”์„ธ์ง€ ๋ฒ„์Šค๋กœ ๋ณด๋‚ด๊ณ  ๋‹ค์‹œ ๋ฐ์ดํ„ฐ ์›จ์–ดํ•˜์šฐ์Šค๋กœ ํ˜๋ ค๋ณด๋ƒ…๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ๋ฐ์ดํ„ฐ๋“ค์€ ์–ด๋–ค ๋ถ„์„ UI์—์„œ ์กฐํšŒ๋ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์ •๋ง ์šฐ๋ฆฌ์˜ ํ…Œ์ŠคํŠธ ์˜ˆ์‚ฐ์˜ 50%๋ฅผ ํ†ตํ•ฉ ์ค‘์‹ฌ์ (intergration-centric)์ด๊ณ  ๋กœ์ง์ด ๊ฑฐ์˜ ์—†๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š”๋ฐ ํ• ์• ํ•ด์•ผ ํ• ๊นŒ์š”? ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์œ ํ˜•๋“ค์ด ๋‹ค์–‘ํ•ด์งˆ ์ˆ˜๋ก(bots, crypto, Alexa-skills) ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ๊ฐ€ ์ ํ•ฉํ•˜์ง€ ์•Š์€ ์‹œ๋‚˜๋ฆฌ์˜ค๋“ค์„ ๋ฐœ๊ฒฌํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์ปค์ง‘๋‹ˆ๋‹ค.

์ง€๊ธˆ์ด ๋‹น์‹ ์˜ ํ…Œ์ŠคํŠธ ํฌํŠธํด๋ฆฌ์˜ค๋ฅผ ๋„“ํžˆ๊ณ  ๋” ๋งŽ์€ ํ…Œ์ŠคํŠธ ์œ ํ˜•๋“ค์— ์ต์ˆ™ํ•ด์งˆ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค. (๋‹ค์Œ ํ•ญ๋ชฉ์—์„œ ๋ช‡ ๊ฐ€์ง€ ์•„์ด๋””์–ด๋“ค์„ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค.) ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ ๊ฐ™์€ ๋ชจ๋ธ๋“ค๋„ ์—ผ๋‘์— ๋‘˜ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋‹น์‹ ์ด ์ง๋ฉดํ•˜๊ณ  ์žˆ๋Š” ํ˜„์‹ค ์„ธ๊ณ„์˜ ๋ฌธ์ œ๋“ค์— ์ ํ•ฉํ•œ ํ…Œ์ŠคํŠธ ์œ ํ˜•๋“ค์„ ์ฐพ์œผ์„ธ์š”. ("์šฐ๋ฆฌ API ๊นจ์กŒ์–ด. Consumer-driven contract ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜์ž!" ์ฒ˜๋Ÿผ์š”.) ์œ„ํ—˜์„ฑ ๋ถ„์„์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํฌ๋ฅดํด๋ฆฌ์˜ค๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ํˆฌ์ž์ž์ฒ˜๋Ÿผ ๋‹น์‹ ์˜ ํ…Œ์ŠคํŠธ๋ฅผ ๋‹ค์–‘ํ™”ํ•˜์„ธ์š” - ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„์„ ๊ฐ€๋Š ํ•˜๊ณ  ์ž ์žฌ์  ์œ„ํ—˜์„ฑ์„ ์ค„์ผ ์ˆ˜ ์žˆ๋Š” ์˜ˆ๋ฐฉ ๋ฐฉ๋ฒ•์„ ์ฐพ์œผ์„ธ์š”.

์ฃผ์˜ ์‚ฌํ•ญ : ์†Œํ”„ํŠธ์›จ์–ด ์„ธ๊ณ„์—์„œ์˜ TDD ๋…ผ์Ÿ์€ ์ „ํ˜•์ ์ธ ์ž˜๋ชป๋œ ์ด๋ถ„๋ฒ•์ž…๋‹ˆ๋‹ค. ์–ด๋–ค ์‚ฌ๋žŒ๋“ค์€ TDD๋ฅผ ๋ชจ๋“  ๊ณณ์— ์ ์šฉํ•˜๋ผ๊ณ  ์ฃผ์žฅํ•˜์ง€๋งŒ, ๋‹ค๋ฅธ ์ผ๋ถ€๋Š” TDD๋ฅผ ์•…๋งˆ๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ์ ˆ๋Œ€์ ์œผ๋กœ ํ•œ์ชฝ๋งŒ ์ฃผ์žฅํ•˜๋Š” ์‚ฌ๋žŒ๋“ค์€ ๋ชจ๋‘ ํ‹€๋ ธ์Šต๋‹ˆ๋‹ค :]


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ๋‹น์‹ ์€ ๊ต‰์žฅํ•œ ROI๋ฅผ ์ฃผ๋Š” ๋ช‡ ๊ฐ€์ง€ ํˆด๋“ค์„ ๋†“์น  ๊ฒƒ์ž…๋‹ˆ๋‹ค. Fuzz, lint, mutation ํ…Œ์ŠคํŠธ๋“ค์€ ๋‹จ 10๋ถ„๋งŒ์— ๋‹น์‹ ์—๊ฒŒ ๊ฐ€์น˜๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: Cindy Sridharan์€ ๊ทธ๋…€์˜ ํ›Œ๋ฅญํ•œ ๊ธ€ โ€˜Testing Microservicesโ€Šโ€”โ€Šthe sane wayโ€™์—์„œ ํ’๋ถ€ํ•œ ํ…Œ์ŠคํŠธ ํฌํŠธํด๋ฆฌ์˜ค๋ฅผ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค. alt text

์˜ˆ์ œ: YouTube: โ€œBeyond Unit Tests: 5 Shiny Node.JS Test Types (2018)โ€ (Yoni Goldberg)


alt text



โšช ๏ธ2.2 ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ๊ฐ€ ์ตœ์„ ์˜ ๋ฐฉ๋ฒ•์ผ ์ˆ˜ ์žˆ๋‹ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ๊ฐ๊ฐ์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋งค์šฐ ์ž‘์€ ๋ถ€๋ถ„๋งŒ์„ ์ปค๋ฒ„ํ•˜๊ณ  ์ „์ฒด๋ฅผ ๋ชจ๋‘ ์ปค๋ฒ„ํ•˜๊ธฐ์—๋Š” ๋น„์šฉ์ด ๋งŽ์ด ๋“ญ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด์—, end-to-end ํ…Œ์ŠคํŠธ๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ ๋งŽ์€ ๋ถ€๋ถ„์„ ์ปค๋ฒ„ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ๊นŠ์ด๊ฐ€ ์–•๊ณ  ๋” ๋А๋ฆฝ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ๊ท ํ˜• ์žกํžŒ ์ ‘๊ทผ๋ฒ•์„ ์ ์šฉํ•˜์—ฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ณด๋‹ค๋Š” ํฌ์ง€๋งŒ end-to-end ํ…Œ์ŠคํŠธ๋ณด๋‹ค๋Š” ์ž‘์€ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์€ ์–ด๋–จ๊นŒ์š”? ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ๋Š” ํ…Œ์ŠคํŠธ ์„ธ๊ณ„์—์„œ ์ž˜ ์•Œ๋ ค์ง€์ง€ ์•Š์€ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. - ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ๋Š” ๋‹ค์Œ์˜ ๋‘ ๊ฐ€์ง€ ์ด์ ์„ ๋ชจ๋‘ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค: ํ•ฉ๋ฆฌ์ ์ธ ์„ฑ๋Šฅ๊ณผ TDD ํŒจํ„ด์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ€๋Šฅ์„ฑ + ํ˜„์‹ค์ ์ด๋ฉด์„œ ํ›Œ๋ฅญํ•œ ์ปค๋ฒ„๋ฆฌ์ง€

์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ๋Š” ๋งˆ์ดํฌ๋กœ ์„œ๋น„์Šค '๋‹จ์œ„'์— ์ค‘์ ์„ ๋‘๊ณ  API์— ๋Œ€ํ•˜์—ฌ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ทธ ์ž์ฒด์— ์†ํ•œ ๊ฒƒ๋“ค (์˜ˆ๋ฅผ๋“ค๋ฉด, ์‹ค์ œ DB ๋˜๋Š” ํ•ด๋‹น DB์˜ ์ธ-๋ฉ”๋ชจ๋ฆฌ ๋ฒ„์ „)์€ ๋ชจํ‚น(Mock)ํ•˜์ง€ ์•Š๊ณ , ๋‹ค๋ฅธ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ˜ธ์ถœ๊ณผ ๊ฐ™์€ ์™ธ๋ถ€์ ์ธ ๊ฒƒ์€ ์Šคํ…(Stub)ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ํ•จ์œผ๋กœ์จ ์šฐ๋ฆฌ๋Š” ์šฐ๋ฆฌ๊ฐ€ ๋ฐฐํฌํ•˜๋Š” ๊ฒƒ์„ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ฐ”๊นฅ์ชฝ์—์„œ ์•ˆ์ชฝ์œผ๋กœ ์ ‘๊ทผํ•˜๋ฉฐ, ์ ๋‹นํ•œ ์‹œ๊ฐ„ ์•ˆ์—์„œ ํฐ ์ž์‹ ๊ฐ์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ์‹œ์Šคํ…œ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ 20%์— ๋ถˆ๊ณผํ•˜๋‹ค๋Š” ๊ฒƒ์„ ๊นจ๋‹ซ๊ธฐ๊นŒ์ง€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐ ์˜ค๋žœ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: Supertest๋ฅผ ํ†ตํ•ด ํ”„๋กœ์„ธ์Šค ๋‚ด Express API์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (๋น ๋ฅด๊ณ  ๋‹ค์–‘ํ•œ ๊ณ„์ธต์„ ์ปค๋ฒ„ํ•จ)

alt text



โšช ๏ธ2.3 ์‹ ๊ทœ ๋ฆด๋ฆฌ์ฆˆ๊ฐ€ API ์‚ฌ์šฉ์„ ๊นจ์ง€๊ฒŒ ํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ๋‹น์‹ ์˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋Š” ๋‹ค์ˆ˜์˜ ํด๋ผ์ด์–ธํŠธ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ  ํ˜ธํ™˜์„ฑ์˜ ์ด์œ ๋กœ ์—ฌ๋Ÿฌ ๋ฒ„์ „์˜ ์„œ๋น„์Šค๋ฅผ ์šด์˜ํ•ฉ๋‹ˆ๋‹ค (๋ชจ๋“  ์‚ฌ๋žŒ์„ ๋งŒ์กฑ์‹œํ‚ค๊ธฐ ์œ„ํ•ด์„œ). ๊ทธ๋Ÿฐ ์ƒํ™ฉ์—์„œ ๋‹น์‹ ์ด ์ผ๋ถ€ ํ•„๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด ์ด ํ•„๋“œ๋ฅผ ๋ฏฟ๊ณ  ์‚ฌ์šฉํ•˜๋˜ ์ผ๋ถ€ ์ค‘์š”ํ•œ ํด๋ผ์ด์–ธํŠธ๋Š” ํ™”๊ฐ€ ๋‚  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ํ†ตํ•ฉ(integration) ์„ธ๊ณ„์—์„œ ํ•ด๊ฒฐํ•˜๊ธฐ ์–ด๋ ค์šด ์ง„ํ‡ด์–‘๋‚œ์— ๋†“์ธ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค: ์„œ๋ฒ„ ์‚ฌ์ด๋“œ๊ฐ€ ์—ฌ๋Ÿฌ ํด๋ผ์ด์–ธํŠธ๋“ค์˜ ๋ชจ๋“  ๊ธฐ๋Œ“๊ฐ’์„ ๊ณ ๋ คํ•˜๋Š” ๊ฒƒ์€ ๋งค์šฐ ์–ด๋ ค์šด ์ผ์ž…๋‹ˆ๋‹ค. - ๋ฐ˜๋ฉด์—, ์„œ๋ฒ„๊ฐ€ ๋ฆด๋ฆฌ์ฆˆ ๋‚ ์งœ๋ฅผ ๊ฒฐ์ •ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํด๋ผ์ด์–ธํŠธ๋Š” ์–ด๋– ํ•œ ํ…Œ์ŠคํŠธ๋„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์†Œ๋น„์ž ์ฃผ๋„ ๊ณ„์•ฝ ํ…Œ์ŠคํŠธ(Consumer-driven contracts)์™€ PACT ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ๋งค์šฐ ํŒŒ๊ดด์ ์ธ ๋ฐฉ๋ฒ•์œผ๋กœ ์ด๋Ÿฌํ•œ ํ”„๋กœ์„ธ์Šค๋ฅผ ํ‘œ์ค€ํ™”ํ•˜๊ธฐ ์œ„ํ•ด ๋‚˜ํƒ€๋‚ฌ์Šต๋‹ˆ๋‹ค. - ์„œ๋ฒ„๊ฐ€ ์„œ๋ฒ„์˜ ํ…Œ์ŠคํŠธ ๊ณ„ํš์„ ๊ฒฐ์ •ํ•˜์ง€ ์•Š๊ณ , ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์˜ ํ…Œ์ŠคํŠธ๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค! PACT๋Š” ํด๋ผ์ด์–ธํŠธ์˜ ๊ธฐ๋Œ“๊ฐ’์„ ๊ธฐ๋กํ•˜์—ฌ "๋ธŒ๋กœ์ปค"๋ผ๋Š” ๊ณต์œ ๋œ ์œ„์น˜์— ์˜ฌ๋ ค๋‘˜ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์„œ๋ฒ„๋Š” ๊ทธ ๊ธฐ๋Œ“๊ฐ’์„ ๋‹น๊ฒจ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ณ  ๋นŒ๋“œํ•  ๋•Œ๋งˆ๋‹ค PACT ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊นจ์ง„ ๊ณ„์•ฝ(contract - ์ถฉ์กฑ๋˜์ง€ ์•Š์€ ํด๋ผ์ด์–ธํŠธ์˜ ๊ธฐ๋Œ“๊ฐ’)์„ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•จ์œผ๋กœ์จ, ๋ชจ๋“  ์„œ๋ฒ„-ํด๋ผ์ด์–ธํŠธ API ๊ฐ„ ์ผ์น˜ํ•˜์ง€ ์•Š์€ ๊ฒƒ๋“ค์„ ๋นŒ๋“œ/CI ํ™˜๊ฒฝ์—์„œ ์กฐ๊ธฐ์— ์žก์„ ์ˆ˜ ์žˆ๊ณ  ๋‹น์‹ ์˜ ํฐ ์ ˆ๋ง๊ฐ์„ ์ค„์—ฌ์ค„ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ๋Œ€์•ˆ์€ ์ˆ˜๋™ ๋ฐฐํฌ๋‚˜ ๋ฐฐํฌ์— ๋Œ€ํ•œ ๋‘๋ ค์›€์„ ์•ˆ๊ณ  ๊ฐ€๋Š” ๊ฒƒ ๋ฟ์ž…๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ:

alt text



โšช ๏ธ 2.4 ๋‹น์‹ ์˜ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: Do: ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์€ ๋ฏธ๋“ค์›จ์–ด(Middleware) ํ…Œ์ŠคํŠธ๋ฅผ ํ”ผํ•ฉ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ๋ฏธ๋“ค์›จ์–ด ํ…Œ์ŠคํŠธ๋Š” ์‹œ์Šคํ…œ์˜ ์ž‘์€ ๋ถ€๋ถ„์ผ ๋ฟ์ด๊ณ  ๋ผ์ด๋ธŒ Express ์„œ๋ฒ„๊ฐ€ ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‘ ๊ฐ€์ง€ ์ด์œ  ๋ชจ๋‘ ํ‹€๋ ธ์Šต๋‹ˆ๋‹ค. - ๋ฏธ๋“ค์›จ์–ด๋Š” ์ž‘์ง€๋งŒ ๋ชจ๋“  ์š”์ฒญ ๋˜๋Š” ๋Œ€๋ถ€๋ถ„์˜ ์š”์ฒญ์— ์˜ํ–ฅ์„ ๋ฏธ์น˜๊ณ , {req,res} JS ๊ฐ์ฒด๋ฅผ ๊ฐ€์ง€๋Š” ์ˆœ์ˆ˜ํ•œ ํ•จ์ˆ˜๋กœ ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋ฏธ๋“ค์›จ์–ด ํ•จ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‹จ์ง€ ํ•จ์ˆ˜๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ํ•จ์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด {req, res} ๊ฐ์ฒด์— ๋Œ€ํ•œ ์ธํ„ฐ๋ ‰์…˜์„ ์ŠคํŒŒ์ด(spy)(์˜ˆ๋ฅผ๋“ค์–ด Sinon์„ ์‚ฌ์šฉ)ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ node-mock-http๋Š” ๋” ๋‚˜์•„๊ฐ€์„œ ํ–‰์œ„์— ๋Œ€ํ•œ ์ŠคํŒŒ์ด์™€ ํ•จ๊ป˜ {req, res} ๊ฐ์ฒด๋„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, response ๊ฐ์ฒด์˜ http ์ƒํƒœ๊ฐ€ ๊ธฐ๋Œ€ํ–ˆ๋˜ ๊ฐ’๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํ™•์ธ(assert)ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์•„๋ž˜ ์˜ˆ์ œ๋ฅผ ๋ณด์„ธ์š”)

โŒ Otherwise: Express ๋ฏธ๋“ค์›จ์–ด์—์„œ์˜ ๋ฒ„๊ทธ === ๋ชจ๋“  ์š”์ฒญ ๋˜๋Š” ๋Œ€๋ถ€๋ถ„์˜ ์š”์ฒญ์—์„œ์˜ ๋ฒ„๊ทธ


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap:์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ๋„คํŠธ์›Œํฌ ํ˜ธ์ถœ ์—†์ด ์ „์ฒด Express ์‹œ์Šคํ…œ๋„ ๊นจ์šฐ์ง€ ์•Š์œผ๋ฉด์„œ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ

//ํ…Œ์ŠคํŠธํ•˜๊ณ  ์‹ถ์€ ๋ฏธ๋“ค์›จ์–ด
const unitUnderTest = require('./middleware')
const httpMocks = require('node-mocks-http');
//Jest ๋ฌธ๋ฒ•์œผ๋กœ Mocha์˜ describe() & it()๊ณผ ๋™์ผ
test('ํ—ค๋”์— ์ธ์ฆ์ •๋ณด๊ฐ€ ์—†๋Š” ์š”์ฒญ์€, http status 403์„ ๋ฆฌํ„ดํ•ด์•ผํ•œ๋‹ค.', () => {
  const request = httpMocks.createRequest({
    method: 'GET',
    url: '/user/42',
    headers: {
      authentication: ''
    }
  });
  const response = httpMocks.createResponse();
  unitUnderTest(request, response);
  expect(response.statusCode).toBe(403);
});



โšช ๏ธ2.5 ์ •์  ๋ถ„์„ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ธก์ •ํ•˜๊ณ  ๋ฆฌํŒฉํ† ๋ง ํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์ •์  ๋ถ„์„ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ ํ’ˆ์งˆ์„ ๊ฐœ์„ ํ•˜๊ณ  ์ฝ”๋“œ๋ฅผ ์œ ์ง€ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ๊ด€์ ์ธ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ •์  ๋ถ„์„ ๋„๊ตฌ๋ฅผ ๋‹น์‹ ์˜ CI ๋นŒ๋“œ์— ์ถ”๊ฐ€ํ•˜์—ฌ ์ฝ”๋“œ ๋ƒ„์ƒˆ(code smell)๊ฐ€ ๋ฐœ๊ฒฌ๋˜๋ฉด ์ค‘๋‹จ๋˜๋„๋ก ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ •์  ๋ถ„์„ ๋„๊ตฌ๊ฐ€ ์ผ๋ฐ˜์ ์ธ ๋ฆฐํŠธ(lint) ๋„๊ตฌ๋ณด๋‹ค ๋” ์ข‹์€ ์ ์€ ์—ฌ๋Ÿฌ ํŒŒ์ผ๋“ค์˜ ์ปจํ…์ŠคํŠธ ์•ˆ์—์„œ ํ’ˆ์งˆ์„ ๊ฒ€์‚ฌํ•˜๊ณ (์˜ˆ: ์ค‘๋ณต ํƒ์ง€), ๊ณ ๊ธ‰ ๋ถ„์„(์˜ˆ: ์ฝ”๋“œ ๋ณต์žก์„ฑ)์„ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ฝ”๋“œ ์ด์Šˆ์— ๋Œ€ํ•œ ํžˆ์Šคํ† ๋ฆฌ์™€ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ถ”์ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ •์  ๋ถ„์„ ๋„๊ตฌ ๋‘ ๊ฐ€์ง€๋Š” Sonarqube (2,600+ stars)์™€ Code Climate (1,500+ stars)์ž…๋‹ˆ๋‹ค.

Credit:: Keith Holliday


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ์ฝ”๋“œ ํ’ˆ์งˆ์ด ์ข‹์ง€ ์•Š์œผ๋ฉด ๋ฒ„๊ทธ์™€ ์„ฑ๋Šฅ์€ ๋น›๋‚˜๋Š” ์ƒˆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‚˜ ์ตœ์‹  ๊ธฐ๋Šฅ์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†๋Š” ๋ฌธ์ œ๊ฐ€ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ๋ณต์žก๋„๊ฐ€ ๋†’์€ ํ•จ์ˆ˜๋ฅผ ์ฐพ์•„๋‚ด๋Š” ์ƒ์šฉ ๋„๊ตฌ์ธ CodeClimate:

alt text



โšช ๏ธ 2.6 ๋…ธ๋“œ ํ˜ผ๋ˆ(chaos)๋Œ€ํ•œ ์ค€๋น„์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์ด์ƒํ•˜๊ฒŒ๋„ ๋Œ€๋ถ€๋ถ„์˜ ์†Œํ”„ํŠธ์›จ์–ด ํ…Œ์ŠคํŠธ๋Š” ์˜ค์ง ๋กœ์ง๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ตœ์•…์˜ ์ƒํ™ฉ(์ •๋ง ํ•ด๊ฒฐํ•˜๊ธฐ ์–ด๋ ต๊ธฐ๋„ ํ•œ ์ƒํ™ฉ) ์ค‘ ์ผ๋ถ€๋Š” ์ธํ”„๋ผ ์ด์Šˆ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํ”„๋กœ์„ธ์Šค ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๊ณผ๋ถ€ํ•˜ ๋˜๊ฑฐ๋‚˜ ์„œ๋ฒ„/ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ฃฝ๋Š” ์ƒํ™ฉ, ๋˜๋Š” API ์†๋„๊ฐ€ 50% ์•„๋ž˜๋กœ ๋–จ์–ด์งˆ ๋•Œ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ์ด ์ธ์‹ํ•˜๋Š” ์ƒํ™ฉ์— ๋Œ€ํ•ด์„œ ํ…Œ์ŠคํŠธํ•œ ์ ์ด ์žˆ๋‚˜์š”? ์ด๋Ÿฌํ•œ ๋ฌธ์ œ ์ƒํ™ฉ๋“ค์„ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์ค„์ด๊ธฐ ์œ„ํ•ด์„œ - ์นด์˜ค์Šค ์—”์ง€๋‹ˆ์–ด๋ง(Chaos engineering)์ด ๋„ทํ”Œ๋ฆญ์Šค์— ์˜ํ•ด ํƒ„์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์นด์˜ค์Šค ์—”์ง€๋‹ˆ์–ด๋ง์€ ํ˜ผ๋ˆ(chaos) ์ƒํ™ฉ์— ๋Œ€ํ•œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ณต์›๋ ฅ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด์„œ ์ƒํ™ฉ์— ๋Œ€ํ•œ ์ธ์‹, ํ”„๋ ˆ์ž„์›Œํฌ, ํˆด๋“ค์„ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์œ ๋ช…ํ•œ ํˆด ์ค‘์— ํ•˜๋‚˜์ธ ์นด์˜ค์Šค ๋ชฝํ‚ค(chaos monkey)๋Š” ์„œ๋ฒ„๋ฅผ ๋ฌด์ž‘์œ„๋กœ ์ข…๋ฃŒ์‹œํ‚ค๊ณ  ์ด๋Ÿฌํ•œ ์ƒํ™ฉ์—๋„ ์‚ฌ์šฉ์ž๋Š” ์„œ๋น„์Šค๋ฅผ ๊ณ„์† ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด ์‹œ์Šคํ…œ์ด ๋‹จ์ผ ์„œ๋ฒ„์— ์˜์กดํ•˜์ง€ ์•Š๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค. (์ฟ ๋ฒ„๋„คํ‹ฐ์Šค ๋ฒ„์ „์ธ kube-monkey๋Š” ํŒŸ(Pod)์„ ์ข…๋ฃŒ์‹œํ‚ด) ์ด๋Ÿฌํ•œ ํˆด๋“ค์€ ๋ชจ๋‘ ํ˜ธ์ŠคํŒ…/ํ”Œ๋žซํผ ๋ ˆ๋ฒจ์—์„œ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‹น์‹ ์ด ์ˆœ์ˆ˜ ๋…ธ๋“œ ํ˜ผ๋ˆ์„ ํ…Œ์ŠคํŠธํ•˜๊ณ  ๋ฐœ์ƒ์‹œํ‚ค๊ณ  ์‹ถ์œผ๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ์š”? ์˜ˆ๋ฅผ ๋“ค๋ฉด, ๋…ธ๋“œ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์–ด๋–ป๊ฒŒ ์žกํžˆ์ง€ ์•Š์€ ์˜ค๋ฅ˜, ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ํ”„๋กœ๋ฏธ์Šค ๊ฑฐ๋ถ€(promise rejection), ์ตœ๋Œ€๋กœ ํ—ˆ์šฉ๋œ 1.7GB์— ๋Œ€ํ•œ v8 ๋ฉ”๋ชจ๋ฆฌ ๊ณผ๋ถ€ํ•˜๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€. ํ˜น์€ ์ด๋ฒคํŠธ ๋ฃจํ”„๊ฐ€ ์ž์ฃผ ์ฐจ๋‹จ๋  ๋•Œ UX๊ฐ€ ๋งŒ์กฑ์Šค๋Ÿฝ๊ฒŒ ์œ ์ง€๋˜๋Š”์ง€ ์—ฌ๋ถ€ ๊ฐ™์€ ๊ฒƒ๋“ค์ด์š”. ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์ œ๊ฐ€ ๋ชจ๋“  ์ข…๋ฅ˜์˜ ๋…ธ๋“œ ๊ด€๋ จ๋œ ์นด์˜ค์Šค ํ–‰์œ„๋ฅผ ์ œ๊ณตํ•˜๋Š” node-chaos (alpha)๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ํƒˆ์ถœ๊ตฌ๋Š” ์—†์Šต๋‹ˆ๋‹ค. ๋จธํ”ผ์˜ ๋ฒ•์น™์€ ์ž๋น„์—†์ด ๋‹น์‹ ์˜ ์‹œ์Šคํ…œ์— ํƒ€๊ฒฉ์„ ์ค„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: Node-chaos๋Š” ๋ชจ๋“  ์ข…๋ฅ˜์˜ Node.js ํ–‰์œ„๋“ค์„ ๋ฐœ์ƒ์‹œ์ผœ์„œ ๋‹น์‹ ์˜ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์–ผ๋งˆ๋‚˜ ํ˜ผ๋ˆ ์ƒํƒœ์— ๋Œ€ํ•œ ๋ณต์›๋ ฅ์ด ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

alt text


โšช ๏ธ2.7 ๊ธ€๋กœ๋ฒŒํ•œ ์ดˆ๊ธฐ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ง‘ํ•ฉ์„ ๋งŒ๋“ค์ง€ ๋ง๊ณ  ๊ฐ ํ…Œ์ŠคํŠธ ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ํ™ฉ๊ธˆ๋ฅ (์„น์…˜ 0)์— ๋”ฐ๋ฅด๋ฉด ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ์ปคํ”Œ๋ง์„ ๋ฐฉ์ง€ํ•˜๊ณ  ํ…Œ์ŠคํŠธ ํ๋ฆ„์— ๋Œ€ํ•ด์„œ ์‰ฝ๊ฒŒ ์ถ”๋ก ํ•˜๊ธฐ ์œ„ํ•ด ์ž์‹ ์˜ DB ๋ฐ์ดํ„ฐ๋“ค์„ ์ถ”๊ฐ€ํ•˜๊ณ  ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋กœ ํ…Œ์ŠคํŠธ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ํ˜„์‹ค ์„ธ๊ณ„์—์„  ์„ฑ๋Šฅ ํ–ฅ์ƒ์„ ์œ„ํ•ด ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์ „์— ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ DB์— ์ถ”๊ฐ€ํ•˜๋Š”(โ€˜test fixtureโ€™๋ผ๊ณ  ์•Œ๋ ค์ ธ ์žˆ์Œ) ํ…Œ์Šคํ„ฐ๋“ค์— ์˜ํ•ด์„œ ์ด ๊ทœ์น™์€ ์ข…์ข… ๊นจ์ง€๊ณค ํ•ฉ๋‹ˆ๋‹ค. ์„ฑ๋Šฅ์€ ์‹ค์ œ๋กœ ์ค‘์š”ํ•œ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค. - ์ด ๋ฌธ์ œ๋Š” ์™„ํ™”๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค ('์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ' ์„น์…˜์„ ๋ณด์„ธ์š”). ํ•˜์ง€๋งŒ ํ…Œ์ŠคํŠธ ๋ณต์žก์„ฑ์€ ๋Œ€๋ถ€๋ถ„์˜ ๋‹ค๋ฅธ ๊ณ ๋ ค์‚ฌํ•ญ๋“ค์„ ์ง€๋ฐฐํ•ด ๋ฒ„๋ฆฌ๋Š” ๋”์šฑ ๊ณ ํ†ต์Šค๋Ÿฐ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค. ์‹ค์งˆ์ ์œผ๋กœ ๊ฐ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์— ํ•„์š”ํ•œ DB ๋ ˆ์ฝ”๋“œ๋งŒ ๋ช…์‹œ์ ์œผ๋กœ ์ถ”๊ฐ€ํ•˜๊ณ  ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ๋ฅผ ๊ฐ€์ง€๊ณ ๋งŒ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”. ๋งŒ์•ฝ ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•œ ๋ฌธ์ œ๋ผ๋ฉด - ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š” ํ…Œ์ŠคํŠธ๋“ค์— ๋Œ€ํ•ด์„œ๋งŒ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ฑ„์šฐ๋Š” ํ˜•ํƒœ๋กœ ํƒ€ํ˜‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์˜ˆ: ์ฟผ๋ฆฌ)

โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๊ณ  ๋ฐฐํฌ๋Š” ์ค‘๋‹จ๋˜์–ด ํŒ€์›๋“ค์€ ์ง€๊ธˆ ์†Œ์ค‘ํ•œ ์‹œ๊ฐ„์„ ํ• ์• ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฒ„๊ทธ๊ฐ€ ์žˆ์Šต๋‹ˆ๊นŒ? ์ฐพ์•„๋ด…์‹œ๋‹ค, ์˜ค ์ด๋Ÿฐ - ๋‘ ๊ฐœ์˜ ํ…Œ์ŠคํŠธ๊ฐ€ ๋™์ผํ•œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ(seed data)๋ฅผ ๋ณ€๊ฒฝํ•œ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์ด์ง€ ์•Š๊ณ  ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ๊ธ€๋กœ๋ฒŒ DB ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ํ›…์ด ๊ฑธ๋ ค์žˆ์Šต๋‹ˆ๋‹ค.

before(async () => {
  // DB์— ์‚ฌ์ดํŠธ์™€ ์–ด๋“œ๋ฏผ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋Š” ์–ด๋””์— ์žˆ๋‚˜์š”? ์™ธ๋ถ€์— ์žˆ์Šต๋‹ˆ๋‹ค. ์™ธ๋ถ€ json ํŒŒ์ผ์ด๋‚˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ”„๋ ˆ์ž„์›Œํฌ์— ์žˆ์Šต๋‹ˆ๋‹ค. 
  await DB.AddSeedDataFromJson('seed.json');
});
it("์‚ฌ์ดํŠธ ์ด๋ฆ„์„ ๋ณ€๊ฒฝํ•˜๋ฉด, ์„ฑ๊ณต ๊ฒฐ๊ณผ๊ฐ’์„ ๋ฐ›์•„์˜จ๋‹ค", async () => {
  //"portal"์ด๋ผ๋Š” ์ด๋ฆ„์˜ ์‚ฌ์ดํŠธ๊ฐ€ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. - ์”จ๋“œ ํŒŒ์ผ์—์„œ ๋ดค์Šต๋‹ˆ๋‹ค. 
  const siteToUpdate = await SiteService.getSiteByName("Portal");
  const updateNameResult = await SiteService.changeName(siteToUpdate, "newName");
  expect(updateNameResult).to.be(true);
});
it("์‚ฌ์ดํŠธ ์ด๋ฆ„์œผ๋กœ ์กฐํšŒ ํ–ˆ์„ ๋•Œ, ํ•ด๋‹น ์‚ฌ์ดํŠธ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค", async () => {
  //"portal"์ด๋ผ๋Š” ์ด๋ฆ„์˜ ์‚ฌ์ดํŠธ๊ฐ€ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. - ์”จ๋“œ ํŒŒ์ผ์—์„œ ๋ดค์Šต๋‹ˆ๋‹ค.
  const siteToCheck = await SiteService.getSiteByName("Portal");
  expect(siteToCheck.name).to.be.equal("Portal"); //์‹คํŒจ! ์ด์ „ ํ…Œ์ŠคํŠธ์—์„œ ์ด๋ฆ„์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค :[
});


:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ํ…Œ์ŠคํŠธ ์•ˆ์—์„œ๋งŒ ๋จธ๋ฌผ๋ฉฐ ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ ์„ธํŠธ ์•ˆ์—์„œ๋งŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

it("์‚ฌ์ดํŠธ ์ด๋ฆ„์„ ๋ณ€๊ฒฝํ•˜๋ฉด, ์„ฑ๊ณต ๊ฒฐ๊ณผ๊ฐ’์„ ๋ฐ›์•„์˜จ๋‹ค", async () => {
  //ํ…Œ์ŠคํŠธ๋Š” ์ƒˆ๋กœ์šด ์‹ ๊ทœ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ทธ ๋ ˆ์ฝ”๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
  const siteUnderTest = await SiteService.addSite({
    name: "siteForUpdateTest"
  });
  const updateNameResult = await SiteService.changeName(siteUnderTest, "newName");
  expect(updateNameResult).to.be(true);
});



์„น์…˜ 3๏ธโƒฃ: ํ”„๋ก ํŠธ์—”๋“œ ํ…Œ์ŠคํŠธ

โšช ๏ธ 3.1 ๊ธฐ๋Šฅ์œผ๋กœ๋ถ€ํ„ฐ ํ™”๋ฉด์„ ๋ถ„๋ฆฌํ•˜์‹ญ์‹œ์˜ค

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์ปดํฌ๋„ŒํŠธ ๋กœ์ง์„ ํ…Œ์ŠคํŠธํ• ๋•Œ, ํ™”๋ฉด์˜ ์„ธ๋ถ€์‚ฌํ•ญ๋“ค์€ ์ œ์™ธ๋˜์–ด์•ผํ•  ๋…ธ์ด์ฆˆ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์„ ์ œ์™ธํ•จ์œผ๋กœ์จ ๋‹น์‹ ์˜ ํ…Œ์ŠคํŠธ๋“ค์€ ์ˆœ์ˆ˜ํ•œ ๋ฐ์ดํ„ฐ์— ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ, ๊ทธ๋ž˜ํ”ฝ ๊ตฌํ˜„์— ๋„ˆ๋ฌด ๊ฒฐํ•ฉ๋˜์ง€ ์•Š๋Š” ์ถ”์ƒ์ ์ธ ๋ฐฉ๋ฒ•์„ ํ†ตํ•ด ์š”๊ตฌ๋˜์–ด์ง€๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋งˆํฌ์—…์œผ๋กœ๋ถ€ํ„ฐ ์ถ”์ถœํ•˜์‹ญ์‹œ์˜ค. ๊ทธ๋ฆฌ๊ณ  ๋А๋ฆฌ๊ฒŒ ๋งŒ๋“œ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜๋“ค์„ ์ œ์™ธํ•œ ์˜ค์ง ์ˆœ์ˆ˜ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ฆํ•˜์‹ญ์‹œ์˜ค(vs HTML/CSS ํ™”๋ฉด ์„ธ๋ถ€์‚ฌํ•ญ). ๋‹น์‹ ์€ ๋ Œ๋”๋งํ•˜๋Š” ๊ฒƒ์„ ํ”ผํ•˜๊ณ  ์˜ค์ง ํ™”๋ฉด์˜ ๋’ท๋ถ€๋ถ„(์„œ๋น„์Šค, ์•ก์…˜, ์Šคํ† ์–ด๋“ฑ๊ณผ ๊ฐ™์€)๋งŒ์„ ํ…Œ์ŠคํŠธ ํ•˜๋ ค๊ณ  ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ, ์ด๊ฒƒ์€ ์‹ค์ œ์™€ ๊ฐ™์ง€๋„ ์•Š์œผ๋ฉฐ ์‹ฌ์ง€์–ด ํ™”๋ฉด์— ์˜ฌ๋ฐ”๋ฅธ ๋ฐ์ดํ„ฐ๊ฐ€ ๋„๋‹ฌํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ๋ฅผ ๋‚˜ํƒ€๋‚ด์ง€๋„ ์•Š๋Š” ๊ฐ€์งœ ํ…Œ์ŠคํŠธ์—์„œ์˜ ๊ฒฐ๊ณผ๊ฐ€ ๋  ๊ฒƒ ์ž…๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ๋‹น์‹ ์˜ ํ…Œ์ŠคํŠธ์˜ ์ˆœ์ˆ˜ํ•˜๊ฒŒ ๊ณ„์‚ฐ๋œ ๋ฐ์ดํ„ฐ๋Š” 10ms ๋‚ด์— ์ค€๋น„๋ ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ์ „์ฒด ํ…Œ์ŠคํŠธ๋Š” ํ™”๋ คํ•˜๊ณ  ๋ถˆํ•„์š”ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋•Œ๋ฌธ์— 500ms(100 ํ…Œ์ŠคํŠธ = 1๋ถ„) ๋™์•ˆ ์ง€์†๋  ๊ฒƒ ์ž…๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ํ™”๋ฉด์˜ ์„ธ๋ถ€์‚ฌํ•ญ์„ ๋นผ๋‚ด๋Š” ๊ฒƒ

test('์˜ค์ง VIP๋ฅผ ๋ณด๊ธฐ์œ„ํ•ด ์‚ฌ์šฉ์ž ๋ชฉ๋ก์„ ํ‘œ์‹œ ํ–ˆ์„ ๋•Œ, ์˜ค์ง VIP ๋ฉค๋ฒ„๋“ค๋งŒ ๋ณด์—ฌ์ ธ์•ผ ํ•œ๋‹ค', () => {
  // Arrange
  const allUsers = [
   { id: 1, name: 'Yoni Goldberg', vip: false }, 
   { id: 2, name: 'John Doe', vip: true }
  ];

  // Act
  const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true}/>);

  // Assert - ์šฐ์„  ํ™”๋ฉด์œผ๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœ
  const allRenderedUsers = getAllByTestId('user').map(uiElement => uiElement.textContent);
  const allRealVIPUsers = allUsers.filter((user) => user.vip).map((user) => user.name);
  expect(allRenderedUsers).toEqual(allRealVIPUsers); // ํ™”๋ฉด์— ์•„๋‹Œ ๋ฐ์ดํ„ฐ๋ฅผ ๋น„๊ต
});


:thumbsdown: ์ž˜๋ชป๋œ ์˜ˆ: ํ™”๋ฉด ์„ธ๋ถ€์‚ฌํ•ญ๋“ค๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ์„ž์–ด์„œ ๊ฒ€์ฆ

test('์˜ค์ง VIP๋ฅผ ๋ณด๊ธฐ์œ„ํ•ด ์‚ฌ์šฉ์ž ๋ชฉ๋ก์„ ํ‘œ์‹œ ํ–ˆ์„ ๋•Œ, ์˜ค์ง VIP ๋ฉค๋ฒ„๋“ค๋งŒ ๋ณด์—ฌ์ ธ์•ผ ํ•œ๋‹ค', () => {
  // Arrange
  const allUsers = [
   {id: 1, name: 'Yoni Goldberg', vip: false }, 
   {id: 2, name: 'John Doe', vip: true }
  ];

  // Act
  const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true}/>);

  // Assert - ํ™”๋ฉด๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ์„ž์–ด์„œ ๊ฒ€์ฆ
  expect(getAllByTestId('user')).toEqual('[<li data-test-id="user">John Doe</li>]');
});



โšช ๏ธ 3.2 ๋ณ€ํ•˜์ง€ ์•Š์€ ์š”์†Œ๋“ค์— ๊ธฐ๋ฐ˜ํ•ด์„œ HTML ์—˜๋ฆฌ๋จผํŠธ๋“ค์„ ์ฐพ์œผ์‹ญ์‹œ์˜ค

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: CSS ๊ฒ€์ƒ‰์ž๋“ค๊ณผ ๋‹ค๋ฅด๊ฒŒ ์–‘์‹ ๋ ˆ์ด๋ธ”๋“ค๊ณผ ๊ฐ™์ด ๊ทธ๋ž˜ํ”ฝ ๋ณ€๊ฒฝ์—๋„ ์‚ด์•„๋‚จ์„ ์š”์†Œ๋“ค์„ ๊ธฐ๋ฐ˜์œผ๋กœ HTML ์—˜๋ฆฌ๋จผํŠธ๋“ค์„ ์ฐพ์œผ์‹ญ์‹œ์˜ค. ๋งŒ์•ฝ ์„ค๊ณ„๋œ ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ์ด์™€ ๊ฐ™์€ ์š”์†Œ๋“ค์„ ๊ฐ€์ง€๊ณ  ์žˆ์ง€ ์•Š๋‹ค๋ฉด, 'test-id-submit-button' ๊ณผ ๊ฐ™์ด ํ…Œ์ŠคํŠธ์— ํ•œ์ •๋œ ์š”์†Œ๋ฅผ ๋งŒ๋“œ์‹ญ์‹œ์˜ค. ์ด ๋ฐฉ๋ฒ•์€ ๋‹น์‹ ์˜ ๊ธฐ๋Šฅ/๋กœ์ง ํ…Œ์ŠคํŠธ๋“ค์ด ๋ฃฉ์•คํ•„๋•Œ๋ฌธ์— ์ ˆ๋Œ€ ๋ง๊ฐ€์ง€์ง€ ์•Š์„ ๊ฒƒ์„ ๋ณด์žฅํ•  ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ์ด ์—˜๋ฆฌ๋จผํŠธ์™€ ์š”์†Œ๊ฐ€ ํ…Œ์ŠคํŠธ์— ์˜ํ•ด ์‚ฌ์šฉ๋˜์–ด์ง€๊ณ  ์ œ๊ฑฐ๋˜์–ด์„œ๋Š” ์•ˆ๋œ๋‹ค๋Š”๊ฒƒ์„ ํŒ€ ์ „์ฒด์—๊ฒŒ ๋ช…ํ™•ํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ๋‹น์‹ ์€ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ๋ฅผ ์›ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ ๋งŽ์€ ์ปดํฌ๋„ŒํŠธ๋“ค, ๋กœ์ง ๊ทธ๋ฆฌ๊ณ  ์„œ๋น„์Šค๋“ค์— ๊ฑธ์ณ์ ธ ์žˆ๊ณ  ๋ชจ๋“  ๊ฒƒ์€ ์™„๋ฒฝํ•˜๊ฒŒ ์ค€๋น„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค - ์Šคํ…, ์ŠคํŒŒ์ด, Ajax ํ˜ธ์ถœ์€ ๊ฒฉ๋ฆฌ๋˜์–ด์ ธ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“ ๊ฒƒ์€ ์™„๋ฒฝํ•œ ๊ฒƒ ์ฒ˜๋Ÿผ ๋ณด์ž…๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€๋งŒ, ์ด ํ…Œ์ŠคํŠธ๋Š” ๋””์ž์ด๋„ˆ์— ์˜ํ•ด div ํด๋ž˜์Šค ์ด๋ฆ„์ด 'thick-border' ์—์„œ 'thin-border'๋กœ ๋ฐ”๋€Œ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ํ•œ์ •๋œ ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ฐพ์œผ์‹ญ์‹œ์˜ค

// the markup code (part of React component)
<h3>
  <Badge pill className="fixed_badge" variant="dark">
    <span data-test-id="errorsLabel">{value}</span>
    {/* data-test-id ์†์„ฑ ์ฐธ๊ณ  */}
  </Badge>
</h3>
// react-testing-library๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์ œ
test("metric์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ „๋‹ฌ๋˜์ง€ ์•Š์œผ๋ฉด, 0์„ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋ณด์—ฌ์ค€๋‹ค", () => {
  // Arrange
  const metricValue = undefined;

  // Act
  const { getByTestId } = render(<DashboardMetric value={undefined} />);

  expect(getByTestId("errorsLabel").text()).toBe("0");
});

:thumbsdown: ์ž˜๋ชป๋œ ์˜ˆ: CSS ์š”์†Œ๋“ค์— ์˜์กด

// the markup code (part of React component)
<span id="metric" className="d-flex-column">{value}</span>
// ๋งŒ์•ฝ ๋””์ž์ด๋„ˆ๊ฐ€ ํด๋ž˜์Šค๋ฅผ ๋ณ€๊ฒฝํ•œ๋‹ค๋ฉด?
// this exammple is using enzyme
test("๋ฐ์ดํ„ฐ๊ฐ€ ์ „๋‹ฌ๋˜์ง€ ์•Š์œผ๋ฉด, 0์„ ๋ณด์—ฌ์ค€๋‹ค", () => {
  // ...

  expect(wrapper.find("[className='d-flex-column']").text()).toBe("0");
});

โšช ๏ธ 3.3 ๊ฐ€๋Šฅํ•œํ•œ, ์‹ค์ œ์™€ ๊ฐ™๊ณ  ์™„์ „ํžˆ ๋ Œ๋”๋ง๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•˜์‹ญ์‹œ์˜ค

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์ ๋‹นํ•œ ํฌ๊ธฐ๊ฐ€ ๋•Œ๋งˆ๋‹ค ์‚ฌ์šฉ์ž๊ฐ€ ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์™ธ๋ถ€๋กœ๋ถ€ํ„ฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ณ , ํ™”๋ฉด๋ฅผ ์™„์ „ํžˆ ๋ Œ๋”๋งํ•˜๊ณ , ๊ทธ์— ๋”ฐ๋ผ ์กฐ์น˜๋ฅผ ์ทจํ•˜๊ณ  ๋ Œ๋”๋ง ๋œ ํ™”๋ฉด์ด ์˜ˆ์ƒ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค. ๋ชจ๋“  ์ข…๋ฅ˜์˜ ๋ชฉํ‚น, ๋ถ€๋ถ„ ๋ฐ ์–•์€ ๋ Œ๋”๋ง์„ ํ”ผํ•˜์‹ญ์‹œ์˜ค. ์ด ์ ‘๊ทผ์€ ์„ธ๋ถ€์ •๋ณด์˜ ๋ถ€์กฑ์œผ๋กœ ์ธํ•ด ๊ฑธ๋ฆฌ์ง€ ์•Š๋Š” ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋‚ด๋ถ€์š”์†Œ๋“ค๊ณผ ํ•จ๊ป˜ ์ง€์ €๋ถ„ํ•ด์ง„ ํ…Œ์ŠคํŠธ๋“ค๊ณผ ๊ฐ™์ด ์œ ์ง€๋ณด์ˆ˜๋ฅผ ํ•˜๊ธฐ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. (see bullet 'Favour blackbox testing'). ๋งŒ์•ฝ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค์ค‘ ํ•˜๋‚˜๊ฐ€ ์‹ฌ๊ฐํ•˜๊ฒŒ ๋А๋ ค์ง€๊ฒŒ ํ•˜๊ฑฐ๋‚˜(์˜ˆ: ์• ๋‹ˆ๋ฉ”์ด์…˜)) ์„ค์ •์„ ๋ณต์žกํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š”, ํ•ด๋‹น์š”์†Œ๋ฅผ ๊ฐ€์ƒ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์ฃผ์˜ํ•  ์ : ์ด ๊ธฐ์ˆ ์€ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ํฌ๊ธฐ๊ฐ€ ์ ๋‹นํ•˜๊ฒŒ ๋ฌถ์—ฌ์žˆ๋Š”, ์†Œํ˜• ํ˜น์€ ์ค‘ํ˜• ์ปดํฌ๋„ŒํŠธ๋“ค์—๊ฒŒ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๋„ˆ๋ฌด ๋งŽ์€ ์ž์‹๋“ค๊ณผ ํ•จ๊ป˜ ๋ Œ๋”๋ง๋œ ์ปดํฌ๋„ŒํŠธ๋Š”, ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•œ ์›์ธ(๊ทผ๋ณธ์›์ธ ๋ถ„์„)์„ ์ถ”๋ก ํ•˜๊ธฐ๋„ ์–ด๋ ต๊ณ  ๋งค์šฐ ๋А๋ ค์งˆ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ๋“ค์—์„œ๋Š”, ๋ถ€๋ชจ์— ๋Œ€ํ•ด์„œ๋Š” ๋ช‡๊ฐ€์ง€ ํ…Œ์ŠคํŠธ๋งŒ์„ ์ž‘์„ฑํ•˜๊ณ , ์ž์‹๋“ค์— ๋Œ€ํ•ด์„œ ๋” ๋งŽ์€ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜์‹ญ์‹œ์˜ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ๋‚ด๋ถ€ ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ์˜ ๋‚ด๋ถ€์— ์˜ํ–ฅ์„ ์ฃผ๊ณ  ๊ทธ๋ฆฌ๊ณ  ๋‚ด๋ถ€์˜ ์ƒํƒœ๋ฅผ ํ™•์ธํ•œ๋‹ค๋ฉด - ๋‹น์‹ ์ด ์ปดํฌ๋„ŒํŠธ์˜ ๊ตฌํ˜„์„ ๋ฆฌํŒฉํ† ๋งํ• ๋•Œ, ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋„ ํ•จ๊ป˜ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹น์‹ ์€ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์œ„ํ•œ ๊ทธ๋Ÿฐ ์—ฌ์œ ๊ฐ€ ์žˆ์Šต๋‹ˆ๊นŒ?


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ์™„์ „ํ•˜๊ฒŒ ๋ Œ๋”๋ง๋œ ์ปดํฌ๋„ŒํŠธ์™€ ํ•จ๊ป˜ ์‹ค์ œ์™€ ๊ฐ™์€ ๋™์ž‘

class Calendar extends React.Component {
  static defaultProps = { showFilters: false };

  render() {
    return (
      <div>
        A filters panel with a button to hide/show filters
        <FiltersPanel showFilter={showFilters} title="Choose Filters" />
      </div>
    );
  }
}

//React & Enzyme ์‚ฌ์šฉ ์˜ˆ
test("์‹ค์ œ์ ์ธ ์ ‘๊ทผ: ํ•„ํ„ฐ๋“ค์„ ํด๋ฆญํ•˜๋ฉด, ํ•„ํ„ฐ๋“ค์ด ํ™”๋ฉด์— ํ‘œ์‹œ๋œ๋‹ค", () => {
  // Arrange
  const wrapper = mount(<Calendar showFilters={false} />);

  // Act
  wrapper.find("button").simulate("click");

  // Assert
  expect(wrapper.text().includes("Choose Filter"));
  // ์‚ฌ์šฉ์ž๊ฐ€ ์š”์†Œ์— ์ ‘๊ทผํ•˜๋Š” ๋ฐฉ๋ฒ•: ํ…์ŠคํŠธ๋ฅผ ์ด์šฉ
});

:thumbsdown: ์ž˜๋ชป๋œ ์˜ˆ: ์–•์€ ๋ Œ๋”๋ง๊ณผ ํ•จ๊ป˜ ์‹ค์ œ๋ฅผ ๋ชฉํ‚น

test("์–•์€/๋ชฉํ‚น ์ ‘๊ทผ: ํ•„ํ„ฐ๋“ค์„ ํด๋ฆญํ•˜๋ฉด, ํ•„ํ„ฐ๋“ค์ด ํ™”๋ฉด์— ํ‘œ์‹œ๋œ๋‹ค", () => {
  // Arrange
  const wrapper = shallow(<Calendar showFilters={false} title="Choose Filter" />);

  // Act
  wrapper
    .find("filtersPanel")
    .instance()
    .showFilters();
  // ๋‚ด๋ถ€๋ฅผ ํƒญํ•˜๊ณ , ํ™”๋ฉด์„ ๋ฌด์‹œํ•œ์ฑ„ ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœ. ํ™”์ดํŠธ๋ฐ•์Šค ์ ‘๊ทผ

  // Assert
  expect(wrapper.find("Filter").props()).toEqual({ title: "Choose Filter" });
  // name์„ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜, ๊ด€๋ จ๋œ ๋‹ค๋ฅธ ๊ฒƒ๋“ค์„ ์ „๋‹ฌํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?
});

โšช ๏ธ 3.4 ์Šฌ๋ฆฝ์„ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค. ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ๋“ค์„ ์œ„ํ•ด ์ง€์›ํ•˜๋Š” ๋‚ด์žฅ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ๊ทธ๋ฆฌ๊ณ  ์†๋„๋ฅผ ๋†’์ด๋ ค ๋…ธ๋ ฅํ•˜์‹ญ์‹œ์˜ค

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ์— ํ…Œ์ŠคํŠธ ์™„๋ฃŒ์‹œ๊ฐ„์€ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. (์˜ˆ: ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ์š”์†Œ์˜ ์ถœํ˜„์„ ์ง€์—ฐ์‹œํ‚ด) - ์ด๋Ÿฐ ๊ฒฝ์šฐ์—๋Š” ์Šฌ๋ฆฝ(์˜ˆ: setTimeout)์„ ํ”ผํ•˜๊ณ , ๋Œ€๋ถ€๋ถ„์˜ ํ”Œ๋žซํผ๋“ค์ด ์ œ๊ณตํ•˜๋Š” ๋” ๊ฒฐ์ •์ ์ธ ๋ฉ”์†Œ๋“œ๋“ค์„ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ๋ช‡๋ช‡ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์€ awaiting ๊ธฐ๋Šฅ์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. (์˜ˆ: Cypress cy.request('url')), ๋Œ€๊ธฐ๋ฅผ ์œ„ํ•œ ๋‹ค๋ฅธ API @testing-library/dom method wait(expect(element)). ๋•Œ๋•Œ๋กœ, ๋” ์šฐ์•„ํ•œ ๋ฐฉ๋ฒ•์€ API๊ฐ™์ด ๋А๋ฆฐ ์ž์›์„ ์Šคํ…ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ํ›„ ์‘๋‹ต์ˆœ๊ฐ„์ด ๊ฒฐ์ •์ ์ด ๋˜๋ฉด, ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค์‹œ ๋ Œ๋”๋ง ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์™ธ๋ถ€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์Šฌ๋ฆฝ์ƒํƒœ์ผ๋•Œ๋Š”, hurry-up the clock๊ฐ€ ์œ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์Šฌ๋ฆฝ์€ ๋‹น์‹ ์˜ ํ…Œ์ŠคํŠธ๋ฅผ ๋А๋ฆฌ๊ณ  ์œ„ํ—˜ํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ ๋•Œ๋ฌธ์— ํ”ผํ•ด์•ผํ•  ํŒจํ„ด์ž…๋‹ˆ๋‹ค(๋„ˆ๋ฌด ์งง์€ ์‹œ๊ฐ„ ๊ธฐ๋‹ค๋ ค์•ผํ•  ๊ฒฝ์šฐ). ๋งŒ์•ฝ ์Šฌ๋ฆฝ๊ณผ ํด๋ง์ด ํ•„์—ฐ์ ์ด๊ณ  ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ์˜ ์ง€์›์ด ์—†๋‹ค๋ฉด, wait-for-expect์™€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ์ค€๊ฒฐ์ • ์†”๋ฃจ์…˜์œผ๋กœ์„œ ๋„์›€์„ ์ค„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ์˜ค๋žœ ์‹œ๊ฐ„๋™์•ˆ ์Šฌ๋ฆฝํ•˜๋Š” ๊ฒฝ์šฐ, ํ…Œ์ŠคํŠธ๋Š” ๋” ๋А๋ ค์งˆ ๊ฒƒ ์ž…๋‹ˆ๋‹ค. ์Šฌ๋ฆฝํ• ๋•Œ, ํ…Œ์ŠคํŠธ์ค‘์ธ ์œ ๋‹›์ด ์ œ ์‹œ๊ฐ„์— ๋ฐ˜์‘ํ•˜์ง€ ์•Š์œผ๋ฉด ํ…Œ์ŠคํŠธ๋Š” ์‹คํŒจํ•  ๊ฒƒ ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๊ทธ๊ฒƒ์€ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋Š” ์•ฝ์ ๊ณผ ๋‚˜์œ ์„ฑ๋Šฅ๊ฐ„์˜ ํŠธ๋ ˆ์ด๋“œ ์˜คํ”„๋ฅผ ๊ฐ€์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ๋น„๋™๊ธฐ ์‹คํ–‰์ด ์™„๋ฃŒ๋ ๋•Œ ์ฒ˜๋ฆฌ๋˜๋Š” E2E API (Cypress)

// using Cypress
cy.get('#show-products').click()// navigate
cy.wait('@products')// wait for route to appear
// ๋ผ์šฐํŠธ๊ฐ€ ์ค€๋น„๋˜๋ฉด ์‹คํ–‰ ๋ฉ๋‹ˆ๋‹ค.

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: DOM ์š”์†Œ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

// @testing-library/dom
test("์˜ํ™” ์ œ๋ชฉ์ด ๋‚˜ํƒ€๋‚œ๋‹ค", async () => {
  // ์š”์†Œ๋Š” ์ดˆ๊ธฐ์— ์กด์žฌ ํ•˜์ง€ ์•Š์Œ...

  // ์ถœํ˜„์„ ๋Œ€๊ธฐ
  await wait(() => {
    expect(getByText("the lion king")).toBeInTheDocument();
  });

  // ์ถœํ˜„์„ ๊ธฐ๋‹ค๋ฆฐ ํ›„ ์š”์†Œ๋ฅผ ๋ฆฌํ„ด
  const movie = await waitForElement(() => getByText("the lion king"));
});

:thumbsdown: ์ž˜๋ชป๋œ ์˜ˆ: ์‚ฌ์šฉ์ž ์ •์˜ ์Šฌ๋ฆฝ ์ฝ”๋“œ

test("์˜ํ™” ์ œ๋ชฉ์ด ๋‚˜ํƒ€๋‚œ๋‹ค", async () => {
  // ์ดˆ๊ธฐ์— ์š”์†Œ๊ฐ€ ์กด์žฌ ํ•˜์ง€ ์•Š์Œ...

  // ์‚ฌ์šฉ์ž ์ •์˜ ๋Œ€๊ธฐ ๋กœ์ง (์ฃผ์˜: ๋งค์šฐ ๋‹จ์ˆœ, ํƒ€์ž„์•„์›ƒ์ด ์•„๋‹˜)
  const interval = setInterval(() => {
    const found = getByText("the lion king");
    if (found) {
      clearInterval(interval);
      expect(getByText("the lion king")).toBeInTheDocument();
    }
  }, 100);

  // ์ถœํ˜„์„ ๊ธฐ๋‹ค๋ฆฐ ํ›„ ์š”์†Œ๋ฅผ ๋ฆฌํ„ด
  const movie = await waitForElement(() => getByText("the lion king"));
});

โšช ๏ธ 3.5 ํ™”๋ฉด์˜ ๋‚ด์šฉ์ด ๋„คํŠธ์›Œํฌ๋ฅผ ํ†ตํ•ด ์–ด๋–ป๊ฒŒ ์ œ๊ณต๋ ์ง€ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค

โœ… ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ๋„คํŠธ์›Œํฌ๋ฅผ ์ ๊ฒ€ํ•˜๊ณ  ํŽ˜์ด์ง€๋กœ๋“œ ์ƒํƒœ๋ฅผ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ๋Š” ๋„คํŠธ์›Œํฌ ์•กํ‹ฐ๋ธŒ ๋ชจ๋‹ˆํ„ฐ๋ฅผ ์ ์šฉํ•˜์‹ญ์‹œ์˜ค. pingdom, AWS CloudWatch, gcp StackDriver ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‰ฝ๊ฒŒ ์„œ๋ฒ„์˜ ์ƒํƒœ์™€ response ๋‚ด์šฉ์„ ๋ชจ๋‹ˆํ„ฐ๋ง ํ•˜๋„๋ก ์„ค์ • ํ•  ์ˆ˜ ์žˆ๊ณ , ํ™”๋ฉด์—์„œ์˜ ์˜ค๋ฅ˜๋Š” ์ตœ์†Œํ™” ํ•˜๋ฉด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋“ค์€ ํ”„๋ก ํŠธ์—”๋“œ์— ์ตœ์ ํ™” ๋œ ๋‹ค๋ฅธ ๋„๊ตฌ(lighthouse, pagespeed) ๋ณด๋‹ค ์„ ํ˜ธ๋˜๊ณ  ์žˆ๊ณ , ๋” ๋‹ค์–‘ํ•œ ๋ถ„์„ ๊ธฐ๋Šฅ์„ ์ œ๊ณต ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๋Š” ํŽ˜์ด์ง€ ๋กœ๋”ฉ์‹œ๊ฐ„์ด๋‚˜, ์ฃผ์š” ํ•ญ๋ชฉ์˜ ๋กœ๋”ฉ, ํ™”๋ฉด์ด ์ธํ„ฐ๋ ‰ํ‹ฐ๋ธŒ ํ•˜๊ฒŒ ๋˜๋Š” ์‹œ๊ฐ„๊ณผ ๊ฐ™์ด ํ™”๋ฉด์— ์ง์ ‘์ ์œผ๋กœ ์˜ํ–ฅ์„ ์ฃผ๋Š” ํ•ญ๋ชฉ์ด๋‚˜ ์ƒํƒœ์— ์ค‘์ ์„ ๋‘ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฌด์—‡๋ณด๋‹ค contents๊ฐ€ ์••์ถ• ๋˜์—ˆ๋Š”์ง€, ์ฒซ byte ์‘๋‹ต๊นŒ์ง€์˜ ์‹œ๊ฐ„, image ์ตœ์ ํ™”, ์ ์ ˆํ•œ DOM ์˜ ํฌ๊ธฐ ํŒŒ์•…, SSL ๋“ฑ์˜ ๊ธฐ์ˆ ์  ์ด์Šˆ์— ์ค‘์ ์„ ๋‘ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฐ ํ•ญ๋ชฉ์— ๋Œ€ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง์€ ๊ฐœ๋ฐœ์‹œ์ ์ด๋‚˜, CI, 24x7 ์šด์˜ ์ค‘ ์ˆ˜ํ–‰ํ•  ๊ฒƒ์„ ๊ถŒ์žฅ ํ•ฉ๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ์™„๋ฒฝํ•œ UI ๋ฅผ ๊ฐœ๋ฐœํ•ด ๋‘๊ณ , 100% ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ๊นŒ์ง€ ์™„๋ฃŒ ํ–ˆ์ง€๋งŒ, ์ตœ์•…์˜ UX๋ฅผ ์ œ๊ณตํ•˜๊ฒŒ ๋˜๊ฑฐ๋‚˜ CDN์˜ ์„ค์ • ์˜ค๋ฅ˜ ๋“ฑ์˜ ์ด์œ  ๋•Œ๋ฌธ์— ๋„ˆ๋ฌด ๋А๋ฆฐ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๊ฒŒ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: Lighthouse ํŽ˜์ด์ง€ ๋กœ๋”ฉ ๊ฒ€์‚ฌ ๋ณด๊ณ ์„œ


โšช ๏ธ 3.6 ๋ฐฑ์—”๋“œ API์™€ ๊ฐ™์ด ์ž์ฃผ ๋ฉˆ์ถœ ์ˆ˜ ์žˆ๊ฑฐ๋‚˜ ๋А๋ฆฐ ๋ฆฌ์†Œ์Šค๋Š” stub ํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์ฃผ์š” ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ(E2Eํ…Œ์ŠคํŠธ ์•„๋‹˜) ์ž‘์„ฑ ์‹œ, backend API ์ฒ˜๋Ÿผ ๊ทธ ๊ธฐ๋Šฅ์˜ ์ฃผ ์—ญํ• ์—์„œ ๋ฒ—์–ด๋‚˜๋Š” ํ•ญ๋ชฉ์€ ์ œ์™ธํ•  ์ˆ˜ ์žˆ๋„๋ก stub(์˜ˆ: test double) ํ•˜์‹ญ์‹œ์˜ค. ์‹ค์ œ ๋„คํŠธ์›Œํฌ๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ๋ง๊ณ , test double ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(Sinon, Test doubles)๋ฅผ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ์ด ๊ฒฝ์šฐ ๊ฐ€์žฅ ํฐ ์žฅ์ ์€ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ํ…Œ์ŠคํŠธ ์‹คํŒจ๋ฅผ ์˜ˆ๋ฐฉ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ๋‹จ์—์„œ API ์ •์˜์— ๋”ฐ๋ผ test๋ฅผ ์ ์šฉํ•˜๋Š” ์ž‘์—…์€ ์•ˆ์ •์ ์ด์ง€ ์•Š๊ณ  ๋‹น์‹ ์˜ component๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์ง€๋งŒ ์ˆ˜์‹œ๋กœ test๊ฐ€ ์‹คํŒจ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.(์šด์˜ ์ƒํƒœ์˜ env ์„ค์ •์€ testing์—์„œ๋Š” ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”. API ์š”์ฒญ์— ๋ณ‘๋ชฉ์ด ๋ฐœ์ƒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.) ์ด๋Ÿฐ์‹์œผ๋กœ ํ•ด์„œ API ์‘๋‹ต ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ, ์—๋Ÿฌ๊ฐ€ ์‘๋‹ต๋˜๋Š” ๊ฒฝ์šฐ ๋“ฑ์˜ ๋‹ค์–‘ํ•œ API ์ƒํƒœ์— ๋”ฐ๋ฅธ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œํ•œ๋ฒˆ ๊ฐ•์กฐํ•˜์ง€๋งŒ ์‹ค์ œ ๋„คํŠธ์›Œํฌ ํ˜ธ์ถœ์€ test๋ฅผ ๋งค์šฐ ๋А๋ฆฌ๊ฒŒ ๋งŒ๋“ค๊ฒƒ์ž…๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ํ‰๊ท  ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์‹œ๊ฐ„์ด ๋ช‡ ms ์ธ ๊ฒฝ์šฐ, API ํ˜ธ์ถœ๋กœ ๊ฐœ๋‹น ์ตœ์†Œ 100ms ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๊ฒŒ ๋˜๊ณ  ํ…Œ์ŠคํŠธ๋Š” ์•ฝ 20๋ฐฐ ๋А๋ ค์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: Stubbing ํ•˜๊ฑฐ๋‚˜ API ์‘๋‹ต๊ฐ’ ๋ณ€์กฐ

// unit under test
export default function ProductsList() {
  const [products, setProducts] = useState(false);

  const fetchProducts = async () => {
    const products = await axios.get("api/products");
    setProducts(products);
  };

  useEffect(() => {
    fetchProducts();
  }, []);

  return products ? <div>{products}</div> : <div data-test-id="no-products-message">No products</div>;
}

// test
test("products๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ, ์ ์ ˆํ•œ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œํ•œ๋‹ค", () => {
  // Arrange
  nock("api")
    .get(`/products`)
    .reply(404);

  // Act
  const { getByTestId } = render(<ProductsList />);

  // Assert
  expect(getByTestId("no-products-message")).toBeTruthy();
});

โšช ๏ธ 3.7 ์ „์ฒด ์‹œ์Šคํ…œ์— ๊ฑธ์นœ ์—”๋“œ-ํˆฌ-์—”๋“œ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฑฐ์˜ ์—†์Šต๋‹ˆ๋‹ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: E2E(end-to-end)๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์‹ค์ œ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์‚ฌ์šฉํ•œ UI๋ฅผ ์œ„ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์˜๋ฏธํ•˜์ง€๋งŒ(3.6 ์ฐธ๊ณ ), ๋‹ค๋ฅธ ์˜๋ฏธ๋กœ ์‹ค์ œ ๋ฐฑ์—”๋“œ๋ฅผ ํฌํ•จํ•˜์—ฌ ์ „์ฒด ์‹œ์Šคํ…œ์„ ํ™•์žฅํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ํ›„์ž์˜ ํ…Œ์ŠคํŠธ ์œ ํ˜•์€ ๊ตํ™˜ ์Šคํ‚ค๋งˆ์— ๋Œ€ํ•œ ์ž˜๋ชป๋œ ์ดํ•ด๋กœ ์ธํ•ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ๊ฐ„์˜ ํ†ตํ•ฉ ๋ฒ„๊ทธ๋ฅผ ์ปค๋ฒ„ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ƒ๋‹นํžˆ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ๋ฐฑ์—”๋“œ๊ฐ„ ํ†ตํ•ฉ ๋ฌธ์ œ(์˜ˆ : ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค A๊ฐ€ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค B์— ์ž˜๋ชป๋œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ธ๋‹ค)๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ณ  ๋ฐฐํฌ ์‹คํŒจ๋ฅผ ๊ฐ์ง€ํ•˜๋Š” ํšจ๊ณผ์ ์ธ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. Cypress์™€ Pupeteer๊ฐ™์€ UI ํ”„๋ ˆ์ž„์›Œํฌ๋งŒํผ ์นœ์ˆ™ํ•˜๊ณ  ์„ฑ์ˆ™ํ•œ E2E ํ…Œ์ŠคํŠธ ๋ฐฑ์—”๋“œ ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ์—†์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํ…Œ์ŠคํŠธ์˜ ๋‹จ์ ์€ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ๋งŽ์€ ํ™˜๊ฒฝ์„ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐ ๋“œ๋Š” ๋†’์€ ๋น„์šฉ๊ณผ ์ฃผ๋กœ ๋ถˆ์•ˆ์ •์„ฑ ์ž…๋‹ˆ๋‹ค - 50๊ฐœ์˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๊ฐ€ ์ œ๊ณต๋˜๋Š”๋ฐ, ํ•˜๋‚˜๊ฐ€ ์‹คํŒจํ•˜๋”๋ผ๋„ ์ „์ฒด E2E๊ฐ€ ์‹คํŒจ. ๋”ฐ๋ผ์„œ ์ด ๊ธฐ๋ฒ•์„ ์ ์ ˆํžˆ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋ฉฐ, ๊ทธ ์ค‘ 1~10๊ฐœ ์ •๋„๋งŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์†Œ์ˆ˜์˜ E2E ํ…Œ์ŠคํŠธ ์ผ์ง€๋ผ๋„ ๋ฐฐํฌ ๋ฐ ํ†ตํ•ฉ ์˜ค๋ฅ˜์™€ ๊ฐ™์€ ์œ ํ˜•์˜ ๋ฌธ์ œ๋ฅผ ์žก์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜๊ณผ ๊ฐ™์€ ์Šคํ…Œ์ด์ง• ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: UI๋Š” ๋ฐฑ์—”๋“œ์—์„œ ๋ฆฌํ„ดํ•˜๋Š” ํŽ˜์ด๋กœ๋“œ๊ฐ€ ์˜ˆ์ƒ๊ณผ ๋งค์šฐ ๋‹ค๋ฅด๋‹ค๋Š” ๊ฒƒ์„ ์•Œ์•„์ฐจ๋ฆฌ๊ธฐ ์œ„ํ•˜์—ฌ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ์— ๋งŽ์€ ํˆฌ์ž๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โšช ๏ธ 3.8 ๋กœ๊ทธ์ธ ์ž๊ฒฉ ์ฆ๋ช…์„ ์žฌ์‚ฌ์šฉํ•˜์—ฌ E2E ํ…Œ์ŠคํŠธ ์†๋„ ํ–ฅ์ƒ

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์‹ค์ œ ๋ฐฑ์—”๋“œ๋ฅผ ํฌํ•จํ•˜๊ณ  API ํ˜ธ์ถœ์ด ๊ฐ€๋Šฅํ•œ ์‚ฌ์šฉ์ž ํ† ํฐ์„ ์‚ฌ์šฉํ•˜๋Š” E2E ํ…Œ์ŠคํŠธ์—์„œ, ๋ชจ๋“  ์š”์ฒญ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  ๋กœ๊ทธ์ธ์ด ๋˜๋Š” ์ˆ˜์ค€์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ๊ฒฉ๋ฆฌํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹™๋‹ˆ๋‹ค. ๋Œ€์‹  ํ…Œ์ŠคํŠธ๊ฐ€ ์‹œ์ž‘๋˜๊ธฐ ์ „์— ํ•œ ๋ฒˆ๋งŒ ๋กœ๊ทธ์ธํ•˜๊ณ (์ฆ‰, before-all), ํ† ํฐ์„ ๋กœ์ปฌ ์ €์žฅ์†Œ์— ์ €์žฅํ•ด์„œ ์—ฌ๋Ÿฌ ์š”์ฒญ์— ์žฌ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ์ด๊ฒƒ์€ ํ•ต์‹ฌ ํ…Œ์ŠคํŠธ ์›์น™ ์ค‘ ํ•˜๋‚˜๋ฅผ ์œ„๋ฐ˜ํ•˜๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค - ๋ฆฌ์†Œ์Šค ์ปคํ”Œ๋ง ์—†์ด ํ…Œ์ŠคํŠธ๋ฅผ ์ž์œจ์ ์œผ๋กœ ์œ ์ง€ํ•˜์‹ญ์‹œ์˜ค. ์ด๊ฒƒ์€ ์šฐ๋ คํ•  ๋งŒ ํ•˜์ง€๋งŒ E2E ํ…Œ์ŠคํŠธ์—์„œ ์„ฑ๋Šฅ์€ ํ•ต์‹ฌ ๊ด€์‹ฌ์‚ฌ์ด๋ฉฐ, ๊ฐœ๋ณ„ ํ…Œ์ŠคํŠธ๋ฅผ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ๋งค๋ฒˆ 1~3๊ฐœ์˜ API ์š”์ฒญ์„ ๋ณด๋‚ด๊ฒŒ ๋˜๋ฉด ์‹คํ–‰ ์‹œ๊ฐ„์ด ๋”์งํ•ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž๊ฒฉ ์ฆ๋ช…์„ ์žฌ์‚ฌ์šฉํ•œ๋‹ค๊ณ ํ•ด์„œ ํ…Œ์ŠคํŠธ๊ฐ€ ๋™์ผํ•œ ์‚ฌ์šฉ์ž ๋ ˆ์ฝ”๋“œ์— ๋Œ€ํ•ด ์ˆ˜ํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค๋Š” ์˜๋ฏธ๋Š” ์•„๋‹™๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ๋ ˆ์ฝ”๋“œ(์˜ˆ: ํ…Œ์ŠคํŠธ ์œ ์ € ๊ฒฐ์ œ ๋‚ด์—ญ)์— ์˜์กดํ•˜๋Š” ๊ฒฝ์šฐ ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ๋ฅผ ํ…Œ์ŠคํŠธ์˜ ์ผ๋ถ€๋กœ ์ƒ์„ฑํ•˜๊ณ  ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์™€์˜ ๊ณต์œ ๋ฅผ ํ”ผํ•˜์‹ญ์‹œ์˜ค. ๋˜ํ•œ ๋ฐฑ์—”๋“œ๊ฐ€ ์œ„์กฐ๋  ์ˆ˜ ์žˆ์Œ์„ ๊ธฐ์–ตํ•˜์‹ญ์‹œ์˜ค - ํ…Œ์ŠคํŠธ๊ฐ€ ํ”„๋ก ํŠธ์—”๋“œ์— ์ค‘์ ์„ ๋‘” ๊ฒฝ์šฐ, ํ…Œ์ŠคํŠธ๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ  ๋ฐฑ์—”๋“œ API๋ฅผ ์Šคํ…ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.(3.6 ์ฐธ๊ณ )


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: 200๊ฐœ์˜ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ์ฃผ์–ด์กŒ๊ณ  ๋กœ๊ทธ์ธ์— 100ms๊ฐ€ ์†Œ์š”๋œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๋ฉด ๋งค๋ฒˆ ๋กœ๊ทธ์ธ์—๋งŒ 20์ดˆ๊ฐ€ ์†Œ์š”๋œ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: before-each๊ฐ€ ์•„๋‹Œ before-all์— ๋กœ๊ทธ์ธ

let authenticationToken;

// ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „์— ๋ฐœ์ƒ
before(() => {
  cy.request('POST', 'http://localhost:3000/login', {
    username: Cypress.env('username'),
    password: Cypress.env('password'),
  })
  .its('body')
  .then((responseFromLogin) => {
    authenticationToken = responseFromLogin.token;
  })
})

// ๊ฐ ํ…Œ์ŠคํŠธ ์ „์— ๋ฐœ์ƒ
beforeEach(setUser => () {
  cy.visit('/home', {
    onBeforeLoad (win) {
      win.localStorage.setItem('token', JSON.stringify(authenticationToken))
    },
  })
})

โšช ๏ธ 3.9 ์‚ฌ์ดํŠธ ์ „์ฒด ํŽ˜์ด์ง€๋ฅผ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ๋Š” E2E smoke ํ…Œ์ŠคํŠธ๋ฅผ ๋งŒ๋“œ์‹ญ์‹œ์˜ค

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์šด์˜์ค‘์ธ ์„œ๋น„์Šค๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋ง ํ•˜๊ฑฐ๋‚˜ ๊ฐœ๋ฐœ์ค‘์—๋„ ์ „์ฒด ํŽ˜์ด์ง€๋ฅผ ์ ๊ฒ€ํ•  ์ˆ˜ ์žˆ๋Š” E2E ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜์‹ญ์‹œ์˜ค. ์ด๋Ÿฐ ์œ ํ˜•์˜ ํ…Œ์ŠคํŠธ๋Š” ๊ฐ„๋‹จํžˆ ์ž‘์„ฑํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ๊ทธ์— ๋”ฐ๋ฅธ ํšจ๊ณผ๋Š” ๊ฑฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋Šฅ์ ์ด๊ฑฐ๋‚˜ ๋„คํŠธ์›Œํฌ, ๊ฐœ๋ฐœ ๊ด€๋ จ ์ด์Šˆ๋ฅผ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์ข…๋ฅ˜์˜ ํ…Œ์ŠคํŠธ๋“ค์€ E2E ๋งŒํผ ์‹ ๋ขฐํ•  ์ˆ˜๋Š” ์—†๊ณ  ์™„๋ฒฝํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.(์ผ๋ถ€ ์šด์˜ํŒ€์—์„œ๋Š” ๋‹จ์ˆœํžˆ ์šด์˜ ์„œ๋ฒ„๋กœ ping์„ ํ•˜๊ณ ์žˆ๊ณ , ๊ฐœ๋ฐœ์ž๋“ค์ด ๋‹จ์ˆœ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ๋งŒ์„ ์‹คํ–‰ํ•˜์—ฌ ํŒจํ‚ค์ง•์ด๋‚˜ ๋ธŒ๋ผ์šฐ์ ธ ๊ด€๋ จ ์ด์Šˆ๋Š” ํ™•์ธ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.) smoke ํ…Œ์ŠคํŠธ๋Š” ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ€์ฒดํ•˜๊ธฐ ๋ณด๋‹ค๋Š” smoke ๋ฐœ๊ฒฌ์„ ์œ„ํ•œ ๋„๊ตฌ๋กœ ๋ณผ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋„ ํŒจ์Šคํ•˜๊ณ  ์„œ๋น„์Šค ํ•ผ์Šค์ฒดํฌ๋„ ์ •์ƒ์ด๋ผ ๋ชจ๋“ ๊ฒƒ์ด ์™„๋ฒฝํ•ด ๋ณด์ผ์ˆ˜ ์žˆ์ง€๋งŒ, Payment component๊ฐ€ ํŒจํ‚ค์ง• ์ด์Šˆ๊ฐ€ ์žˆ์–ด์„œ ํŽ˜์ด์ง€ ์ด๋™ ์‹œ ํ™”๋ฉด ๋žœ๋”๋ง์ด ์•ˆ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ๋ชจ๋“  ํŽ˜์ด์ง€์˜ smoke ํƒ์ƒ‰ํ•˜๊ธฐ

it('๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ smoke ํ…Œ์ŠคํŠธ ํ•  ๋•Œ, ํŽ˜์ด์ง€๋“ค์ด ์ •์ƒ์ ์œผ๋กœ ๋กœ๋“œ๋˜์–ด์•ผ ํ•œ๋‹ค', () => {
    // Cypress๋ฅผ ์ด์šฉํ•œ ์˜ˆ์ œ ์ž…๋‹ˆ๋‹ค
    // ๋‹ค๋ฅธ E2E ๋„๊ตฌ๋กœ๋„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค
    cy.visit('https://mysite.com/home');
    cy.contains('Home');
    cy.contains('https://mysite.com/Login');
    cy.contains('Login');
    cy.contains('https://mysite.com/About');
    cy.contains('About');
  })

โšช ๏ธ 3.10 ๋‹ค๊ฐ™์ด ํ˜‘์—…๊ฐ€๋Šฅํ•œ ๋ฌธ์„œ๋กœ ํ…Œ์ŠคํŠธ ๋‚ด์šฉ์„ ๋‚ด๋ณด๋‚ด๊ธฐ ํ•˜์‹ญ์‹œ์˜ค

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: App์˜ ์‹ ๋ขฐ๋„๋ฅผ ๋†’์ผ ์ˆ˜ ์žˆ๊ณ , ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ๋˜ ๋‹ค๋ฅธ ๊ฐœ์„ ์˜ ๊ธฐํšŒ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ๋‚ด์šฉ์€ ๋œ ๊ธฐ์ˆ ์ ์ด๋ฉด์„œ ์ œํ’ˆ/UX์™€ ๊ด€๋ จ๋œ ํ‘œํ˜„์œผ๋กœ ๋˜์–ด ์žˆ์–ด์„œ, ๋‹ค์–‘ํ•œ ํ˜‘์—…์ž๋“ค(๊ฐœ๋ฐœ์ž, ๊ณ ๊ฐ ๋“ฑ)๊ฐ„์— ์˜์‚ฌ์†Œํ†ต ์ˆ˜๋‹จ์œผ๋กœ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์–ด๋–ค ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์€ ๋น„์ฆˆ๋‹ˆ์Šค ํ๋ฆ„๊ณผ ์˜ˆ์ƒ๋˜๋Š” ๊ฒฐ๊ณผ๋“ค์„ ๋ˆ„๊ตฌ๋‚˜(์Šคํ…Œ์ดํฌํ™€๋”, PM) ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ์–ธ์–ด๋กœ ํ‘œํ˜„ํ•˜์—ฌ ๊ฐ™์ด ํ™•์ธํ•˜๊ณ  ํ˜‘์—… ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์›€์„ ์ฃผ๋Š” ํ•„์ˆ˜์ ์ธ ๋ฌธ์„œ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณ ๊ฐ์€ ์ž์‹ ์˜ ์ธ์ˆ˜์กฐ๊ฑด์„ ๊ฐ™์ด ์ •์˜ํ•ด ๋‚˜๊ฐ€๋ฉด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ์ด๊ฒƒ์€ ๊ฒฐ๊ตญ โ€˜์ธ์ˆ˜ ํ…Œ์ŠคํŠธโ€™ ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์ด๊ฒŒ ๋ฐ”๋กœ BDD (behavior-driven testing) ์ž…๋‹ˆ๋‹ค. ๊ฐ€์žฅ ์œ ๋ช…ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ๋Š” Cucumber(์ž๋ฐ”ํ–ฅ)๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜์˜ ์˜ˆ์ œ๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”. ์ด์™€ ๋น„์Šทํ•˜๋ฉด๋„ ์ข€ ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ๋Š” UI ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ ๋‹ค์–‘ํ•œ ์ƒํƒœ๋ณ„ ์‹ค์ œ ํ™”๋ฉด์„ ํ™•์ธํ•˜๊ณ  ์–ด๋–ค ๊ฒฝ์šฐ ๊ทธ๋Ÿฐ ์ƒํƒœ๊ฐ€ ๋ Œ๋”๋ง ๋˜๋Š”์ง€ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ๋Š” StoryBook ์ด ์žˆ์Šต๋‹ˆ๋‹ค.(์˜ˆ. Grid๋ฅผ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์™€ ์—†๋Š” ๊ฒฝ์šฐ, ํ•„ํ„ฐ๋œ ๊ฒฝ์šฐ๋กœ ๋ Œ๋”๋งํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.) ์ด๋Ÿฐ ๊ฒƒ๋“ค์€ ์ œํ’ˆ ๊ด€๊ณ„์ž์—๊ฒŒ ๋งค๋ ฅ์ ์ผ ์ˆ˜๋„ ์žˆ์ง€๋งŒ ๋ณดํ†ต ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ๊ฐœ๋ฐœ ๋ฌธ์„œ ์ฒ˜๋Ÿผ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์„ ์œ„ํ•ด ๋งŽ์€ ๊ณต์ˆ˜๋ฅผ ๋“ค์˜€์ง€๋งŒ ๊ฒฐ๊ณผ์ ์œผ๋กœ ์œ„์™€ ๊ฐ™์€ ์žฅ์ ์„ ๋†“์น˜์ง€ ๋ฉ๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: cucumber-js ์˜ humnan ์–ธ์–ด๋ฅผ ์‚ฌ์šฉํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

// Cucumber ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ์„ค๋ช…: ํ‰๋ฌธ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ˆ„๊ตฌ๋“  ์ดํ•ดํ•˜๊ณ  ํ˜‘์—… ํ•  ์ˆ˜ ์žˆ๋‹ค

Feature: Twitter new tweet
 
  I want to tweet something in Twitter
  
  @focus
  Scenario: Tweeting from the home page
    Given I open Twitter home
    Given I click on "New tweet" button
    Given I type "Hello followers!" in the textbox 
    Given I click on "Submit" button
    Then I see message "Tweet saved"
    

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: Storybook์„ ์ด์šฉํ•œ components ์ƒํƒœ, ์ž…๋ ฅ ๊ฐ’๋ณ„ visualizing

โšช ๏ธ 3.11 ์ž๋™ํ™”๋œ ํˆด์„ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ๊ฐ์  ๋ฌธ์ œ(Visual Issues)๋ฅผ ๊ฐ์ง€ํ•ด๋ผ.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์ปจํ…Œ์ธ ๊ฐ€ ๊ฒน์น˜๊ฑฐ๋‚˜ ๊นจ์ง€๋Š” ๋“ฑ์˜ ์‹œ๊ฐ์  ๋ฌธ์ œ๋“ค์ด ๊ฐ์ง€๋  ๋•Œ, UI ์Šคํฌ๋ฆฐ ์ƒท์„ ์บก์ฒ˜ํ•˜๊ธฐ ์œ„ํ•ด ์ž๋™ํ™” ๋„๊ตฌ๋ฅผ ์…‹์—…ํ•˜์„ธ์š”. ์ด๋ฅผ ํ†ตํ•ด, ์˜ฌ๋ฐ”๋ฅธ ๋ฐ์ดํ„ฐ๊ฐ€ ์ค€๋น„ ๋  ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ๋ฆฌํ•˜๊ฒŒ ๋ณ€๊ฒฝ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ธฐ์ˆ ์€ ํ˜„์žฌ ๋„๋ฆฌ ์ฑ„ํƒ๋˜์ง€๋Š” ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ์˜ ํ…Œ์ŠคํŠธ ์‚ฌ๊ณ  ๋ฐฉ์‹์€ ์—ฌ์ „ํžˆ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ์— ์˜์กดํ•˜์ง€๋งŒ ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์ œ๋กœ ๊ฒฝํ—˜ํ•˜๋Š” ๊ฒƒ์€ ์‹œ๊ฐ์  ์š”์†Œ์ด๋ฉฐ ๋‹ค์–‘ํ•œ ๋””๋ฐ”์ด์Šค ์œ ํ˜•๋•Œ๋ฌธ์— ์ผ๋ถ€ UI ๋ฒ„๊ทธ๋“ค์€ ๊ฐ„๊ณผ๋˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค. ์ผ๋ถ€ ๋ฌด๋ฃŒ ํˆด๋“ค์€ ์œก์•ˆ ๊ฒ€์‚ฌ๋ฅผ ์œ„ํ•œ ์Šคํฌ๋ฆฐ ์ƒท์„ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ์ €์žฅํ•˜๋Š” ๊ธฐ๋Šฅ๊ณผ ๊ฐ™์€ ๊ธฐ๋ณธ์ ์ธ ๊ธฐ๋Šฅ๋“ค์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฐฉ๋ฒ•์€ ์ž‘์€ ํฌ๊ธฐ์˜ App์—๋Š” ์ถฉ๋ถ„ํ•˜์ง€๋งŒ, ๋ณ€๊ฒฝ์ด ๋ฐœ์ƒํ•  ๋•Œ๋งˆ๋‹ค ์‚ฌ๋žŒ์˜ ์†๊ธธ์ด ํ•„์š”ํ•œ ๋‹ค๋ฅธ ์ˆ˜๋™ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ธฐ์—๋Š” ์ œ์•ฝ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด์—, ๋ช…ํ™•ํ•œ ์ •์˜๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— UI ๋ฌธ์ œ๋ฅผ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜๋Š” ๊ฒƒ์€ ์ƒ๋‹นํžˆ ์–ด๋ ค์šด ์ผ์ž…๋‹ˆ๋‹ค. - ์ด ๋ถ€๋ถ„์ด Visual Regression ํ…Œ์ŠคํŠธ ์˜์—ญ์ž…๋‹ˆ๋‹ค. ์ด์ „ ๋ฒ„์ „์˜ UI๋ฅผ ์ตœ๊ทผ ๋ณ€๊ฒฝ๊ณผ ๋น„๊ตํ•˜์—ฌ ์ฐจ์ด์ ์„ ๊ฐ์ง€ํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ถ€ ์˜คํ”ˆ์†Œ์Šค/๋ฌด๋ฃŒ ํˆด๋“ค์€ ์ด ๊ธฐ๋Šฅ๋“ค์˜ ์ผ๋ถ€๋ฅผ ์ œ๊ณตํ•ด ์ฃผ์ง€๋งŒ(์˜ˆ. wraith, PhantomCSS) ์ƒ๋‹นํ•œ ์…‹์—… ์‹œ๊ฐ„์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ƒ์šฉ ํˆด๋“ค์€(์˜ˆ. Applitools, Percy.io) ์„ค์น˜๊ฐ€ ๊ฐ„ํŽธํ•˜๊ณ  ๊ด€๋ฆฌ UI, ์•Œ๋žŒ, โ€˜์‹œ๊ฐ์  ๋…ธ์ด์ฆˆ(์˜ˆ. ๊ด‘๊ณ , ์• ๋‹ˆ๋ฉ”์ด์…˜)โ€™๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ์Šค๋งˆํŠธ ์บก์ณ์™€ ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚ค๋Š” DOM/css์˜ ๊ทผ๋ณธ ์›์ธ์„ ๋ถ„์„ํ•˜๋Š” ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.


โŒ Otherwise: How good is a content page that display great content (100% tests passed), loads instantly but half of the content area is hidden?


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:thumbsdown: Anti Pattern Example: A typical visual regression - right content that is served badly

alt text


:clap: Doing It Right Example: Configuring wraith to capture and compare UI snapshots

โ€‹# Add as many domains as necessary. Key will act as a labelโ€‹

domains:
  english: "http://www.mysite.com"โ€‹

โ€‹# Type screen widths below, here are a couple of examplesโ€‹

screen_widths:

  - 600โ€‹
  - 768โ€‹
  - 1024โ€‹
  - 1280โ€‹


โ€‹# Type page URL paths below, here are a couple of examplesโ€‹
paths:
  about:
    path: /about
    selector: '.about'โ€‹
  subscribe:
      selector: '.subscribe'โ€‹
    path: /subscribe

:clap: Doing It Right Example: Using Applitools to get snapshot comaprison and other advanced features

import * as todoPage from "../page-objects/todo-page";

describe("visual validation", () => {
  before(() => todoPage.navigate());
  beforeEach(() => cy.eyesOpen({ appName: "TAU TodoMVC" }));
  afterEach(() => cy.eyesClose());

  it("should look good", () => {
    cy.eyesCheckWindow("empty todo list");
    todoPage.addTodo("Clean room");
    todoPage.addTodo("Learn javascript");
    cy.eyesCheckWindow("two todos");
    todoPage.toggleTodo(0);
    cy.eyesCheckWindow("mark as completed");
  });
});



์„น์…˜ 4๏ธโƒฃ: ํ…Œ์ŠคํŠธ ํšจ๊ณผ ์ธก์ •



โšช ๏ธ 4.1 ์ž์‹ ๊ฐ์„ ๊ฐ–๊ธฐ์— ์ถฉ๋ถ„ํ•œ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ํ™•๋ณดํ•˜์‹ญ์‹œ์˜ค. ~80%๊ฐ€ ์ด์ƒ์ ์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ํ…Œ์ŠคํŠธ์˜ ๋ชฉ์ ์€ ๋น ๋ฅธ ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ์ถฉ๋ถ„ํ•œ ์ž์‹ ๊ฐ์„ ๊ฐ–๊ธฐ ์œ„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ถ„๋ช…ํžˆ ๋” ๋งŽ์€ ์ฝ”๋“œ๊ฐ€ ํ…Œ์ŠคํŠธ ๋ ์ˆ˜๋ก ํŒ€์€ ๋” ์ž์‹ ๊ฐ์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ์–ผ๋งˆ๋‚˜ ๋งŽ์€ ๋ผ์ธ(๋ธŒ๋žœ์น˜, ๊ตฌ๋ฌธ(statements) ๋“ฑ)์ด ํ…Œ์ŠคํŠธ์— ์˜ํ•ด ์ปค๋ฒ„๋˜์—ˆ๋Š”์ง€์— ๋Œ€ํ•œ ์ง€ํ‘œ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ์–ด๋А ์ •๋„๊ฐ€ ์ถฉ๋ถ„ํ• ๊นŒ์š”? 10โ€“30%๋Š” ๋นŒ๋“œ ์ •ํ™•์„ฑ์— ๋Œ€ํ•ด ํŒ๋‹จํ•˜๊ธฐ์—๋Š” ๋ถ„๋ช…ํžˆ ๋„ˆ๋ฌด ๋‚ฎ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด์— 100%๋Š” ๋น„์šฉ์ด ๋งŽ์ด ๋“ค๊ณ  ์ •์ž‘ ๋‹น์‹ ์˜ ๊ด€์‹ฌ์„ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด ์•„๋‹Œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ ์˜ฎ๊ฒจ๋ฒ„๋ฆด์ง€๋„ ๋ชจ๋ฆ…๋‹ˆ๋‹ค. ์ด๊ฒƒ์— ๋Œ€ํ•œ ๋‹ต์€ ์ˆ˜์น˜๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์œ ํ˜•๊ณผ ๊ฐ™์€ ๋‹ค์–‘ํ•œ ์š”์†Œ๋“ค์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง„๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. - ๋งŒ์•ฝ ๋‹น์‹ ์ด Airbus A380์˜ ์ฐจ์„ธ๋Œ€ ๋ฒ„์ „์„ ๋งŒ๋“ค๋ฉด 100%๋กœ ๋งž์ถฐ์•ผ ํ•˜์ง€๋งŒ ์›นํˆฐ ์‚ฌ์ดํŠธ๋ผ๋ฉด 50%๋ฉด ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. ๋น„๋ก ํ…Œ์ŠคํŠธ์— ์—ด์„ฑ์ธ ๋Œ€๋ถ€๋ถ„์˜ ์‚ฌ๋žŒ๋“ค์€ ์ ์ ˆํ•œ ์ปค๋ฒ„๋ฆฌ์ง€ ์ž„๊ณ„๊ฐ’์ด ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ ธ์•ผ ํ•œ๋‹ค๊ณ  ํ•˜์ง€๋งŒ, ๊ทธ๋“ค ์ค‘ ๋Œ€๋ถ€๋ถ„์€ ๋Œ€๋‹ค์ˆ˜์˜ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ์กฑํ•˜๊ธฐ ์œ„ํ•ด์„œ ๊ฒฝํ—˜์ƒ์œผ๋กœ 80%(๋งˆํ‹ด ํŒŒ์šธ๋Ÿฌ: โ€œin the upper 80s or 90sโ€)๊ฐ€ ์ ์ ˆํ•˜๋‹ค๊ณ  ์–˜๊ธฐํ•ฉ๋‹ˆ๋‹ค.

๊ตฌํ˜„ ํŒ: ๋‹น์‹ ์˜ CI ํ™˜๊ฒฝ์—์„œ ์ปค๋ฒ„๋ฆฌ์ง€ ์ž„๊ณ„์น˜๋ฅผ ์„ค์ •ํ•˜์—ฌ ๊ทธ ๊ธฐ์ค€์— ๋ฏธ์น˜์ง€ ๋ชปํ•˜๋ฉด ๋นŒ๋“œ๋ฅผ ๋ฉˆ์ถ”๋„๋ก ํ•˜๊ณ  ์‹ถ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. (์ปดํฌ๋„ŒํŠธ ๋‹น ์ž„๊ณ„์น˜๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ ์˜ˆ์ œ ์ฝ”๋“œ๋ฅผ ๋ณด์„ธ์š”). ์ด ์œ„์—, ๋นŒ๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ๊ฐ์†Œ์— ๋Œ€ํ•œ ๊ฐ์ง€๋„ ๊ณ ๋ คํ•ด ๋ณด์„ธ์š”. (์ƒˆ๋กœ ์ปค๋ฐ‹ ๋œ ์ฝ”๋“œ๊ฐ€ ์ปค๋ฒ„๋ฆฌ์ง€์— ๋ชป ๋ฏธ์น  ๋•Œ) - ์ด๋ ‡๊ฒŒ ํ•จ์œผ๋กœ์จ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์˜ฌ๋ฆฌ๊ฑฐ๋‚˜ ์ ์–ด๋„ ์œ ์ง€ํ•˜๋„๋ก ์••๋ฐ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋งํ•œ๋Œ€๋กœ ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ์˜ค์ง ํ•˜๋‚˜์˜ ์–‘์  ์ง€ํ‘œ์ผ ๋ฟ ํ…Œ์ŠคํŠธ์˜ ๊ฒฌ๊ณ ์„ฑ์„ ๋‚˜ํƒ€๋‚ด๊ธฐ์—๋Š” ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋‹ค์Œ ํ•ญ๋ชฉ์— ๋‚˜์™€์žˆ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋‹น์‹ ์„ ์†์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: Confidence and numbers go hand in hand, without really knowing that you tested most of the systemโ€Šโ€”โ€Šthere will also be some fear. and fear will slow you down


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:clap: ์˜ˆ์ œ: ์ผ๋ฐ˜์ ์ธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ณด๊ณ ์„œ

alt text


:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ์ปดํฌ๋„ŒํŠธ ๋‹น ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์„ค์ •ํ•˜์‹ญ์‹œ์˜ค. (Jest๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ)

![alt text](assets/bp-18-code-coverage2.jpeg "Setting up coverage per component (using Jest)



โšช ๏ธ 4.2 ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฆฌํฌํŠธ๋ฅผ ํ™•์ธํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๋˜์ง€ ์•Š์€ ๋ถ€๋ถ„๊ณผ ๊ธฐํƒ€ ์ด์ƒํ•œ ์ ๋“ค์„ ๊ฐ์ง€ํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์ผ๋ถ€ ๋ฌธ์ œ๋“ค์€ ๋ ˆ์ด๋”๋ง ์•„๋ž˜๋กœ ์ˆจ์–ด๋ฒ„๋ ค ๊ธฐ์กด์˜ ํˆด๋“ค์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฐพ๊ธฐ ๋งค์šฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ๋“ค์€ ์‹ค์ œ๋กœ ๋ฒ„๊ทธ๋Š” ์•„๋‹ˆ์ง€๋งŒ ์‹ฌ๊ฐํ•œ ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์ƒ๊ฐ์ง€ ๋ชป ํ•œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋™์ž‘๋“ค์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ผ๋ถ€ ์ฝ”๋“œ ์˜์—ญ์€ ์ ˆ๋Œ€ ๋˜๋Š” ๊ฑฐ์˜ ํ˜ธ์ถœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. - โ€˜PricingCalculatorโ€™๋ผ๋Š” ์ƒํ’ˆ ๊ฐ€๊ฒฉ์„ ์„ค์ •ํ•˜๋Š” ํด๋ž˜์Šค๊ฐ€ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ด ๋ณด์„ธ์š”. DB์— 100000๊ฐœ์˜ ์ƒํ’ˆ์ด ์žˆ๊ณ  ํŒ๋งค๋„ ๋งŽ์ง€๋งŒ ์ด ํด๋ž˜์Šค๋Š” ์‹ค์ œ๋กœ ์ ˆ๋Œ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š” ๊ฒƒ์œผ๋กœ ๋ฐํ˜€์กŒ์Šต๋‹ˆ๋‹ค... ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฆฌํฌํŠธ๋ฅผ ํ†ตํ•ด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋‹น์‹ ์ด ์›ํ•˜๋Š” ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ์™ธ์—๋„ ๋ฆฌํฌํŠธ๋Š” ์–ด๋–ค ์ฝ”๋“œ๋“ค์ด ํ…Œ์ŠคํŠธ๋˜์ง€ ์•Š์•˜๋Š”์ง€๋ฅผ ๊ฐ•์กฐํ•ด์„œ ๋ณด์—ฌ์ค„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. - ์ฝ”๋“œ์˜ 80%๊ฐ€ ํ…Œ์ŠคํŠธ ๋˜์—ˆ๋‹ค๋Š” ์•Œ๋ฆผ์ด ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด ์ปค๋ฒ„๋˜์—ˆ๋Š”์ง€์— ๋Œ€ํ•œ ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋‚ด์ง„ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฆฌํฌํŠธ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์€ ์‰ฝ์Šต๋‹ˆ๋‹ค. - ์šด์˜ ๋˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ๋•Œ ์ปค๋ฒ„๋ฆฌ์ง€ ํŠธ๋ž˜ํ‚น์„ ํ•˜๋ฉด์„œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•˜์„ธ์š”. ๊ทธ๋Ÿฌ๊ณ  ๋‚˜์„œ ๊ฐ ์ฝ”๋“œ ์˜์—ญ์ด ์–ผ๋งˆ๋‚˜ ์ž์ฃผ ํ˜ธ์ถœ๋๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ํ˜•ํ˜•์ƒ‰์ƒ‰์˜ ๋ฆฌํฌํŠธ๋ฅผ ๋ณด์„ธ์š”. ์ž ๊น ์‹œ๊ฐ„์„ ๋‚ด์„œ ์ด ๋ฐ์ดํ„ฐ๋“ค์„ ๋ณด๋ฉด ๋ช‡ ๊ฐ€์ง€ ๋ฌธ์ œ์ ๋“ค์„ ๋ฐœ๊ฒฌํ•˜๊ฒŒ ๋  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ์–ด๋–ค ์ฝ”๋“œ๊ฐ€ ํ…Œ์ŠคํŠธ๋˜์ง€ ์•Š์•˜๋Š”์ง€ ์•Œ ์ˆ˜ ์—†์œผ๋ฉด ๋ฌธ์ œ์˜ ์›์ธ๋„ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: ์ด ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฆฌํฌํŠธ์—๋Š” ์–ด๋–ค ๋ฌธ์ œ๊ฐ€ ์žˆ๋‚˜์š”? ํ˜„์‹ค ์„ธ๊ณ„ ์‹œ๋‚˜๋ฆฌ์˜ค๋กœ QA์—์„œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‚ฌ์šฉ์„ ์ถ”์ ํ–ˆ๊ณ  ํฅ๋ฏธ๋กœ์šด ๋กœ๊ทธ์ธ ํŒจํ„ด์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค. (ํžŒํŠธ: ๋กœ๊ทธ์ธ ์‹คํŒจ ํšŸ์ˆ˜๊ฐ€ ๋น„๋ก€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ถ„๋ช…ํžˆ ๋ฌด์–ธ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค.) ๋งˆ์นจ๋‚ด ์ผ๋ถ€ ํ”„๋ก ํŠธ์—”๋“œ ๋ฒ„๊ทธ๊ฐ€ ๋ฐฑ์—”๋“œ ๋กœ๊ทธ์ธ API๋ฅผ ๊ณ„์† ํ˜ธ์ถœํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์ด ๋ฐํ˜€์กŒ์Šต๋‹ˆ๋‹ค.

![alt text](assets/bp-19-coverage-yoni-goldberg-nodejs-consultant.png "Whatโ€™s wrong with this coverage report? based on a real-world scenario where we tracked our application usage in QA and find out interesting login patterns (Hint: the amount of login failures is non-proportional, something is clearly wrong. Finally it turned out that some frontend bug keeps hitting the backend login API)



โšช ๏ธ 4.3 mutation ํ…Œ์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋…ผ๋ฆฌ์ ์ธ ๋ฒ”์œ„๋ฅผ ์ธก์ •

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์ „ํ†ต์ ์ธ ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ •์€ ๊ฑฐ์ง“๋ง์Ÿ์ด: 100%์˜ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•จ์ˆ˜ ์ค‘ ์˜ฌ๋ฐ”๋ฅธ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ธฐ๋Šฅ์€ ์—†์Šต๋‹ˆ๋‹ค. ์‹ฌ์ง€์–ด ํ•˜๋‚˜๋„. ์–ด์ฐŒํ•˜์—ฌ? ํ…Œ์ŠคํŠธ๊ฐ€ ๋ฐฉ๋ฌธํ•œ ์ฝ”๋“œ ๋ผ์ธ์„ ๋‹จ์ˆœํ•˜๊ฒŒ ์ธก์ •ํ•˜์ง€๋งŒ, ํ…Œ์ŠคํŠธ์—์„œ ์‹ค์ œ๋กœ ํ…Œ์ŠคํŠธ(์˜ฌ๋ฐ”๋ฅธ ์‘๋‹ต์„ assertion) ํ•œ ๊ฒƒ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ์ถœ์žฅ์„ ์œ„ํ•ด ์—ฌํ–‰ํ•˜๊ณ  ์—ฌ๊ถŒ ์Šคํ…œํ”„๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์‚ฌ๋žŒ์ฒ˜๋Ÿผ - ์ด๊ฒƒ์€ ๋‹จ์ง€ ๊ณตํ•ญ๊ณผ ํ˜ธํ…”์„ ๋ฐฉ๋ฌธํ–ˆ์„ ๋ฟ, ์ผ์„ ํ–ˆ๋Š”์ง€ ์–ด๋–ค ๊ฒƒ๋„ ์ฆ๋ช…ํ•˜์ง€ ๋ชปํ•œ๋‹ค.

mutation ๊ธฐ๋ฐ˜์˜ ํ…Œ์ŠคํŠธ๋Š” ๋‹จ์ˆœํ•œ '๋ฐฉ๋ฌธ'์ด ์•„๋‹Œ ์‹ค์ œ๋กœ ํ…Œ์ŠคํŠธ '๋œ' ์ฝ”๋“œ์˜ ์–‘์„ ์ธก์ •ํ•˜๋Š”๋ฐ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค. Stryker๋Š” mutation ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ JavaScript ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋ฉฐ ๊ตฌํ˜„์ด ์ •๋ง ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค:

(1) ์˜๋„์ ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  "๋ฒ„๊ทธ๋ฅผ ์‹ฌ์Šต๋‹ˆ๋‹ค". ์˜ˆ๋ฅผ ๋“ค๋ฉด, newOrder.price === 0 ๋Š” newOrder.price != 0์ด ๋ฉ๋‹ˆ๋‹ค. ์ด "๋ฒ„๊ทธ"๋ฅผ mutation์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

(2) ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณตํ•˜๋ฉด ์šฐ๋ฆฌ๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค - ํ…Œ์ŠคํŠธ๋Š” ๋ฒ„๊ทธ๋ฅผ ๋ฐœ๊ฒฌํ•˜๋Š” ๋ชฉ์ ์„ ๋‹ฌ์„ฑํ•˜์ง€ ๋ชปํ–ˆ๊ณ , mutation์€ ์‚ด์•„๋‚จ์•˜๋‹ค. ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋ฉด ์—„์ฒญ๋‚œ mutation์ด ์ฃฝ์—ˆ๋‹ค.

๋ชจ๋“  ํ˜น์€ ๋Œ€๋ถ€๋ถ„์˜ mutation์ด ์ฃฝ์—ˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๋ฉด ์ „ํ†ต์ ์ธ ์ปค๋ฒ„๋ฆฌ์ง€๋ณด๋‹ค ํ›จ์”ฌ ๋” ๋†’์€ ์‹ ๋ขฐ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ ๊ตฌ์„ฑ ์‹œ๊ฐ„์€ ๋น„์Šทํ•ฉ๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: 85%์˜ ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ํ…Œ์ŠคํŠธ์—์„œ ์ฝ”๋“œ์˜ 85%์—์„œ ๋ฒ„๊ทธ๋ฅผ ๊ฐ์ง€ํ•œ๋‹ค๋Š” ์˜๋ฏธ์ž…๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: 100% ์ปค๋ฒ„๋ฆฌ์ง€, 0% ํ…Œ์ŠคํŠธ

function addNewOrder(newOrder) {
  logger.log(`Adding new order ${newOrder}`);
  DB.save(newOrder);
  Mailer.sendMail(newOrder.assignee, `A new order was places ${newOrder}`);

  return { approved: true };
}

it("addNewOrder๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ณ , ์ด๋Ÿฌํ•œ ํ…Œ์ŠคํŠธ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค.", () => {
  addNewOrder({ assignee: "John@mailer.com", price: 120 });
}); // 100% ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ ๋‚˜์˜ค์ง€๋งŒ ์•„๋ฌด๊ฒƒ๋„ ํ™•์ธํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: mutation ํ…Œ์ŠคํŠธ ๋„๊ตฌ์ธ Stryker ๋ณด๊ณ ์„œ๋Š” ํ…Œ์ŠคํŠธ ๋˜์ง€ ์•Š์€ ์ฝ”๋“œ์˜ ์–‘์„ ๊ฐ์ง€ํ•˜๊ณ  ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.

alt text



โšช ๏ธ 4.4 ํ…Œ์ŠคํŠธ ๋ฆฐํ„ฐ๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋ฌธ์ œ ๋ฐฉ์ง€

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ESLint ํ”Œ๋Ÿฌ๊ทธ์ธ ์„ธํŠธ๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ํŒจํ„ด์„ ๊ฒ€์‚ฌํ•˜๊ณ  ๋ฌธ์ œ๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ธฐ ์œ„ํ•ด ํŠน๋ณ„ํžˆ ์ œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด eslint-plugin-mocha๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ๊ธ€๋กœ๋ฒŒ ์ˆ˜์ค€์—์„œ ์ž‘์„ฑ๋  ๋•Œ(describe() ๋ฌธ ์•„๋ž˜์— ์žˆ์ง€ ์•Š์Œ) ๋˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ๊ฑด๋„ˆ ๋›ฐ๊ณ  ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผ๋˜์—ˆ๋‹ค๋Š” ์ž˜๋ชป๋œ ๋ฏฟ์Œ์„ ๊ฐ€์งˆ ๋•Œ ๊ฒฝ๊ณ ํ•ฉ๋‹ˆ๋‹ค. ์œ ์‚ฌํ•˜๊ฒŒ, eslint-plugin-jest๋Š” ์˜ˆ๋ฅผ ๋“ค์–ด ํ…Œ์ŠคํŠธ์— ์•„๋ฌด๋Ÿฐ assertion์ด ์—†์„ ๋•Œ ๊ฒฝ๊ณ ํ•ฉ๋‹ˆ๋‹ค.(์•„๋ฌด๊ฒƒ๋„ ํ™•์ธํ•˜์ง€ ์•Š์Œ)


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: 90%์˜ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€์™€ 100%์˜ ๋…น์ƒ‰ ํ…Œ์ŠคํŠธ๋ฅผ ๋ณด๋ฉฐ ๋ฏธ์†Œ์ง“๋Š” ๊ฒƒ์€ ๋งŽ์€ ํ…Œ์ŠคํŠธ๊ฐ€ ์•„๋ฌด๊ฒƒ๋„ assertionํ•˜์ง€ ์•Š๊ณ  ๋งŽ์€ ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๊ฐ€ ๊ฑด๋„ˆ ๋›ฐ์–ด์ง„๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ๋•Œ ๊นŒ์ง€๋งŒ์ž…๋‹ˆ๋‹ค. ์ด ์ž˜๋ชป๋œ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์–ด๋–ค ๊ฒƒ๋„ ๋ฐฐํฌํ•˜์ง€ ์•Š์•˜๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: ์˜ค๋ฅ˜๋กœ ๊ฐ€๋“ ์ฐฌ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค, ์šด ์ข‹๊ฒŒ๋„ ๋ฆฐํ„ฐ๊ฐ€ ์žก์•˜์Šต๋‹ˆ๋‹ค.

describe("Too short description", () => {
  const userToken = userService.getDefaultToken() // *error:no-setup-in-describe, use hooks (sparingly) instead
  it("Some description", () => {});//* error: valid-test-description. Must include the word "Should" + at least 5 words
});

it.skip("Test name", () => {// *error:no-skipped-tests, error:error:no-global-tests. Put tests only under describe or suite
  expect("somevalue"); // error:no-assert
});

it("Test name", () => {*//error:no-identical-title. Assign unique titles to tests
});



์„น์…˜ 5๏ธโƒฃ: ์ง€์†์ ์ธ ํ†ตํ•ฉ



โšช ๏ธ 5.1 ๋ฆฐํ„ฐ๋ฅผ ํ’์„ฑํ•˜๊ฒŒ ๊ตฌ์„ฑํ•˜๊ณ  ๋ฆฐํŠธ ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š” ๋นŒ๋“œ๋ฅผ ์ค‘๋‹จํ•˜์‹ญ์‹œ์˜ค

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ๋ฆฐํ„ฐ๋Š” ๊ณต์งœ ์ ์‹ฌ์ด๋ฉฐ, 5๋ถ„์˜ ์„ค์ •๋งŒ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ง€์ผœ์ฃผ๊ณ  ์ž…๋ ฅ๊ณผ ๋™์‹œ์— ์ค‘์š”ํ•œ ๋ฌธ์ œ๋ฅผ ํฌ์ฐฉํ•˜๋Š” ์ž๋™ ์กฐ์ข… ์žฅ์น˜๋ฅผ ๊ฑฐ์ € ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฆฐํ„ฐ๊ฐ€ ์žฅ์‹(์„ธ๋ฏธ์ฝœ๋ก )์— ์ง€๋‚˜์ง€ ์•Š๋˜ ์‹œ๋Œ€๋Š” ์ง€๋‚˜๊ฐ”์Šต๋‹ˆ๋‹ค. ์š”์ฆ˜์˜ ๋ฆฐํ„ฐ๋Š” ์˜ฌ๋ฐ”๋ฅด๊ฒŒ throw๋˜์ง€ ์•Š๊ณ  ์ •๋ณด๊ฐ€ ์†์‹ค๋˜๋Š” ์˜ค๋ฅ˜์™€ ๊ฐ™์€ ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ๋ฅผ ํฌ์ฐฉ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ๊ทœ์น™ ์„ธํŠธ (ESLint standard ํ˜น์€ Airbnb style) ์œ„์—, ๋‹จ์–ธ๋ฌธ์ด ๋น ์ง„ ํ…Œ์ŠคํŠธ๋ฅผ ๋ฐœ๊ฒฌํ•ด์ฃผ๋Š” eslint-plugin-chai-expect์™€ ๊ฐ™์€ ํŠนํ™”๋œ ๋ฆฐํ„ฐ๋ฅผ ํฌํ•จํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์‹ญ์‹œ์˜ค. eslint-plugin-promise ๋Š” resolve๋˜์ง€ ์•Š๋Š” Promise๋ฅผ ๋ฐœ๊ฒฌํ•ด์ค๋‹ˆ๋‹ค (์ด๋Ÿฐ ์ฝ”๋“œ๋Š” ๊ณ„์†ํ•ด์„œ ์‹คํ–‰๋˜๋Š”๊ฒƒ์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค), eslint-plugin-security๋Š” DOS ๊ณต๊ฒฉ์— ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋Š” ์ทจ์•ฝํ•œ ์ •๊ทœ์‹์„ ๋ฐœ๊ฒฌํ•ด์ฃผ๋ฉฐ, eslint-plugin-you-dont-need-lodash-underscore๋Š” ์ฝ”๋“œ๊ฐ€ Lodash._map(...)๊ณผ ๊ฐ™์€ V8 ์ฝ”์–ด ๋ฉ”์†Œ๋“œ์˜ ์ผ๋ถ€์ธ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ๊ฒฝ๊ณ ํ•ด์ค๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ํ”„๋กœ๋•์…˜์ด ๊ณ„์† ๊นจ์ง€๋Š”๋ฐ ๋กœ๊ทธ์— ์—๋Ÿฌ์˜ stack trace๊ฐ€ ํ‘œ์‹œ๋˜์ง€ ์•Š๋Š” ์šฐ์šธํ•œ ๋‚ ์„ ์ƒ์ƒํ•ด๋ด…์‹œ๋‹ค. ์–ด๋–ป๊ฒŒ ๋œ ๊ฑธ๊นŒ์š”? ์‹ค์ˆ˜๋กœ ์ฝ”๋“œ๊ฐ€ ์—๋Ÿฌ๊ฐ€ ์•„๋‹Œ ๊ฐ์ฒด๋ฅผ ๋˜์ง€๊ณ  ์žˆ์–ด์„œ stack trace๊ฐ€ ์†์‹ค๋˜์—ˆ๋‹ค๋ฉด, ๋ฒฝ์— ๋จธ๋ฆฌ๋ฅผ ๋“ค์ด๋ฐ•๊ธฐ ๋”ฑ ์ข‹์„๊ฒƒ์ž…๋‹ˆ๋‹ค. 5๋ถ„์˜ ๋ฆฐํ„ฐ ์„ค์ •์œผ๋กœ ์ด๋Ÿฐ ์˜คํƒ€๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ํ•˜๋ฃจ๋ฅผ ์ง€์ผœ๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โœ ์ฝ”๋“œ ์˜ˆ์ œ

:thumbsdown: ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์˜ˆ: ์ž˜๋ชป๋œ Error ๊ฐ์ฒด๊ฐ€ ์‹ค์ˆ˜๋กœ throw๋˜์–ด ์ด ์˜ค๋ฅ˜์— ๋Œ€ํ•œ stack trace๊ฐ€ ๋‚˜ํƒ€๋‚˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์šด ์ข‹๊ฒŒ๋„ ESLint๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ”„๋กœ๋•์…˜ ๋ฒ„๊ทธ๋ฅผ ์žก์•„๋ƒ…๋‹ˆ๋‹ค.

alt text



โšช ๏ธ 5.2 ๋กœ์ปฌ ๊ฐœ๋ฐœ์ž CI๋กœ ํ”ผ๋“œ๋ฐฑ ์ฃผ๊ธฐ ๋‹จ์ถ•

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ํ…Œ์ŠคํŠธ, ๋ฆฐํŠธ, ์ทจ์•ฝ์  ํ™•์ธ ๋“ฑ๊ณผ ๊ฐ™์€ ๊ทผ์‚ฌํ•œ ํ’ˆ์งˆ ๊ฒ€์‚ฌ๊ฐ€ ํฌํ•จ๋œ CI๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๊นŒ? ๊ฐœ๋ฐœ์ž๊ฐ€ ์ด ํŒŒ์ดํ”„๋ผ์ธ์„ ๋กœ์ปฌ์—์„œ๋„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์„œ ํ”ผ๋“œ๋ฐฑ ์ฃผ๊ธฐ๋ฅผ ๋‹จ์ถ•ํ•˜์‹ญ์‹œ์˜ค. ์™œ? ํšจ์œจ์ ์ธ ํ…Œ์ŠคํŠธ ํ”„๋กœ์„ธ์Šค๋Š” ๋งŽ์€ ๋ฐ˜๋ณต ๋ฃจํ”„๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. (1) ์‹œ๋„ -> (2) ํ”ผ๋“œ๋ฐฑ -> (3) ๋ฆฌํŒฉํ„ฐ๋ง. ํ”ผ๋“œ๋ฐฑ์ด ๋น ๋ฅผ์ˆ˜๋ก ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ชจ๋“ˆ ๊ฐ๊ฐ์„ ๊ฐœ์„ ํ•˜๋ฉฐ ๊ฒฐ๊ณผ๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•œํŽธ, ํ”ผ๋“œ๋ฐฑ์ด ๋Šฆ์–ด์ง€๋ฉด์„œ ํ•˜๋ฃจ์— ๊ฐœ์„ ์ด ๋ฐ˜๋ณต๋˜๋Š” ๋นˆ๋„๊ฐ€ ์ ์–ด์ง„๋‹ค๋ฉด, ํŒ€์€ ์ด๋ฏธ ๋‹ค๋ฅธ ์ฃผ์ œ / ์ž‘์—… / ๋ชจ๋“ˆ๋กœ ๋„˜์–ด๊ฐˆ ์ˆ˜ ์žˆ์œผ๋ฉฐ ํ•ด๋‹น ๋ชจ๋“ˆ์˜ ์ˆ˜์ •์ด ์ด๋ฃจ์–ด์ง€์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์‹ค์ œ๋กœ ๋ช‡๋ช‡ CI ๊ณต๊ธ‰ ์—…์ฒด (์˜ˆ: CircleCI load CLI) ๋Š” ํŒŒ์ดํ”„๋ผ์ธ์˜ ๋กœ์ปฌ ์‹คํ–‰์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. wallaby์™€ ๊ฐ™์€ ๋ช‡๋ช‡ ์ƒ์šฉ ๋„๊ตฌ๋Š” ๊ฐœ๋ฐœ์ž ํ”„๋กœํ† ํƒ€์ž…์œผ๋กœ ๋†’์€ ๊ฐ€์น˜์˜ ํ…Œ์ŠคํŠธ ํ†ต์ฐฐ๋ ฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค(ํ˜‘์ฐฌ ์•„๋‹˜). ๋˜๋Š” ๋ชจ๋“  ํ’ˆ์งˆ ๊ด€๋ จ ๋ช…๋ น์–ด(์˜ˆ : test, lint, vulnerabilities)๋ฅผ ์‹คํ–‰ํ•˜๋Š” npm ์Šคํฌ๋ฆฝํŠธ๋ฅผ package.json์— ์ถ”๊ฐ€ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณ‘๋ ฌํ™”๋ฅผ ์œ„ํ•ด concurrently์™€ ๊ฐ™์€ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ๋ช…๋ น์–ด ์ค‘ ํ•˜๋‚˜๊ฐ€ ์‹คํŒจํ•  ๊ฒฝ์šฐ์—๋Š” 0์ด ์•„๋‹Œ ์ข…๋ฃŒ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ์ด์ œ ๊ฐœ๋ฐœ์ž๋Š” ํ•˜๋‚˜์˜ ๋ช…๋ น์„ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. โ€˜npm run qualityโ€™โ€” ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. githook์„ ์‚ฌ์šฉํ•˜์—ฌ ํ’ˆ์งˆ ๊ฒ€์‚ฌ์— ์‹คํŒจํ•œ ๊ฒฝ์šฐ ์ปค๋ฐ‹์„ ์ค‘๋‹จํ•˜๋Š” ๊ฒƒ๋„ ๊ณ ๋ คํ•˜์‹ญ์‹œ์˜ค (husky๊ฐ€ ๋„์›€๋  ์ˆ˜ ์žˆ์Œ)


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ ๋‹ค์Œ ๋‚ ์— ํ’ˆ์งˆ ๊ฒฐ๊ณผ๊ฐ€ ๋„์ฐฉํ•œ๋‹ค๋ฉด ํ…Œ์ŠคํŠธ๋Š” ๊ฐœ๋ฐœ ๊ณผ์ •์— ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํฌํ•จ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ์ฝ”๋“œ ํ’ˆ์งˆ ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” npm ์Šคํฌ๋ฆฝํŠธ๋Š” ์š”์ฒญ ์‹œ ๋˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์ƒˆ ์ฝ”๋“œ๋ฅผ ํ‘ธ์‹œํ•˜๋ ค๊ณ  ํ•  ๋•Œ ๋ชจ๋‘ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

"scripts": {
    "inspect:sanity-testing": "mocha **/**--test.js --grep \"sanity\"",
    "inspect:lint": "eslint .",
    "inspect:vulnerabilities": "npm audit",
    "inspect:license": "license-checker --failOn GPLv2",
    "inspect:complexity": "plato .",

    "inspect:all": "concurrently -c \"bgBlue.bold,bgMagenta.bold,yellow\" \"npm:inspect:quick-testing\" \"npm:inspect:lint\" \"npm:inspect:vulnerabilities\" \"npm:inspect:license\""
  },
  "husky": {
    "hooks": {
      "precommit": "npm run inspect:all",
      "prepush": "npm run inspect:all"
    }
}



โšช ๏ธ 5.3 ์‹ค์ œ ํ”„๋กœ๋•์…˜ ๋ฏธ๋Ÿฌ๋ฅผ ํ†ตํ•ด e2e ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: e2e ํ…Œ์ŠคํŠธ๋Š” ๋ชจ๋“  CI ํŒŒ์ดํ”„๋ผ์ธ์˜ ์ฃผ์š” ๊ณผ์ œ์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ด€๋ จ ํด๋ผ์šฐ๋“œ ์„œ๋น„์Šค๊ฐ€ ๋™์ผํ•œ ์ž„์‹œ ํ”„๋กœ๋•์…˜ ๋ฏธ๋Ÿฌ๋ฅผ ์ฆ‰์„์—์„œ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์€ ์ง€๋ฃจํ•˜๊ณ  ๋น„์šฉ์ด ๋งŽ์ด ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ตœ๊ณ ์˜ ํƒ€ํ˜‘์ ์„ ์ฐพ๋Š”๊ฒƒ์ด ๊ฒŒ์ž„์ž…๋‹ˆ๋‹ค. Docker-compose๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹จ์ผ ํ…์ŠคํŠธ ํŒŒ์ผ๋กœ ๋™์ผํ•œ ์ปจํ…Œ์ด๋„ˆ๋กœ ๊ฒฉ๋ฆฌ๋œ ๋„์ปค ํ™˜๊ฒฝ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์ง€๋งŒ ๋ฐฑ์—… ๊ธฐ์ˆ (์˜ˆ: ๋„คํŠธ์›Œํ‚น, ๋ฐฐํฌ ๋ชจ๋ธ)์€ ์‹ค์ œ ํ”„๋กœ๋•์…˜๊ณผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ์‹ค์ œ AWS ์„œ๋น„์Šค ๊ธฐ๋Šฅ๊ณผ ํ•จ๊ป˜ ๋™์ž‘ํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ์ด๋ฅผ 'AWS Local'๊ณผ ๊ฒฐํ•ฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Serverless์™€ Serverless๊ฐ™์€ ์—ฌ๋Ÿฌ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ AWS SAM์„ ์ด์šฉํ•ด ๋กœ์ปฌ์—์„œ FaaS ์ฝ”๋“œ๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฑฐ๋Œ€ํ•œ ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค ์ƒํƒœ๊ณ„๋Š” ๋งŽ์€ ์ƒˆ๋กœ์šด ๋„๊ตฌ๊ฐ€ ์ž์ฃผ ๋‚˜์˜ค์ง€๋งŒ, ์•„์ง ๋กœ์ปฌ ๋ฐ CI ๋ฏธ๋Ÿฌ๋ง์„ ์œ„ํ•œ ํŽธ๋ฆฌํ•œ ๋„๊ตฌ์˜ ํ‘œ์ค€์„ ๊ณต์‹ํ™”ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ•œ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์€ ์‹ค์ œ์™€ ๋น„์Šทํ•˜์ง€๋งŒ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์ ์€ Minikube ๋ฐ MicroK8s๊ณผ ๊ฐ™์€ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ '์ตœ์†Œํ™”๋œ ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค'๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋˜ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์€ ์›๊ฒฉ '์‹ค์ œ ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค'๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ผ๋ถ€ CI ๋ฒค๋”(์˜ˆ: Codefresh์€ ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค ํ™˜๊ฒฝ๊ณผ ํ†ตํ•ฉ๋˜์–ด ์žˆ์–ด์„œ ์‹ค์ œ ์ƒํ™ฉ์—์„œ CI ํŒŒ์ดํ”„๋ผ์ธ์„ ์‰ฝ๊ฒŒ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋˜ ์›๊ฒฉ ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค์— ๋Œ€ํ•œ ์‚ฌ์šฉ์ž ์ง€์ • ์Šคํฌ๋ฆฝํŒ…์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ํ”„๋กœ๋•์…˜ ๋ฐ ํ…Œ์ŠคํŠธ์— ์„œ๋กœ ๋‹ค๋ฅธ ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋‘ ๊ฐ€์ง€ ๋ฐฐํฌ ๋ชจ๋ธ์˜ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๊ณ  ๊ฐœ๋ฐœ์ž์™€ ์šด์˜ ํŒ€์ด ๋ถ„๋ฆฌ๋ฉ๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค ํด๋Ÿฌ์Šคํ„ฐ๋ฅผ ์ฆ‰์‹œ ์ƒ์„ฑํ•˜๋Š” CI ํŒŒ์ดํ”„๋ผ์ธ (๋™์  ํ™˜๊ฒฝ ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค)

deploy:
stage: deploy
image: registry.gitlab.com/gitlab-examples/kubernetes-deploy
script:
- ./configureCluster.sh $KUBE_CA_PEM_FILE $KUBE_URL $KUBE_TOKEN
- kubectl create ns $NAMESPACE
- kubectl create secret -n $NAMESPACE docker-registry gitlab-registry --docker-server="$CI_REGISTRY" --docker-username="$CI_REGISTRY_USER" --docker-password="$CI_REGISTRY_PASSWORD" --docker-email="$GITLAB_USER_EMAIL"
- mkdir .generated
- echo "$CI_BUILD_REF_NAME-$CI_BUILD_REF"
- sed -e "s/TAG/$CI_BUILD_REF_NAME-$CI_BUILD_REF/g" templates/deals.yaml | tee ".generated/deals.yaml"
- kubectl apply --namespace $NAMESPACE -f .generated/deals.yaml
- kubectl apply --namespace $NAMESPACE -f templates/my-sock-shop.yaml
environment:
name: test-for-ci



โšช ๏ธ 5.4 ํ…Œ์ŠคํŠธ ์‹คํ–‰์˜ ๋ณ‘๋ ฌํ™”

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์ œ๋Œ€๋กœ๋งŒ ํ•˜๋ฉด ํ…Œ์ŠคํŠธ๋Š” ๊ฑฐ์˜ ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ์„ ์ฃผ๋Š” 24/7 ์นœ๊ตฌ์ž…๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ๋‹จ์ผ ์Šค๋ ˆ๋“œ์—์„œ 500๊ฐœ์˜ CPU ์ œํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋Š”๋ฐ๋Š” ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์šด์ข‹๊ฒŒ๋„ ์ตœ์‹  ํ…Œ์ŠคํŠธ ๋Ÿฌ๋„ˆ์™€ CI ํ”Œ๋žซํผ(์˜ˆ: Jest, AVA, Mocha extension)์€ ํ…Œ์ŠคํŠธ๋ฅผ ์—ฌ๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋กœ ๋ณ‘๋ ฌํ™”ํ•˜์—ฌ ํ”ผ๋“œ๋ฐฑ ์‹œ๊ฐ„์„ ํฌ๊ฒŒ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ถ€ CI ๋ฒค๋”๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ปจํ…Œ์ด๋„ˆ ๊ฐ„ ๋ณ‘๋ ฌํ™”ํ•˜์—ฌ(!) ํ”ผ๋“œ๋ฐฑ ์‹œ๊ฐ„์„ ๋”์šฑ ๋‹จ์ถ•์‹œํ‚ต๋‹ˆ๋‹ค. ๊ฐ๊ฐ ๋‹ค๋ฅธ ํ”„๋กœ์„ธ์Šค์—์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰(๋กœ์ปฌ์—์„œ ์—ฌ๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋กœ ํ˜น์€ ์—ฌ๋Ÿฌ ๋จธ์‹ ์„ ์‚ฌ์šฉํ•˜๋Š” ์ผ๋ถ€ ํด๋ผ์šฐ๋“œ CLI๋ฅผ ํ†ตํ•ด)ํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ์ž๋™ํ™” ํ•˜์‹ญ์‹œ์˜ค.

โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ์ƒˆ ์ฝ”๋“œ๋ฅผ ํ‘ธ์‰ฌํ•˜๊ณ  ์ด๋ฏธ ๋‹ค์Œ ๊ธฐ๋Šฅ์„ ์ฝ”๋”ฉ ํ•  ๋•Œ, (ํ•œ ์‹œ๊ฐ„ ํ›„์—) ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ์–ป๋Š” ๊ฒƒ์€ ํ…Œ์ŠคํŠธ์˜ ๊ด€๋ จ์„ฑ์„ ๋–จ์–ด๋œจ๋ฆฌ๊ธฐ ์œ„ํ•œ ํ›Œ๋ฅญํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: Mocha Parallel๊ณผ Jest ๋Š” ํ…Œ์ŠคํŠธ ๋ณ‘๋ ฌํ™” ๋•๋ถ„์— ๋•๋ถ„์— ๊ธฐ์กด์˜ Mocha๋ฅผ ์‰ฝ๊ฒŒ ๋Šฅ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. (Credit: JavaScript Test-Runners Benchmark)

alt text



โšช ๏ธ 5.5 ๋ผ์ด์„ผ์Šค ๋ฐ ํ‘œ์ ˆ์„ ๊ฒ€์‚ฌํ•˜์—ฌ ๋ฒ•์  ๋ฌธ์ œ๋ฅผ ํ”ผํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ๋ผ์ด์„ผ์‹ฑ๊ณผ ํ‘œ์ ˆ ๋ฌธ์ œ๋Š” ๋‹น์žฅ ๋‹น์‹ ์˜ ์ฃผ์š” ๊ด€์‹ฌ์‚ฌ๊ฐ€ ์•„๋‹ ์ˆ˜ ์žˆ์ง€๋งŒ, 10๋ถ„ ์•ˆ์— ์ด ๋‚ด์šฉ์„ ํ™•์ธํ•˜์ง€ ์•Š์œผ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? ๋งŽ์€ npm ํŒจํ‚ค์ง€์˜ ๋ผ์ด์„ผ์Šค ์ฒดํฌ ๋ฐ ํ‘œ์ ˆ ํ™•์ธ(์ƒ์šฉ ๋„๊ตฌ์˜ ๋ฌด๋ฃŒ ํ”Œ๋žœ)์„ CI ํŒŒ์ดํ”„๋ผ์ธ์— ์‰ฝ๊ฒŒ ํฌํ•จ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ œํ•œ์ ์ธ ๋ผ์ด์„ผ์Šค์˜ ์ข…์†์„ฑ์ด๋‚˜ Stack Overflow์—์„œ ๋ณต์‚ฌํ•˜์—ฌ ๋ถ™์—ฌ๋„ฃ์€ ์ผ๋ถ€ ์ €์ž‘๊ถŒ์„ ์œ„๋ฐ˜ํ•œ ๊ฒƒ์œผ๋กœ ๋ณด์ด๋Š” ์ฝ”๋“œ๋ฅผ ์ ๊ฒ€ํ•˜์‹ญ์‹œ์˜ค.

โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ๊ฐœ๋ฐœ์ž๊ฐ€ ์˜๋„์น˜ ์•Š๊ฒŒ ๋ถ€์ ์ ˆํ•œ ๋ผ์ด์„ผ์Šค๊ฐ€ ํฌํ•จ๋œ ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์ƒ์šฉ ์ฝ”๋“œ๋ฅผ ๋ณต์‚ฌํ•˜์—ฌ ๋ฒ•์  ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ:

//install license-checker in your CI environment or also locally
npm install -g license-checker

//ask it to scan all licenses and fail with exit code other than 0 if it found unauthorized license. The CI system should catch this failure and stop the build
license-checker --summary --failOn BSD

alt text



โšช ๏ธ 5.6 ์ทจ์•ฝํ•œ ์ข…์†์„ฑ์„ ์ง€์†์ ์œผ๋กœ ๊ฒ€์‚ฌ

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: Express์™€ ๊ฐ™์ด ์ƒ๋‹นํžˆ ํ‰ํŒ์ด ์ข‹์€ ์ข…์†์„ฑ ์กฐ์ฐจ๋„ ์ทจ์•ฝ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. npm audit๊ณผ ๊ฐ™์€ ์ปค๋ฎค๋‹ˆํ‹ฐ ๋„๊ตฌ ๋˜๋Š” snyk๊ฐ™์€ ์ƒ์šฉ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‰ฝ๊ฒŒ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(๋ฌด๋ฃŒ ์ปค๋ฎค๋‹ˆํ‹ฐ ๋ฒ„์ „๋„ ์ œ๊ณต). ๋‘˜๋‹ค ๋‹น์‹ ์˜ CI์˜ ๋ชจ๋“  ๋นŒ๋“œ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ์ „์šฉ ๋„๊ตฌ ์—†์ด ์ฝ”๋“œ๋ฅผ ์ทจ์•ฝ์ ์œผ๋กœ๋ถ€ํ„ฐ ์•ˆ์ „ํ•˜๊ฒŒ ์œ ์ง€ํ•˜๋ ค๋ฉด ์ƒˆ๋กœ์šด ์œ„ํ˜‘์— ๋Œ€ํ•œ ์ตœ์‹  ์†Œ์‹์„ ์ง€์†์ ์œผ๋กœ ์ซ“์•„์•ผ ํ•ด์„œ ๋งค์šฐ ์ง€๋ฃจํ•ฉ๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: NPM Audit ๊ฒฐ๊ณผ

alt text



โšช ๏ธ 5.7 ์ข…์†์„ฑ ์—…๋ฐ์ดํŠธ ์ž๋™ํ™”

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: yarn๊ณผ npm์ด ์ตœ๊ทผ ์†Œ๊ฐœํ•œ package-lock.json๋Š” ์ค‘๋Œ€ํ•œ ๋„์ „์ž…๋‹ˆ๋‹ค(์ง€์˜ฅ์œผ๋กœ ๊ฐ€๋Š” ๊ธธ์€ ์ข‹์€ ์˜๋„๋กœ ํฌ์žฅ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค). ๊ธฐ๋ณธ์ ์œผ๋กœ ์ด์ œ ํŒจํ‚ค์ง€๋Š” ๋”์ด์ƒ ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. 'npm install'๊ณผ 'npm update'์„ ํ†ตํ•ด ์ƒˆ๋กœ์šด ๋ฐฐํฌ๋ฅผ ๋งŽ์ด ์ˆ˜ํ–‰ํ•˜๋Š” ํŒ€๋„ ์ƒˆ๋กœ์šด ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐ›์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ํ•˜์œ„ ์ข…์† ํŒจํ‚ค์ง€ ๋ฒ„์ „์ด ์ตœ์ƒ์ด๊ฑฐ๋‚˜, ์ตœ์•…์˜ ๊ฒฝ์šฐ์—๋Š” ์ทจ์•ฝํ•œ ์ฝ”๋“œ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ํŒ€์€ ์ด์ œ ํŒจํ‚ค์ง€๋ฅผ ์ˆ˜๋™์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•ด ๊ฐœ๋ฐœ์ž์˜ ์˜์ง€์™€ ๊ธฐ์–ต์— ์˜์กดํ•˜๊ฑฐ๋‚˜ ncu๊ฐ™์€ ๋„๊ตฌ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋ณด๋‹ค ์•ˆ์ •์ ์ธ ๋ฐฉ๋ฒ•์€ ๊ฐ€์žฅ ์•ˆ์ •์ ์ธ ์ข…์†์„ฑ ๋ฒ„์ „์„ ์–ป๋Š” ํ”„๋กœ์„ธ์Šค๋ฅผ ์ž๋™ํ™”ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ฌ˜์ฑ…์€ ์—†์ง€๋งŒ ๊ฐ€๋Šฅํ•œ ๋‘๊ฐ€์ง€ ์ž๋™ํ™” ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค:

(1) CI๋Š” 'npm outdated' ๋˜๋Š” 'npm-check-updates(ncu)'๊ฐ™์€ ํˆด์„ ์‚ฌ์šฉํ•˜์—ฌ ์˜ค๋ž˜๋œ ์ข…์†์„ฑ์„ ๊ฐ€์ง„ ๋นŒ๋“œ๋ฅผ ์‹คํŒจํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒํ•˜๋ฉด ๊ฐœ๋ฐœ์ž๊ฐ€ ์ข…์†์„ฑ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

(2) ์ฝ”๋“œ๋ฅผ ์Šค์บ”ํ•˜๊ณ  ์—…๋ฐ์ดํŠธ๋œ ์ข…์†์„ฑ์œผ๋กœ PR์„ ์ž๋™์œผ๋กœ ๋ณด๋‚ด์ฃผ๋Š” ์ƒ์šฉ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ๋‚จ์•„์žˆ๋Š” ํฅ๋ฏธ๋กœ์šด ์งˆ๋ฌธ ํ•˜๋‚˜๋Š” ์ข…์†์„ฑ ์—…๋ฐ์ดํŠธ ์ •์ฑ…์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ํŒจ์น˜์—์„œ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•˜๋ฉด ๋„ˆ๋ฌด ๋งŽ์€ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”์ด์ € ๋ฒ„์ „์ด ๊ณต๊ฐœ๋  ๋•Œ ๋ฐ”๋กœ ์—…๋ฐ์ดํŠธ ํ•˜๋ฉด ๋ถˆ์•ˆ์ •ํ•œ ๋ฒ„์ „์„ ๊ฐ€๋ฆฌํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.(๋งŽ์€ ํŒจํ‚ค์ง€๊ฐ€ ์ถœ์‹œ๋œ ์งํ›„ ์ฒซ ๋‚ ์— ์ทจ์•ฝํ•œ ๊ฒƒ์œผ๋กœ ๋ฐํ˜€ ์ง esline-scope์‚ฌ๊ฑด ์ฐธ์กฐ)

ํšจ์œจ์ ์ธ ์—…๋ฐ์ดํŠธ ์ •์ฑ…์€ ์ผ๋ถ€ 'ํˆฌ์ž ๊ธฐ๊ฐ„'์„ ํ—ˆ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋กœ์ปฌ ์‚ฌ๋ณธ์„ ๋ฒ„๋ฆฌ๊ธฐ ์ „์— ์ข…์†์„ฑ์˜ ์‹œ๊ฐ„๊ณผ ๋ฒ„์ „์„ @latest๋ณด๋‹ค ์•ฝ๊ฐ„ ๋’ค์ณ์ง€๊ฒŒ ํ•˜์‹ญ์‹œ์˜ค. (์˜ˆ: ๋กœ์ปฌ ๋ฒ„์ „์€ 1.3.1 ๋ ˆํŒŒ์ง€ํ† ๋ฆฌ ๋ฒ„์ „์€ 1.3.8)


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ๋‹น์‹ ์˜ ์ œํ’ˆ์€ ์ž‘์„ฑ์ž๊ฐ€ ์œ„ํ—˜ํ•˜๋‹ค๊ณ  ๋ช…์‹œ์ ์œผ๋กœ ํƒœ๊ทธํ•œ ํŒจํ‚ค์ง€๋ฅผ ์‹คํ–‰ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: ์ฝ”๋“œ๊ฐ€ ์ตœ์‹  ๋ฒ„์ „๋ณด๋‹ค ์–ด๋А์ •๋„ ๋’ค์ณ์ง€๋Š”์ง€ ๊ฐ์ง€ํ•˜๊ธฐ ์œ„ํ•˜์—ฌ ncu๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋˜๋Š” CI ํŒŒ์ดํ”„๋ผ์ธ ๋‚ด์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

alt text



โšช ๏ธ 5.8 ๊ธฐํƒ€, ๋…ธ๋“œ์™€ ๊ด€๋ จ์—†๋Š” CI ํŒ

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ์ด ๊ธ€์€ Node.js์™€ ๊ด€๋ จ์ด ์žˆ๊ฑฐ๋‚˜ ์ตœ์†Œํ•œ Node.js๋กœ ์˜ˆ๋ฅผ ๋“ค ์ˆ˜ ์žˆ๋Š” ํ…Œ์ŠคํŠธ ์กฐ์–ธ์— ์ค‘์ ์„ ๋‘๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๋ฒˆ์—๋Š” Node.js์™€ ๊ด€๋ จ์—†์ง€๋งŒ ์ž˜ ์•Œ๋ ค์ง„ ํŒ ๋ช‡๊ฐœ๋ฅผ ๊ทธ๋ฃนํ™” ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  1. ์„ ์–ธ์  ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ๋Œ€๋ถ€๋ถ„์˜ ๋ฒค๋”์—์„œ๋Š” ์„ ํƒํ•  ์ˆ˜ ์—†์ง€๋งŒ, ์ด์ „ ๋ฒ„์ „์˜ Jenkins์—์„œ ์ฝ”๋“œ ๋˜๋Š” UI๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. ๊ณ ์œ  Docker๋ฅผ ์ง€์›ํ•˜๋Š” ๋ฒค๋”๋ฅผ ์„ ํƒํ•˜์‹ญ์‹œ์˜ค.
  3. ์ผ์ฐ ์‹คํŒจํ•˜๊ณ  ๊ฐ€์žฅ ๋น ๋ฅธ ํ…Œ์ŠคํŠธ๋ฅผ ๋จผ์ € ์‹คํ–‰ํ•˜์‹ญ์‹œ์˜ค. ๋‹ค์–‘ํ•œ ๋น ๋ฅธ Inspection(์˜ˆ: ๋ฆฐํŠธ, ๋‹จ์œ„ํ…Œ์ŠคํŠธ)๋ฅผ ๊ทธ๋ฃนํ™” ํ•˜๊ณ  ์ฝ”๋“œ ์ปค๋ฏธํ„ฐ์— ๋Œ€ํ•œ ์‹ ์†ํ•œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋Š” ์Šค๋ชจํฌ ํ…Œ์ŠคํŠธ ๋‹จ๊ณ„/๋งˆ์ผ์Šคํ†ค์„ ๋งŒ๋“œ์‹ญ์‹œ์˜ค.
  4. ํ…Œ์ŠคํŠธ ๋ณด๊ณ ์„œ, ์ปค๋ฒ„๋ฆฌ์ง€, ๋ณ€ํ™”, ๋กœ๊ทธ ๋“ฑ์˜ ๋ชจ๋“  ๊ฒฐ๊ณผ๋ฌผ์„ ํ›‘์–ด๋ณด๊ธฐ ์‰ฝ๊ฒŒ ํ•˜์‹ญ์‹œ์˜ค.
  5. ๊ฐ ์ด๋ฒคํŠธ์— ๋Œ€ํ•ด ์—ฌ๋Ÿฌ ํŒŒ์ดํ”„๋ผ์ธ/์ž‘์—…์„ ์ž‘์„ฑํ•˜๊ณ , ๊ทธ ์‚ฌ์ด ๋‹จ๊ณ„๋ฅผ ์žฌ์‚ฌ์šฉ ํ•˜์‹ญ์‹œ์˜ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด, feature ๋ธŒ๋žœ์น˜ ์ปค๋ฐ‹์ด๋‚˜ ๋งˆ์Šคํ„ฐ PR์— ๋Œ€ํ•œ ์ž‘์—… ๊ตฌ์„ฑ. ๊ฐ ์žฌ์‚ฌ์šฉ ๋กœ์ง์ด ๊ณต์œ  ๋‹จ๊ณ„๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ํ•˜์‹ญ์‹œ์˜ค.(๋Œ€๋ถ€๋ถ„์˜ ๋ฒค๋”๋Š” ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ ์œ„ํ•œ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค)
  6. ์ž‘์—… ์„ ์–ธ์— ์–ด๋–คํ•œ๊ฒƒ๋„ ์ˆจ๊ฒจ๋†“์ง€ ๋งˆ์‹ญ์‹œ์˜ค.
  7. ๋ฆด๋ฆฌ์Šค ๋นŒ๋“œ์—์„œ ๋ช…์‹œ์ ์œผ๋กœ ๋ฒ„์ „์„ ์ถฉ๋Œ ์‹œ์ผœ๋ณด๊ฑฐ๋‚˜ ์ตœ์†Œํ•œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๊ทธ๋ ‡๊ฒŒํ–ˆ๋Š”์ง€ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค.
  8. ํ•œ๋ฒˆ๋งŒ ๋นŒ๋“œํ•˜๊ณ  ๋‹จ์ผ ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ(์˜ˆ: Docker ์ด๋ฏธ์ง€)์— ๋Œ€ํ•ด ๋ชจ๋“  ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜์‹ญ์‹œ์˜ค.
  9. ๋นŒ๋“œ๊ฐ„์— ์ƒํƒœ๊ฐ€ ๋ณ€ํ•˜์ง€ ์•Š๋Š” ์ž„์‹œ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธํ•˜์‹ญ์‹œ์˜ค. node_modules ์บ์‹ฑ์€ ์œ ์ผํ•œ ์˜ˆ์™ธ ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: ์ˆ˜๋…„๊ฐ„์˜ ๋…ธํ•˜์šฐ๋ฅผ ๋†“์น˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.



โšช ๏ธ 5.9 ๋นŒ๋“œ ๋งคํŠธ๋ฆญ์Šค: ์—ฌ๋Ÿฌ ๋…ธ๋“œ ๋ฒ„์ „์„ ์‚ฌ์šฉํ•ด์„œ ๋™์ผํ•œ CI ๋‹จ๊ณ„๋ฅผ ์‹คํ–‰ ํ•˜์‹ญ์‹œ์˜ค.

:white_check_mark: ์ด๋ ‡๊ฒŒ ํ•ด๋ผ: ํ’ˆ์งˆ ๊ฒ€์‚ฌ๋Š” ์„ธ๋Ÿฐ๋””ํ”ผํ‹ฐ์— ๊ด€ํ•œ ๊ฒƒ์œผ๋กœ, ๋ฌธ์ œ๋ฅผ ์กฐ๊ธฐ์— ๋ฐœ๊ฒฌํ•˜๋Š”๋ฐ ๋„์›€์ด ๋˜๋Š” ๋” ๋งŽ์€ ๊ธฐํšŒ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํŒจํ‚ค์ง€๋ฅผ ๊ฐœ๋ฐœํ•˜๊ฑฐ๋‚˜ ๋‹ค์–‘ํ•œ ๊ตฌ์„ฑ ๋ฐ ๋…ธ๋“œ ๋ฒ„์ „์œผ๋กœ ์—ฌ๋Ÿฌ ๊ณ ๊ฐ์˜ ์ œํ’ˆ์„ ์‹คํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ, CI๋Š” ๋ชจ๋“  ๊ตฌ์„ฑ์˜ ์ˆœ์—ด์— ๋Œ€ํ•ด ํ…Œ์ŠคํŠธ ํŒŒ์ดํ”„ ๋ผ์ธ์„ ์‹คํ–‰ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ผ๋ถ€ ๊ณ ๊ฐ์€ MySQL์„ ์‚ฌ์šฉํ•˜๊ณ  ๋‹ค๋ฅธ ๊ณ ๊ฐ์€ PostgreSQL์„ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ์‹œ๋‹ค. ์ผ๋ถ€ CI ๋ฒค๋”๋Š” '๋งคํŠธ๋ฆญ์Šค'๋ผ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์—ฌ MySQL, PostgreSQL ๋ฐ 8, 9, 10๊ณผ ๊ฐ™์€ ์—ฌ๋Ÿฌ ๋…ธ๋“œ ๋ฒ„์ „์˜ ๋ชจ๋“  ์ˆœ์—ด์— ๋Œ€ํ•ด ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ์—๋Š” ์–ด๋– ํ•œ ์ถ”๊ฐ€ ๋…ธ๋ ฅ์—†์ด ๊ตฌ์„ฑ(์„ค์ •)๋งŒ์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค(ํ…Œ์ŠคํŠธ ๋˜๋Š” ๊ธฐํƒ€ ํ’ˆ์งˆ ๊ฒ€์‚ฌ๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •). ๋งคํŠธ๋ฆญ์Šค๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋‹ค๋ฅธ CI๋Š” ํ™•์žฅ์ด๋‚˜ ์กฐ์ •์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โŒ ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด: So after doing all that hard work of writing testing are we going to let bugs sneak in only because of configuration issues?


โœ ์˜ˆ์ œ ์ฝ”๋“œ

:clap: ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: Travis(CI ๋ฒค๋”) ๋นŒ๋“œ ์ •์˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—ฌ๋Ÿฌ ๋…ธ๋“œ ๋ฒ„์ „์— ๋Œ€ํ•œ ๋™์ผํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜์‹ญ์‹œ์˜ค.

language: node_js
node_js:
- "7"
- "6"
- "5"
- "4"
install:
- npm install
script:
- npm run test



ํŒ€

Yoni Goldberg



Role: ์ €์ž

About: ์ €๋Š” ํฌ์ถ˜ 500๋Œ€ ๊ธฐ์—… ๋ฐ ์Šคํƒ€ํŠธ์—…๊ณผ ํ•จ๊ป˜ JS ๋ฐ Node.js ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•˜๋Š” ๋…๋ฆฝ ์ปจ์„คํ„ดํŠธ์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์–ด๋–ค ์ฃผ์ œ๋ณด๋‹ค ๋” ํฅ๋ฏธ๋ฅผ ๋„๋Š” ํ…Œ์ŠคํŠธ ๊ธฐ์ˆ ์„ ์Šต๋“ํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ Node.js Best Practices์˜ ์ €์ž์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.


Workshop: ๐Ÿ‘จโ€๐Ÿซ ์ด๋Ÿฌํ•œ ๋ชจ๋“  ํ”„๋ž™ํ‹ฐ์Šค์™€ ๊ธฐ์ˆ ์„ ๋ฐฐ์šฐ๊ณ  ์‹ถ์Šต๋‹ˆ๊นŒ?(์œ ๋Ÿฝ & ๋ฏธ๊ตญ) ํ…Œ์ŠคํŠธ ์›Œํฌ์ƒต์— ๋“ฑ๋กํ•˜์‹ญ์‹œ์˜ค


Follow:




Bruno Scheufler

Role: ๊ธฐ์ˆ  ๊ฒ€ํ†  ๋ฐ ๊ณ ๋ฌธ

๋ชจ๋“  ํ…์ŠคํŠธ๋ฅผ ์ˆ˜์ •, ๊ฐœ์„ , lint ๋ฐ ๋‹ค๋“ฌ์—ˆ์Šต๋‹ˆ๋‹ค.

About: ํ’€ ์Šคํƒ ์›น ์—”์ง€๋‹ˆ์–ด, Node.js ๋ฐ GraphQL์˜ ์—ด๋ ฌํ•œ ์ง€์ง€์ž



Ido Richter

Role: ์ปจ์…‰, ๋””์ž์ธ ๋ฐ ํ›Œ๋ฅญํ•œ ์กฐ์–ธ

About: ์ •ํ†ตํ•œ ํ”„๋ก ํŠธ ์—”๋“œ ๊ฐœ๋ฐœ์ž, CSS ์ „๋ฌธ๊ฐ€ ๋ฐ ์ด๋ชจํ‹ฐ์ฝ˜์— ๊ด€์‹ฌ์ด ๋งŽ์€ ์‚ฌ๋žŒ