๐ ์ด ๊ฐ์ด๋๊ฐ ๋น์ ์ ํ ์คํธ ๊ธฐ์ ์ ํ ๋จ๊ณ ๋์ด ์ฌ๋ฆฌ๋ ์ด์
May 9, 2022 ยท View on GitHub
๐ ์ด ๊ฐ์ด๋๊ฐ ๋น์ ์ ํ ์คํธ ๊ธฐ์ ์ ํ ๋จ๊ณ ๋์ด ์ฌ๋ฆฌ๋ ์ด์
๐ ์ฒ ์ ํ๊ณ ๋งค์ฐ ํฌ๊ด์ ์ธ 45๊ฐ์ง ์ด์์ ๋ชจ๋ฒ ์ฌ๋ก
JavaScript ๋ฐ Node.js์ ๋ํ A๋ถํฐ Z๊น์ง์ ๋ฏฟ์์งํ ๊ฐ์ด๋์ ๋๋ค. ์์ญ ๊ฐ์ง ์ต๊ณ ์ ๋ธ๋ก๊ทธ ๊ฒ์๋ฌผ, ์์ ๋ฐ ๋๊ตฌ๋ฅผ ์์ฝํ๊ณ ์ ๋ฆฌํฉ๋๋ค.
๐ข ๊ธฐ์ด๋ฅผ ๋ฐ์ด๋์ด ๊ณ ๊ธ์ผ๋ก
์ด์์ค์ธ ์ ํ์ ํ ์คํธ, ๋์ฐ๋ณ์ด ํ ์คํธ, ์์ฑ ๊ธฐ๋ฐ ํ ์คํธ ๋ฐ ๊ธฐํ ์ฌ๋ฌ ์ ๋ต์ & ์ ๋ฌธ ๋๊ตฌ์ ๊ฐ์ ๊ณ ๊ธ ์ฃผ์ ๋ก ๋์ด๊ฐ๋ ์ฌ์ ์ ๊ฒฝํํ์ญ์์ค. ์ด ๊ฐ์ด๋์ ๋ชจ๋ ๋จ์ด๋ฅผ ์ฝ์ผ๋ฉด ๋น์ ์ ํ ์คํธ ๊ธฐ์ ์ด ํ๊ท ๋ณด๋ค ๋์์ง ์ ์์ต๋๋ค.
๐ Full-stack: ํ๋ก ํธ, ๋ฐฑ์๋, CI, ๋ฌด์์ด๋
๋ชจ๋ ์์ฉํ๋ก๊ทธ๋จ ๊ณ์ธต์ ๊ธฐ์ด๊ฐ ๋๋ ์ ๋น์ฟผํฐ์ค ํ ์คํธ ๋ฐฉ๋ฒ์ ์ดํดํ๋ ๊ฒ์ผ๋ก๋ถํฐ ์์ํ์ญ์์ค. ๊ทธ๋ฐ ๋ค์ ํ๋ก ํธ์๋/UI, ๋ฐฑ์๋, CI ํน์ ์ด ๋ชจ๋ ๊ฒ์ ๊ณต๋ถํ์ธ์.
Yoni Goldberg ์์ฑ
- JavaScript & Node.js ์ปจ์คํดํธ
- ๐จโ๐ซ ๋์ ํ ์คํ ์ํฌ์ต - ์ ๋ฝ๊ณผ ๋ฏธ๊ตญ์์์ ์ ์ํฌ์ต์ ๋ํด์ ์์๋ณด์ญ์์ค.
- ํธ์ํฐ ํ๋ก์ฐ ํ๊ธฐ
- LA, ๋ฒ ๋ก๋, ํ๋ฅดํค์ฐ, ๋ฌด๋ฃ ์จ๋น๋๋ฅผ ๋ค์ผ๋ฌ ์ค์ญ์์ค. ํฅํ ์ด๋ฒคํธ๋ ๊ณง ๊ฒฐ์ ๋ ๊ฒ์ ๋๋ค.
- ์ ์ JavaScript ๋ด์ค ๋ ํฐ - ์ธ์ฌ์ดํธ์ ์ค์ง ์ ๋ต์ ์ธ ๋ฌธ์ ์ ๋ํ ๋ด์ฉ
๋ชฉ์ฐจ
์น์
0: ํฉ๊ธ๋ฅ
๋ชจ๋ ๋ชจ๋ ์ฌ๋๋ค์๊ฒ ์๊ฐ์ ์ฃผ๋ ํ๋์ ์กฐ์ธ(ํ๋์ ํน์ํ ํญ๋ชฉ)
์น์
1: ํ
์คํธ ํด๋ถ
๊ธฐ์ด - ๊น๋ํ ํ ์คํธ ๊ตฌ์ฑํ๊ธฐ(12๊ฐ)
์น์
2: ๋ฐฑ์๋
๋ฐฑ์๋ ๋ฐ ๋ง์ดํฌ๋ก์๋น์ค ํ ์คํธ ํจ์จ์ ์ผ๋ก ์์ฑํ๊ธฐ(8๊ฐ)
์น์
3: ํ๋ก ํธ์๋
์ปดํฌ๋ํธ ๋ฐ E2E ํ ์คํธ๋ฅผ ํฌํจํ ์น UI์ ๋ํ ํ ์คํธ ์์ฑํ๊ธฐ(11๊ฐ)
์น์
4: ํ
์คํธ ํจ๊ณผ ์ธก์
๊ฐ์์๋ฅผ ๊ฐ์ํ๊ธฐ - ํ ์คํธ ํ์ง ์ธก์ (4๊ฐ)
์น์
5: ์ง์์ ์ธ ํตํฉ
์๋ฐ์คํฌ๋ฆฝํธ ์ธ๊ณ์์ CI์ ๋ํ ์ง์นจ(9๊ฐ)
์น์ 0๏ธโฃ: ํฉ๊ธ๋ฅ
โช ๏ธ 0 ํฉ๊ธ๋ฅ : ๋ฆฐ ํ ์คํธ๋ฅผ ์ํ ์ค๊ณ
:white_check_mark: ์ด๋ ๊ฒ ํด๋ผ: ํ ์คํธ ์ฝ๋๋ ์ ํ ์ฝ๋์ ๋ค๋ฆ ๋๋ค. ๋จ์ํ๊ณ , ์งง๊ณ , ์ถ์ํ๊ฐ ์๊ณ , ๋ฌด๋ํ๊ณ , ์์ ํ๊ธฐ์ ํธ๋ฆฌํ๊ณ , ๋ฆฐํ๊ฒ ๋์์ธ ํ์ญ์์ค. ํ ์คํธ๋ฅผ ๋ณด๊ณ ์ฆ์ ์๋ฏธ๋ฅผ ์์์ฑ ์ ์์ด์ผ ํฉ๋๋ค.
์ฐ๋ฆฌ ๋จธ๋ฆฌ์์ ์ ํ ์ฝ๋๋ก ๊ฐ๋ํ๊ณ ๋ถ๊ฐ์ ์ธ ๋ณต์กํ ๊ฒ๋ค์ ์๊ฐํ ์ฌ์ ๊ฐ ์์ต๋๋ค. ๋ ๋ค๋ฅธ ์ด๋ ค์ด ์ฝ๋๋ฅผ ์ต์ง๋ก ์๊ฐํด๋ด๋ ค๊ณ ํ๋ค๋ฉด, ํ์ ์๋๋ฅผ ๋ฆ์ถ๊ฒ ๋์ด ์ฐ๋ฆฌ๊ฐ ํ ์คํธ๋ฅผ ํ๋ ์ด์ ๊ฐ ๋ฌด์ํด ์ง ๊ฒ์ ๋๋ค. ์ค์ ๋ก ๋ง์ ํ๋ค์ด ์ด๋ฐ ์ด์ ๋ฅผ ํ ์คํธ๋ฅผ ํฌ๊ธฐํฉ๋๋ค.
ํ ์คํธ๋ ์น์ ํ๊ณ ์๋ ๋๋ฃ์ ํจ๊ป ์ผํ๋ ๊ฒ์ด ์ฆ๊ฑฐ์ธ ์ ์๋ ๊ธฐํ์ด๊ณ , ์ ์ ํฌ์๋ก ํฐ ๊ฐ์น๋ฅผ ์ ๊ณตํ๋ ๊ฒ์ ๋๋ค. ๊ณผํ์ ์ฐ๋ฆฌ์๊ฒ ๋ ๊ฐ์ ๋ ์์คํ ์ด ์๋ค๊ณ ๋งํฉ๋๋ค. ๋น ๋๋ก์์ ์๋์ฐจ๋ฅผ ์ด์ ํ๋ ๋ฑ์ ๊ฐํธํ ํ๋์ ์ฌ์ฉ๋๋ ์์คํ 1, ๊ทธ๋ฆฌ๊ณ ์ํ ๋ฐฉ์ ์์ ํธ๋ ๊ฒ๊ณผ ๊ฐ์ด ๋ณต์กํ๊ณ ์์์ ์ธ ์ฐ์ฐ์ ์ํ ์์คํ 2. ํ ์คํธ ์ฝ๋๋ฅผ ๋ณผ ๋ ์ํ ๋ฌธ์ ๋ฅผ ํธ๋ ๊ฒ ๊ฐ์๊ฒ ์๋, HTML ๋ฌธ์๋ฅผ ์์ ํ๋ ๊ฒ๋ง ํผ ์ฌ์์ผํ๋ ์์คํ 1์ ๋ง๊ฒ ํ ์คํธ๋ฅผ ์ค๊ณํ์ญ์์ค.
์ ํ์ ์ธ ์ฒด๋ฆฌํฝ ๊ธฐ์ , ํด ๊ทธ๋ฆฌ๊ณ ๋น์ฉ-ํจ์จ์ ์ด๊ณ ๋ฐ์ด๋ ROI๋ฅผ ์ ๊ณตํ๋ ํ ์คํธ ๋์ ์ ์ ์ผ๋ก ์ด๋ฌํ ๋ชฉ์ ์ ๋ฌ์ฑํ ์ ์์ต๋๋ค. ํ์ํ ๋งํผ์ ํ ์คํธ, ์ตํต์ฑ ์๊ฒ ์ ์งํ๋ ค๋ ๋ ธ๋ ฅ, ๋๋ก๋ ์ ์์ผํจ๊ณผ ๋จ์์ฑ์ ์ํด ์ผ๋ถ ํ ์คํธ์ ์ ๋ขฐ์ฑ์ ํฌ๊ธฐํ๋ ๊ฒ๋ ๊ฐ์น๊ฐ ์์ต๋๋ค.

์๋ ๋๋ถ๋ถ์ ์กฐ์ธ์ ์ด ์์น์ ํ์์ ๋๋ค.
์์ํ ์ค๋น ๋์ จ๋์?
์น์ 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: ์ฌ๋ฐ๋ฅธ ์: ์ธ ๋ถ๋ถ์ผ๋ก ๊ตฌ์ฑ๋ ํ ์คํธ ์ด๋ฆ

โช ๏ธ 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โ์์ ํ๋ถํ ํ
์คํธ ํฌํธํด๋ฆฌ์ค๋ฅผ ์ ์ํฉ๋๋ค. 
์์ : YouTube: โBeyond Unit Tests: 5 Shiny Node.JS Test Types (2018)โ (Yoni Goldberg)

โช ๏ธ2.2 ์ปดํฌ๋ํธ ํ ์คํธ๊ฐ ์ต์ ์ ๋ฐฉ๋ฒ์ผ ์ ์๋ค.
:white_check_mark: ์ด๋ ๊ฒ ํด๋ผ: ๊ฐ๊ฐ์ ๋จ์ ํ ์คํธ๋ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋งค์ฐ ์์ ๋ถ๋ถ๋ง์ ์ปค๋ฒํ๊ณ ์ ์ฒด๋ฅผ ๋ชจ๋ ์ปค๋ฒํ๊ธฐ์๋ ๋น์ฉ์ด ๋ง์ด ๋ญ๋๋ค. ๋ฐ๋ฉด์, end-to-end ํ ์คํธ๋ ๊ฐ๋จํ๊ฒ ๋ง์ ๋ถ๋ถ์ ์ปค๋ฒํ ์ ์์ง๋ง ๊น์ด๊ฐ ์๊ณ ๋ ๋๋ฆฝ๋๋ค. ๊ทธ๋ ๋ค๋ฉด ๊ท ํ ์กํ ์ ๊ทผ๋ฒ์ ์ ์ฉํ์ฌ ๋จ์ ํ ์คํธ๋ณด๋ค๋ ํฌ์ง๋ง end-to-end ํ ์คํธ๋ณด๋ค๋ ์์ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๊ฒ์ ์ด๋จ๊น์? ์ปดํฌ๋ํธ ํ ์คํธ๋ ํ ์คํธ ์ธ๊ณ์์ ์ ์๋ ค์ง์ง ์์ ๋ฐฉ๋ฒ์ ๋๋ค. - ์ปดํฌ๋ํธ ํ ์คํธ๋ ๋ค์์ ๋ ๊ฐ์ง ์ด์ ์ ๋ชจ๋ ์ ๊ณตํฉ๋๋ค: ํฉ๋ฆฌ์ ์ธ ์ฑ๋ฅ๊ณผ TDD ํจํด์ ์ ์ฉํ ์ ์๋ ๊ฐ๋ฅ์ฑ + ํ์ค์ ์ด๋ฉด์ ํ๋ฅญํ ์ปค๋ฒ๋ฆฌ์ง
์ปดํฌ๋ํธ ํ
์คํธ๋ ๋ง์ดํฌ๋ก ์๋น์ค '๋จ์'์ ์ค์ ์ ๋๊ณ API์ ๋ํ์ฌ ๋์ํฉ๋๋ค. ๋ง์ดํฌ๋ก์๋น์ค ๊ทธ ์์ฒด์ ์ํ ๊ฒ๋ค (์๋ฅผ๋ค๋ฉด, ์ค์ DB ๋๋ ํด๋น DB์ ์ธ-๋ฉ๋ชจ๋ฆฌ ๋ฒ์ )์ ๋ชจํน(Mock)ํ์ง ์๊ณ , ๋ค๋ฅธ ๋ง์ดํฌ๋ก์๋น์ค ํธ์ถ๊ณผ ๊ฐ์ ์ธ๋ถ์ ์ธ ๊ฒ์ ์คํ
(Stub)ํฉ๋๋ค. ๊ทธ๋ ๊ฒ ํจ์ผ๋ก์จ ์ฐ๋ฆฌ๋ ์ฐ๋ฆฌ๊ฐ ๋ฐฐํฌํ๋ ๊ฒ์ ํ
์คํธํ๊ณ ์ดํ๋ฆฌ์ผ์ด์
์ ๋ฐ๊นฅ์ชฝ์์ ์์ชฝ์ผ๋ก ์ ๊ทผํ๋ฉฐ, ์ ๋นํ ์๊ฐ ์์์ ํฐ ์์ ๊ฐ์ ์ป์ ์ ์์ต๋๋ค.
โ ๊ทธ๋ ์ง ์์ผ๋ฉด: ์์คํ ์ปค๋ฒ๋ฆฌ์ง๊ฐ 20%์ ๋ถ๊ณผํ๋ค๋ ๊ฒ์ ๊นจ๋ซ๊ธฐ๊น์ง ๋จ์ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๋ฐ ์ค๋ ์๊ฐ์ด ๊ฑธ๋ฆด ์ ์์ต๋๋ค.
โ ์ฝ๋ ์์
:clap: ์ฌ๋ฐ๋ฅธ ์: Supertest๋ฅผ ํตํด ํ๋ก์ธ์ค ๋ด Express API์ ์ ๊ทผํ ์ ์์ต๋๋ค. (๋น ๋ฅด๊ณ ๋ค์ํ ๊ณ์ธต์ ์ปค๋ฒํจ)
 allows approaching Express API in-process (fast and cover many layers) alt text](assets/bp-13-component-test-yoni-goldberg.png)
โช ๏ธ2.3 ์ ๊ท ๋ฆด๋ฆฌ์ฆ๊ฐ API ์ฌ์ฉ์ ๊นจ์ง๊ฒ ํ์ง ๋ง์ญ์์ค.
:white_check_mark: ์ด๋ ๊ฒ ํด๋ผ: ๋น์ ์ ๋ง์ดํฌ๋ก์๋น์ค๋ ๋ค์์ ํด๋ผ์ด์ธํธ๋ฅผ ๊ฐ์ง๊ณ ์๊ณ ํธํ์ฑ์ ์ด์ ๋ก ์ฌ๋ฌ ๋ฒ์ ์ ์๋น์ค๋ฅผ ์ด์ํฉ๋๋ค (๋ชจ๋ ์ฌ๋์ ๋ง์กฑ์ํค๊ธฐ ์ํด์). ๊ทธ๋ฐ ์ํฉ์์ ๋น์ ์ด ์ผ๋ถ ํ๋๋ฅผ ๋ณ๊ฒฝํ๋ฉด ์ด ํ๋๋ฅผ ๋ฏฟ๊ณ ์ฌ์ฉํ๋ ์ผ๋ถ ์ค์ํ ํด๋ผ์ด์ธํธ๋ ํ๊ฐ ๋ ๊ฒ์ ๋๋ค. ์ด๊ฒ์ ํตํฉ(integration) ์ธ๊ณ์์ ํด๊ฒฐํ๊ธฐ ์ด๋ ค์ด ์งํด์๋์ ๋์ธ ๋ฌธ์ ์ ๋๋ค: ์๋ฒ ์ฌ์ด๋๊ฐ ์ฌ๋ฌ ํด๋ผ์ด์ธํธ๋ค์ ๋ชจ๋ ๊ธฐ๋๊ฐ์ ๊ณ ๋ คํ๋ ๊ฒ์ ๋งค์ฐ ์ด๋ ค์ด ์ผ์ ๋๋ค. - ๋ฐ๋ฉด์, ์๋ฒ๊ฐ ๋ฆด๋ฆฌ์ฆ ๋ ์ง๋ฅผ ๊ฒฐ์ ํ๊ธฐ ๋๋ฌธ์ ํด๋ผ์ด์ธํธ๋ ์ด๋ ํ ํ ์คํธ๋ ์ํํ ์ ์์ต๋๋ค. ์๋น์ ์ฃผ๋ ๊ณ์ฝ ํ ์คํธ(Consumer-driven contracts)์ PACT ํ๋ ์์ํฌ๋ ๋งค์ฐ ํ๊ดด์ ์ธ ๋ฐฉ๋ฒ์ผ๋ก ์ด๋ฌํ ํ๋ก์ธ์ค๋ฅผ ํ์คํํ๊ธฐ ์ํด ๋ํ๋ฌ์ต๋๋ค. - ์๋ฒ๊ฐ ์๋ฒ์ ํ ์คํธ ๊ณํ์ ๊ฒฐ์ ํ์ง ์๊ณ , ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ํ ์คํธ๋ฅผ ๊ฒฐ์ ํฉ๋๋ค! PACT๋ ํด๋ผ์ด์ธํธ์ ๊ธฐ๋๊ฐ์ ๊ธฐ๋กํ์ฌ "๋ธ๋ก์ปค"๋ผ๋ ๊ณต์ ๋ ์์น์ ์ฌ๋ ค๋ ์ ์์ต๋๋ค. ๊ทธ๋ฌ๋ฉด ์๋ฒ๋ ๊ทธ ๊ธฐ๋๊ฐ์ ๋น๊ฒจ ๋ฐ์ ์ ์๊ณ ๋น๋ํ ๋๋ง๋ค PACT ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ๊นจ์ง ๊ณ์ฝ(contract - ์ถฉ์กฑ๋์ง ์์ ํด๋ผ์ด์ธํธ์ ๊ธฐ๋๊ฐ)์ ๊ฐ์งํ ์ ์์ต๋๋ค. ์ด๋ ๊ฒ ํจ์ผ๋ก์จ, ๋ชจ๋ ์๋ฒ-ํด๋ผ์ด์ธํธ API ๊ฐ ์ผ์นํ์ง ์์ ๊ฒ๋ค์ ๋น๋/CI ํ๊ฒฝ์์ ์กฐ๊ธฐ์ ์ก์ ์ ์๊ณ ๋น์ ์ ํฐ ์ ๋ง๊ฐ์ ์ค์ฌ์ค ์ ์์ ๊ฒ์ ๋๋ค.
โ ๊ทธ๋ ์ง ์์ผ๋ฉด: ๋์์ ์๋ ๋ฐฐํฌ๋ ๋ฐฐํฌ์ ๋ํ ๋๋ ค์์ ์๊ณ ๊ฐ๋ ๊ฒ ๋ฟ์ ๋๋ค.
โช ๏ธ 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:

โช ๏ธ 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)๋ฅผ ๋ง๋ค์์ต๋๋ค.
โ ๊ทธ๋ ์ง ์์ผ๋ฉด: ํ์ถ๊ตฌ๋ ์์ต๋๋ค. ๋จธํผ์ ๋ฒ์น์ ์๋น์์ด ๋น์ ์ ์์คํ ์ ํ๊ฒฉ์ ์ค ๊ฒ์ ๋๋ค.
โช ๏ธ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์ ์ค์ ์ค๋ฅ ๋ฑ์ ์ด์ ๋๋ฌธ์ ๋๋ฌด ๋๋ฆฐ ์๋น์ค๋ฅผ ์ ๊ณตํ๊ฒ ๋ ์ ์์ต๋๋ค.
โช ๏ธ 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

: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: ์์ : ์ผ๋ฐ์ ์ธ ์ปค๋ฒ๋ฆฌ์ง ๋ณด๊ณ ์

:clap: ์ฌ๋ฐ๋ฅธ ์: ์ปดํฌ๋ํธ ๋น ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ค์ ํ์ญ์์ค. (Jest๋ฅผ ์ฌ์ฉํ์ฌ)

โช ๏ธ 4.2 ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ๋ฅผ ํ์ธํ์ฌ ํ ์คํธ ๋์ง ์์ ๋ถ๋ถ๊ณผ ๊ธฐํ ์ด์ํ ์ ๋ค์ ๊ฐ์งํ์ญ์์ค.
:white_check_mark: ์ด๋ ๊ฒ ํด๋ผ: ์ผ๋ถ ๋ฌธ์ ๋ค์ ๋ ์ด๋๋ง ์๋๋ก ์จ์ด๋ฒ๋ ค ๊ธฐ์กด์ ํด๋ค์ ์ฌ์ฉํ์ฌ ์ฐพ๊ธฐ ๋งค์ฐ ์ด๋ ต์ต๋๋ค. ์ด๊ฒ๋ค์ ์ค์ ๋ก ๋ฒ๊ทธ๋ ์๋์ง๋ง ์ฌ๊ฐํ ์ํฅ์ ์ค ์ ์๋ ์๊ฐ์ง ๋ชป ํ ์ดํ๋ฆฌ์ผ์ด์
๋์๋ค์
๋๋ค. ์๋ฅผ ๋ค์ด, ์ผ๋ถ ์ฝ๋ ์์ญ์ ์ ๋ ๋๋ ๊ฑฐ์ ํธ์ถ๋์ง ์์ต๋๋ค. - โPricingCalculatorโ๋ผ๋ ์ํ ๊ฐ๊ฒฉ์ ์ค์ ํ๋ ํด๋์ค๊ฐ ์๋ค๊ณ ์๊ฐํด ๋ณด์ธ์. DB์ 100000๊ฐ์ ์ํ์ด ์๊ณ ํ๋งค๋ ๋ง์ง๋ง ์ด ํด๋์ค๋ ์ค์ ๋ก ์ ๋ ํธ์ถ๋์ง ์๋ ๊ฒ์ผ๋ก ๋ฐํ์ก์ต๋๋ค... ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ๋ฅผ ํตํด ์ดํ๋ฆฌ์ผ์ด์
์ด ๋น์ ์ด ์ํ๋ ๋๋ก ๋์ํ๋์ง ํ์ธํ ์ ์์ต๋๋ค. ๊ทธ ์ธ์๋ ๋ฆฌํฌํธ๋ ์ด๋ค ์ฝ๋๋ค์ด ํ
์คํธ๋์ง ์์๋์ง๋ฅผ ๊ฐ์กฐํด์ ๋ณด์ฌ์ค ์๋ ์์ต๋๋ค. - ์ฝ๋์ 80%๊ฐ ํ
์คํธ ๋์๋ค๋ ์๋ฆผ์ด ์ค์ํ ๋ถ๋ถ์ด ์ปค๋ฒ๋์๋์ง์ ๋ํ ์ฌ๋ถ๋ฅผ ๋ํ๋ด์ง ์์ต๋๋ค. ๋ฆฌํฌํธ๋ฅผ ๋ง๋๋ ๊ฒ์ ์ฝ์ต๋๋ค. - ์ด์ ๋๋ ํ
์คํธ๋ฅผ ํ ๋ ์ปค๋ฒ๋ฆฌ์ง ํธ๋ํน์ ํ๋ฉด์ ์ดํ๋ฆฌ์ผ์ด์
์ ์คํํ์ธ์. ๊ทธ๋ฌ๊ณ ๋์ ๊ฐ ์ฝ๋ ์์ญ์ด ์ผ๋ง๋ ์์ฃผ ํธ์ถ๋๋์ง๋ฅผ ๋ํ๋ด๋ ํํ์์์ ๋ฆฌํฌํธ๋ฅผ ๋ณด์ธ์. ์ ๊น ์๊ฐ์ ๋ด์ ์ด ๋ฐ์ดํฐ๋ค์ ๋ณด๋ฉด ๋ช ๊ฐ์ง ๋ฌธ์ ์ ๋ค์ ๋ฐ๊ฒฌํ๊ฒ ๋ ์๋ ์์ต๋๋ค.
โ ๊ทธ๋ ์ง ์์ผ๋ฉด: ์ด๋ค ์ฝ๋๊ฐ ํ ์คํธ๋์ง ์์๋์ง ์ ์ ์์ผ๋ฉด ๋ฌธ์ ์ ์์ธ๋ ์ ์ ์์ต๋๋ค.
โ ์ฝ๋ ์์
:thumbsdown: ์ฌ๋ฐ๋ฅด์ง ์์ ์: ์ด ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ์๋ ์ด๋ค ๋ฌธ์ ๊ฐ ์๋์? ํ์ค ์ธ๊ณ ์๋๋ฆฌ์ค๋ก QA์์ ์ดํ๋ฆฌ์ผ์ด์ ์ฌ์ฉ์ ์ถ์ ํ๊ณ ํฅ๋ฏธ๋ก์ด ๋ก๊ทธ์ธ ํจํด์ ์ฐพ์์ต๋๋ค. (ํํธ: ๋ก๊ทธ์ธ ์คํจ ํ์๊ฐ ๋น๋กํ์ง ์์ต๋๋ค. ๋ถ๋ช ํ ๋ฌด์ธ๊ฐ ์๋ชป๋์์ต๋๋ค.) ๋ง์นจ๋ด ์ผ๋ถ ํ๋ก ํธ์๋ ๋ฒ๊ทธ๊ฐ ๋ฐฑ์๋ ๋ก๊ทธ์ธ 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 ๋ณด๊ณ ์๋ ํ ์คํธ ๋์ง ์์ ์ฝ๋์ ์์ ๊ฐ์งํ๊ณ ๊ณ์ฐํฉ๋๋ค.

โช ๏ธ 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๋ถ์ ๋ฆฐํฐ ์ค์ ์ผ๋ก ์ด๋ฐ ์คํ๋ฅผ ๊ฐ์งํ๊ณ ํ๋ฃจ๋ฅผ ์ง์ผ๋ผ ์ ์์ต๋๋ค.
โช ๏ธ 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)

โช ๏ธ 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

โช ๏ธ 5.6 ์ทจ์ฝํ ์ข ์์ฑ์ ์ง์์ ์ผ๋ก ๊ฒ์ฌ
:white_check_mark: ์ด๋ ๊ฒ ํด๋ผ: Express์ ๊ฐ์ด ์๋นํ ํํ์ด ์ข์ ์ข ์์ฑ ์กฐ์ฐจ๋ ์ทจ์ฝ์ ์ด ์์ต๋๋ค. npm audit๊ณผ ๊ฐ์ ์ปค๋ฎค๋ํฐ ๋๊ตฌ ๋๋ snyk๊ฐ์ ์์ฉ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ์ฝ๊ฒ ์ ์ ์์ต๋๋ค(๋ฌด๋ฃ ์ปค๋ฎค๋ํฐ ๋ฒ์ ๋ ์ ๊ณต). ๋๋ค ๋น์ ์ CI์ ๋ชจ๋ ๋น๋์์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
โ ๊ทธ๋ ์ง ์์ผ๋ฉด: ์ ์ฉ ๋๊ตฌ ์์ด ์ฝ๋๋ฅผ ์ทจ์ฝ์ ์ผ๋ก๋ถํฐ ์์ ํ๊ฒ ์ ์งํ๋ ค๋ฉด ์๋ก์ด ์ํ์ ๋ํ ์ต์ ์์์ ์ง์์ ์ผ๋ก ์ซ์์ผ ํด์ ๋งค์ฐ ์ง๋ฃจํฉ๋๋ค.
โช ๏ธ 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 ํ์ดํ๋ผ์ธ ๋ด์์ ์ฌ์ฉํ ์ ์์ต๋๋ค.

โช ๏ธ 5.8 ๊ธฐํ, ๋ ธ๋์ ๊ด๋ จ์๋ CI ํ
:white_check_mark: ์ด๋ ๊ฒ ํด๋ผ: ์ด ๊ธ์ Node.js์ ๊ด๋ จ์ด ์๊ฑฐ๋ ์ต์ํ Node.js๋ก ์๋ฅผ ๋ค ์ ์๋ ํ ์คํธ ์กฐ์ธ์ ์ค์ ์ ๋๊ณ ์์ต๋๋ค. ๊ทธ๋ฌ๋ ์ด๋ฒ์๋ Node.js์ ๊ด๋ จ์์ง๋ง ์ ์๋ ค์ง ํ ๋ช๊ฐ๋ฅผ ๊ทธ๋ฃนํ ํ์์ต๋๋ค.
- ์ ์ธ์ ๊ตฌ๋ฌธ์ ์ฌ์ฉํ์ญ์์ค. ๋๋ถ๋ถ์ ๋ฒค๋์์๋ ์ ํํ ์ ์์ง๋ง, ์ด์ ๋ฒ์ ์ Jenkins์์ ์ฝ๋ ๋๋ UI๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
- ๊ณ ์ Docker๋ฅผ ์ง์ํ๋ ๋ฒค๋๋ฅผ ์ ํํ์ญ์์ค.
- ์ผ์ฐ ์คํจํ๊ณ ๊ฐ์ฅ ๋น ๋ฅธ ํ ์คํธ๋ฅผ ๋จผ์ ์คํํ์ญ์์ค. ๋ค์ํ ๋น ๋ฅธ Inspection(์: ๋ฆฐํธ, ๋จ์ํ ์คํธ)๋ฅผ ๊ทธ๋ฃนํ ํ๊ณ ์ฝ๋ ์ปค๋ฏธํฐ์ ๋ํ ์ ์ํ ํผ๋๋ฐฑ์ ์ ๊ณตํ ์ ์๋ ์ค๋ชจํฌ ํ ์คํธ ๋จ๊ณ/๋ง์ผ์คํค์ ๋ง๋์ญ์์ค.
- ํ ์คํธ ๋ณด๊ณ ์, ์ปค๋ฒ๋ฆฌ์ง, ๋ณํ, ๋ก๊ทธ ๋ฑ์ ๋ชจ๋ ๊ฒฐ๊ณผ๋ฌผ์ ํ์ด๋ณด๊ธฐ ์ฝ๊ฒ ํ์ญ์์ค.
- ๊ฐ ์ด๋ฒคํธ์ ๋ํด ์ฌ๋ฌ ํ์ดํ๋ผ์ธ/์์ ์ ์์ฑํ๊ณ , ๊ทธ ์ฌ์ด ๋จ๊ณ๋ฅผ ์ฌ์ฌ์ฉ ํ์ญ์์ค. ์๋ฅผ ๋ค๋ฉด, feature ๋ธ๋์น ์ปค๋ฐ์ด๋ ๋ง์คํฐ PR์ ๋ํ ์์ ๊ตฌ์ฑ. ๊ฐ ์ฌ์ฌ์ฉ ๋ก์ง์ด ๊ณต์ ๋จ๊ณ๋ฅผ ์ฌ์ฉํ๊ฒ ํ์ญ์์ค.(๋๋ถ๋ถ์ ๋ฒค๋๋ ์ฝ๋ ์ฌ์ฌ์ฉ์ ์ํ ๋ฉ์ปค๋์ฆ์ ์ ๊ณตํฉ๋๋ค)
- ์์ ์ ์ธ์ ์ด๋คํ๊ฒ๋ ์จ๊ฒจ๋์ง ๋ง์ญ์์ค.
- ๋ฆด๋ฆฌ์ค ๋น๋์์ ๋ช ์์ ์ผ๋ก ๋ฒ์ ์ ์ถฉ๋ ์์ผ๋ณด๊ฑฐ๋ ์ต์ํ ๊ฐ๋ฐ์๊ฐ ๊ทธ๋ ๊ฒํ๋์ง ํ์ธํ์ญ์์ค.
- ํ๋ฒ๋ง ๋น๋ํ๊ณ ๋จ์ผ ๋น๋ ๊ฒฐ๊ณผ๋ฌผ(์: Docker ์ด๋ฏธ์ง)์ ๋ํด ๋ชจ๋ ๊ฒ์ฌ๋ฅผ ์ํํ์ญ์์ค.
- ๋น๋๊ฐ์ ์ํ๊ฐ ๋ณํ์ง ์๋ ์์ ํ๊ฒฝ์์ ํ ์คํธํ์ญ์์ค. 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 ์ ๋ฌธ๊ฐ ๋ฐ ์ด๋ชจํฐ์ฝ์ ๊ด์ฌ์ด ๋ง์ ์ฌ๋




