test-guidelines
npx skills add https://github.com/ssiumha/dots --skill test-guidelines
Agent 安装分布
Skill 文档
Test Guidelines
í ì¤í¸ ìì±ì íµì¬ ìì¹ê³¼ ìí¬íë¡ì°ë¥¼ ì ê³µíë ì¤í¬ì ëë¤.
íµì¬ ì² í:
- ì¤ì 구í ì°ì , Mock ê³¼ë¤ë ì¤ê³ 문ì ì í¸
- ìì ë¨ìë¶í° ìì (Bottom-up)
- ë°ë³µëë í¨í´ì fixture/factoryë¡ ì¬ì¬ì©
- Mock íì ì 구조 ì§ë¨ 먼ì , íìíë©´ ì¬ì©ìì ë ¼ì
- í ì¤í¸ ì¤í¨ = ì¤ì 문ì . ì ë ì°ííì§ ë§ ê²
í ì¤í¸ 문ìí
í ì¤í¸ë ì¤í ê°ë¥í ì¤í 문ìì ëë¤.
describe/it ìì±ë²
// â 모í¸í¨
describe('UserService', () => {
it('works', () => {});
});
// â
ëª
íí¨
describe('UserService.login', () => {
it('ì í¨í ì격ì¦ëª
ì¼ë¡ ë¡ê·¸ì¸íë©´ í í°ì ë°ííë¤', () => {});
it('ì못ë ë¹ë°ë²í¸ë©´ AuthError를 ëì§ë¤', () => {});
});
ë³µì¡í í ì¤í¸ì Why 주ì
ì£ì§ ì¼ì´ì¤, ë²ê·¸ ìì , ë¹ì§ê´ì ë¡ì§ì ì íìíì§ ì£¼ì:
def test_handles_leap_year_edge_case():
"""
2ì 29ì¼ ì²ë¦¬ í
ì¤í¸
- Fixes #234: ì¤ë
ê³ì° ì¤ë¥
- Per spec: ISO 8601
"""
assert parse_date("2024-02-29") is not None
ì¤í/ì´ì 참조
// ì구ì¬í 참조
// REQ: ë¡ê·¸ì¸ ì¤í¨ 5í ì ê³ì ì ê¸
it('5í ì°ì ì¤í¨ ì ê³ì ì ì ê·¼ë¤', () => {});
// ë²ê·¸ ìì 참조
// Fixes #456: ë¹ ë¬¸ìì´ ì
ë ¥ ì í¬ëì
it('ë¹ ì
ë ¥ì gracefully ì²ë¦¬íë¤', () => {});
í ì¤í¸ ë¶ë¦¬ ë¨ì
í ì¤í¸ ë 벨 ì ì
| ë 벨 | ë²ì | ìë | ë¹ì¨ |
|---|---|---|---|
| ë¨ì | í¨ì/í´ëì¤ íë | ms | 80% |
| íµí© | 모ë ê° ìí¸ìì© | 100ms-1s | 15% |
| E2E | ì ì²´ ìì¤í | ì´-ë¶ | 5% |
describe 그룹í 기ì¤
describe('모ë/í´ëì¤ëª
')
âââ describe('ë©ìëëª
')
âââ it('ìí©_ê²°ê³¼')
그룹í ìì¹:
- ê°ì setupì ê³µì íë í ì¤í¸ë¼ë¦¬
- ê°ì 기ë¥ì ê²ì¦íë í ì¤í¸ë¼ë¦¬
- ë무 ê¹ì ì¤ì²© í¼í기 (ìµë 3ë¨ê³)
1í ì¤í¸ = 1ì¤í¨ ìì¸
í ì¤í¸ ì¤í¨ ì ì íí íëì ìì¸ë§ ìì´ì¼ í¨:
# â ì¬ë¬ ìì¸ ê°ë¥
def test_user_flow():
user = create_user() # ì¤í¨ ìì¸ 1
login(user) # ì¤í¨ ìì¸ 2
assert profile(user) # ì¤í¨ ìì¸ 3
# â
ìì¸ ëª
í
def test_create_user():
user = create_user()
assert user.id is not None
def test_login_returns_token():
user = create_user() # fixtureë¡ ë¶ë¦¬ ê¶ì¥
token = login(user)
assert token is not None
Instructions
Workflow 1: ì í ì¤í¸ íì¼ ìì±
-
í ì¤í¸ ëì ì½ë ë¶ì
- Globì¼ë¡ ê´ë ¨ íì¼ ì°¾ê¸° (
*.test.*,*_test.*ë±) - 구í ì½ë 먼ì Read (í ì¤í¸ ìì± ì íì)
- Grepì¼ë¡ 기존 í ì¤í¸ í¨í´ íì¸
- ê°ì¥ ìì ë¨ìë¶í° (ìì í¨ì, ë¨ì¼ ë©ìë)
- í¨ì/ë©ìëì ì ë ¥, ì¶ë ¥, ë¶ì í¨ê³¼ íì
- ìì¡´ì± íì¸ (DB, ì¸ë¶ API, íì¼ ìì¤í ë±)
- Globì¼ë¡ ê´ë ¨ íì¼ ì°¾ê¸° (
-
í ì¤í¸ ì ëµ ì립
- AAA í¨í´ (Arrange-Act-Assert) ì ì©
- í ì¤í¸ ì¼ì´ì¤ 목ë¡: Happy path, Edge cases, Error cases
- ê²½ê³ê° í
ì¤í¸:
resources/boundary-testing.md참조 (ì«ì, 문ìì´, ë ì§ ê²½ê³) - ì¬ì©ììê² ì ëµ ì¤ëª ë° íì¸
-
â ï¸ Mock íìì± íë¨ â 구조 ì§ë¨
- Mock ëì ìì¡´ì± ëì´ (ì¤ì ì¬ì©ëë ê²ë§)
- ìì¡´ì± 3ê° ì´ì: 구조 문ì ê°ë¥ì± ì¸ê¸
- ì§ë¬¸í기:
- “ì´ í´ëì¤ê° ìì¡´ì±ì ì§ì ìì±íëì? (new, import)”
- “ìì¡´ì± ì£¼ì (DI)ì¼ë¡ ë³ê²½íë©´ í ì¤í¸ê° ì¬ìì§ ì ìì´ì”
- “ë ìì ë¨ìë¡ ë¶ë¦¬ ê°ë¥íê°ì?”
- ì íì§ ì ì: 구조 ê°ì (ê¶ì¥) vs Mock ì¬ì©(ìì)
- ì¬ì©ì ê²°ì í ì§í
-
Fixture/Factory í¨í´ ê³ ë ¤
- ë°ë³µëë í ì¤í¸ ë°ì´í° í¨í´ íì¸
- ëì¼ ë°ì´í° 3í ì´ì ë°ë³µ ì Factory ì ì
- ì¸ì´ë³ ê°ì´ëìì 구í ë°©ë² ì°¸ì¡°
-
í ì¤í¸ ì½ë ìì±
- Write toolë¡ í ì¤í¸ íì¼ ìì±
- ì ì§ì ìì±: í ì¤í¸ 1ê° ìì± â ì¤í â ë¤ì í ì¤í¸
- ëª íí í ì¤í¸ëª ì¬ì© (무ìì_ì¸ì _ì´ë»ê²)
- ê° í ì¤í¸ë ë 립ì ì¼ë¡ ì¤í ê°ë¥íê²
-
í ì¤í¸ ì¤í ë° ê²ì¦
- Bash toolë¡ í ì¤í¸ ì¤í
- ì¤í¨ ì: í ì¤í¸ ì°í ê¸ì§, ì¤ì 구í 문ì í´ê²° (ìì¹ 5 참조)
- ì¬ì©ììê² ê²°ê³¼ ë³´ê³
Workflow 2: 기존 í ì¤í¸ 리뷰/ìì
-
í ì¤í¸ íì¼ ì½ê¸°
- Globì¼ë¡ í ì¤í¸ íì¼ ëª©ë¡ íì¸
- í° íì¼(>500ì¤)ì Grepì¼ë¡ 구조 먼ì íì
- Read toolë¡ í ì¤í¸ ì½ë ë¶ì (íì ì offset/limit)
- 구í ì½ëë í¨ê» Read (í ì¤í¸ë§ ë³´ì§ ë§ ê²)
- ì¤ë³µ í¨í´ ìë³
-
íì§ ì²´í¬ë¦¬ì¤í¸ ì ì©
- í ì¤í¸ ë 립ì±, ëª íì±, AAA í¨í´ íì¸
- Mock ê³¼ë¤ ì¬ì© ê²í
- ì¬ì¬ì©ì± íì¸
-
í ì¤í¸ ì°í í¨í´ 찾기 (â ï¸ ì¤ì)
- í ì¤í¸ ì°í í¨í´ ê²ì (ìì¹ 5 참조)
- ë°ê²¬ ì ì¦ì ì¬ì©ììê² ë³´ê³
-
ê°ì ì ì ë° ì¬ì©ì íì¸
- ë°ê²¬ë 문ì ì ëì´
- 구체ì ì¸ ê°ì ë°©ë² ì ì
- ì¬ì©ì íì¸ í ìì
Workflow 3: ì¤í¨ í ì¤í¸ ëë²ê¹
-
ì¤í¨ ì ë³´ ìì§
- Bashë¡ verbose ì¤í, ì íí ìë¬ ë©ìì§ íì¸
- ì´ë¤ assertionì´ ì¤í¨íëì§ íì
-
ì½ë ë¶ì
- 구íê³¼ í ì¤í¸ 모ë Read (ìì§ ì ì½ìë¤ë©´)
- ì¤í¨ ìì¸ íë¨: 구í ë²ê·¸ vs í ì¤í¸ 문ì
-
문ì í´ê²°
- 구í ë²ê·¸ â 구í ì½ë ìì
- ì¤í ë¶ëª í â ì¬ì©ììê² ì¤í íì¸
- í ì¤í¸ ê³¼ë¤ ì격 â ì¬ì©ì ì¹ì¸ í ì¡°ì
- ì ë í ì¤í¸ ì°í ê¸ì§ (ìì¹ 5 참조)
í ì¤í¸ ìì± ìì¹
1. ìì ë¨ìë¶í° ìì (Bottom-up)
ì¬ë°ë¥¸ ìì: ìì í¨ì â ë¨ì¼ í´ëì¤ â 모ë íµí© â E2E
ìì ë¨ìì ì¥ì :
- ë¹ ë¥¸ í¼ëë°± (ë°ë¦¬ì´ ë¨ì)
- ëª íí ì¤í¨ ìì¸
- ì¬ì´ ëë²ê¹
í ì¤í¸ ë¹ì¨ (Google ê¶ì¥):
- ë¨ì í ì¤í¸ 80%: ë¹ ë¥¸ í¼ëë°±, ì´ì ì¼ì´ì¤ 커ë²
- íµí©/E2E í ì¤í¸ 20%: ì¤ì ëì ê²ì¦
2. AAA í¨í´ (Arrange-Act-Assert)
- Arrange: ë°ì´í°/ê°ì²´ ì¤ë¹, Fixture/Factory íì©
- Act: í ì¤í¸ ëì ë¨ í ë² í¸ì¶
- Assert: ìì ê²°ê³¼ ë¹êµ, ë¶ì í¨ê³¼ íì¸
3. í ì¤í¸ ë 립ì±
- ê° í ì¤í¸ë ë 립ì ì¼ë¡ ì¤í
- í ì¤í¸ ê° ì¤í ìì 무ê´
- Fixtureë ê° í ì¤í¸ë§ë¤ freshíê²
4. DAMP > DRY ìì¹
í ì¤í¸ ì½ëë DRY(ì¤ë³µ ì ê±°)ë³´ë¤ **DAMP(Descriptive and Meaningful Phrases)**를 ì°ì í©ëë¤.
ì DAMPì¸ê°?:
- í ì¤í¸ ì¤í¨ ì ìì¸ íì ì©ì´ì±ì´ ìµì°ì
- ê° í ì¤í¸ê° ë 립ì ì¼ë¡ ì´í´ ê°ë¥í´ì¼ í¨
- í¬í¼ ë©ìë ê³¼ëí ê³µíµí â 컨í ì¤í¸ ë¶í¬ëª í
# â ê³¼ëí DRY - ì¤í¨ ì ìì¸ íì
ì´ë ¤ì
def test_order_processing():
order = create_order() # ë´ë¶ìì ë¬´ì¨ ì¼ì´?
assert process(order) == expected_result() # 기ëê°ì´ ëì§?
# â
DAMP - ëª
ìì ì´ê³ ì´í´í기 ì¬ì
def test_order_with_discount_applies_10_percent():
order = Order(items=[Item("book", 10000)], discount_code="SAVE10")
result = process(order)
assert result.total == 9000
// â ê³¼ëí DRY
it('processes order', () => {
const order = createTestOrder();
expect(process(order)).toEqual(expectedResult());
});
// â
DAMP
it('applies 10% discount when valid code provided', () => {
const order = { items: [{ name: 'book', price: 10000 }], discountCode: 'SAVE10' };
expect(process(order).total).toBe(9000);
});
5. 1í ì¤í¸ = 1ë ¼ë¦¬
í ì¤í¸ ë´ ì¡°ê±´ë¬¸ì ìí ì í¸ì ëë¤.
í¼í´ì¼ í í¨í´:
- í ì¤í¸ ë´ if/else, for 조건문
- ì¬ë¬ ìë리ì¤ë¥¼ íëì í ì¤í¸ì 몰ìë£ê¸°
# â ì¬ë¬ ìë리ì¤ë¥¼ íëì
def test_calculate_discount():
for discount in [0, 10, 50, 100]:
if discount > 50:
assert calculate(discount) == "invalid"
else:
assert calculate(discount) > 0
# â
íë¼ë¯¸í°í í
ì¤í¸ë¡ ë¶ë¦¬
@pytest.mark.parametrize("discount,expected", [
(0, 1000),
(10, 900),
(50, 500),
])
def test_calculate_discount_valid(discount, expected):
assert calculate(discount) == expected
def test_calculate_discount_over_50_is_invalid():
assert calculate(100) == "invalid"
// â ì¬ë¬ ìë리ì¤ë¥¼ íëì
it('calculates discount', () => {
[0, 10, 50, 100].forEach(discount => {
if (discount > 50) {
expect(calculate(discount)).toBe('invalid');
} else {
expect(calculate(discount)).toBeGreaterThan(0);
}
});
});
// â
test.eachë¡ ë¶ë¦¬
it.each([
[0, 1000],
[10, 900],
[50, 500],
])('calculates %i%% discount as %i', (discount, expected) => {
expect(calculate(discount)).toBe(expected);
});
it('rejects discount over 50%', () => {
expect(calculate(100)).toBe('invalid');
});
6. ëª íí í ì¤í¸ëª
í¨í´: ë©ìëëª
_ìí©_ììê²°ê³¼
ì¢ì ì:
calculateTotal_withValidInput_returnsCorrectSumfetchUser_whenUserNotFound_throwsNotFoundError
7. í ì¤í¸ ì¤í¨ ëì ìì¹ (â ï¸ ì¤ì)
â ì ë ê¸ì§:
- í
ì¤í¸ 주ì ì²ë¦¬
- skip, xfail, disabled ì¬ì©
- ì¡°ê±´ë¶ assert (if 문 ìì assert)
- í
ì¤í¸ ì¡°ê±´ì ì¤íê³¼ 무ê´íê² ìì
- íì íµê³¼íë ì미 ìë assert
- Mockì¼ë¡ 무ìì ì°í
â
ì¬ë°ë¥¸ ëì:
1. ì¤í¨ ìì¸ ì íí íì
2. 구í ì½ë ìì (ì¤í ì¤ì)
3. ì¤íì´ ì못ëë¤ë©´ ì¬ì©ìì ë
¼ì
4. í
ì¤í¸ë ì¤í¨ ìíë¡ ì ì§íê³ ì´ì ì¶ì
í ì¤í¸ë ê±°ì§ë§íì§ ìëë¤:
- í ì¤í¸ ì¤í¨ = 구í 문ì or ì¤í ì¤í´
- í ì¤í¸ë¥¼ ìì í´ì íµê³¼ìí¤ë ê²ì ì기기ë§
- ì¤í¨íë í ì¤í¸ê° íµê³¼íë ê±°ì§ í ì¤í¸ë³´ë¤ ë«ë¤
Fixture & Factory í¨í´
Fixture
- í ì¤í¸ì íìí ê³ ì ë ìí/ë°ì´í°
- Setup/Teardownì¼ë¡ ê´ë¦¬
- ê° í ì¤í¸ë§ë¤ freshí ìí
Factory í¨í´
- í ì¤í¸ ë°ì´í° ìì± ì¬ì¬ì© í¨ì/í´ëì¤
- ê¸°ë³¸ê° + íì ì override
- ëì¼ ë°ì´í° 3í ì´ì ë°ë³µ ì ì¬ì©
ì¬ì¬ì©ì± íë¨
â
Fixture/Factory ë§ë¤ê¸°:
- ëì¼ ë°ì´í° 3í ì´ì ë°ë³µ
- ë³µì¡í ê°ì²´ ìì± (5ê° ì´ì ìì±)
- Setup/Teardown ê³µíµ
â ë¶íì:
- 1-2ê° í
ì¤í¸ììë§ ì¬ì©
- ë§¤ì° ê°ë¨í ë°ì´í°
ê±°ì§ ìì±(False Positive) ë°©ì§
ê±°ì§ ìì±ì´ë?
ë¡ì§ì ì ìì¸ë° 리í©í ë§ì¼ë¡ í ì¤í¸ê° ì¤í¨íë íìì ëë¤. ê±°ì§ ìì±ì´ ë§ì¼ë©´ í ì¤í¸ ì 뢰ëê° íë½íê³ , ê°ë°ìê° í ì¤í¸ë¥¼ 무ìíê² ë©ëë¤.
ìì¸: 구í ì¸ë¶ì¬í ìì¡´
# â 구í ì¸ë¶ì¬íì ìì¡´ - ê±°ì§ ìì± ë°ì
def test_apply_discount():
mock_discount = Mock()
service = OrderService(discount_calculator=mock_discount)
service.calculate_price(100)
# ë´ë¶ì ì¼ë¡ ì´ë¤ í¨ì를 í¸ì¶íëì§ ê²ì¦
mock_discount.apply.assert_called_once_with(100) # 리í©í ë§íë©´ 깨ì§
// â 구í ì¸ë¶ì¬íì ìì¡´
it('applies discount', () => {
const mockDiscount = jest.fn();
const service = new OrderService({ discountCalculator: { apply: mockDiscount } });
service.calculatePrice(100);
expect(mockDiscount).toHaveBeenCalledWith(100); // 리í©í ë§íë©´ 깨ì§
});
í´ê²°: íì(ê²°ê³¼) ê¸°ë° ê²ì¦
# â
íì ê¸°ë° - ìµì¢
ê²°ê³¼ê°ë§ ê²ì¦
def test_apply_discount():
service = OrderService(discount_calculator=RealDiscountCalculator())
result = service.calculate_price(100)
assert result == 90 # ì´ë»ê² ê³ì°íëì§ë ì¤ìíì§ ìì
// â
íì 기ë°
it('applies 10% discount', () => {
const service = new OrderService({ discountCalculator: new RealDiscountCalculator() });
expect(service.calculatePrice(100)).toBe(90);
});
ê³µê° APIë§ í ì¤í¸
- private í¨ì ì§ì í ì¤í¸ â 깨ì§ê¸° ì¬ì´ í ì¤í¸
- public ì¸í°íì´ì¤ë¥¼ íµí´ ê°ì ê²ì¦
# â private í¨ì ì§ì í
ì¤í¸
def test_internal_calculation():
service = OrderService()
result = service._calculate_tax(100) # private í¨ì ì§ì í¸ì¶
assert result == 10
# â
public API를 íµí´ ê²ì¦
def test_order_includes_tax():
service = OrderService()
result = service.calculate_total(100) # public ë©ìë
assert result == 110 # ì¸ê¸ í¬í¨ë ê²°ê³¼
// â private í¨ì ì§ì í
ì¤í¸
it('calculates tax internally', () => {
const service = new OrderService();
expect((service as any).calculateTax(100)).toBe(10); // private ì ê·¼
});
// â
public API를 íµí´ ê²ì¦
it('order total includes tax', () => {
const service = new OrderService();
expect(service.calculateTotal(100)).toBe(110);
});
ê±°ì§ ìì± ì²´í¬ë¦¬ì¤í¸
- Mockì í¸ì¶ íì/ì¸ì를 ê²ì¦íê³ ìì§ ììê°?
- private í¨ì를 ì§ì í ì¤í¸íê³ ìì§ ììê°?
- ë´ë¶ ìí ë³ê²½ì ì§ì ê²ì¦íê³ ìì§ ììê°?
- 리í©í ë§í´ë í ì¤í¸ê° íµê³¼íëê°?
Mock/Stub ì¬ì© ì² í
â ï¸ Mock ê³¼ë¤ = ì¤ê³ 문ì ì í¸
Mockì´ ë§ì´ íìíë¤ë©´ 먼ì ì§ë¬¸í기:
구조 ì§ë¨ ì§ë¬¸:
- í ì¤í¸ ëìì´ ìì¡´ì±ì ì§ì ìì±íëì? (new, import) â DI í¨í´ 미ì ì© ì í¸
- Mock ëìì´ 3ê° ì´ìì¸ê°ì? â ì± ì ê³¼ë¤ (SRP ìë°) ê°ë¥ì±
- ë´ë¶ 구íì Mockí´ì¼ íëì? â ê°í ê²°í© ì í¸
ëì ì°ì ìì:
- 구조 ê°ì ì ì (DI ì ì©, ì± ì ë¶ë¦¬)
- ì¸í°íì´ì¤ ì¶ì¶ + Fake 구í
- ì¬ì©ì ì¹ì¸ í Mock (ìì í´ê²°ì± ì¼ë¡ ëª ì)
Robert Martin Goldilocks Rule
“Mock across architecturally significant boundaries, but not within those boundaries.”
â
Mock OK (ìí¤í
ì² ê²½ê³):
- ì¸ë¶ API (ê²°ì , ì´ë©ì¼, 3rd party)
- íì¼ ìì¤í
ëë I/O
- íì¬ ìê° ìì¡´ì±
- ë¤í¸ìí¬ ìì²
- ë린 ì°ì° (> 1ì´)
â ï¸ íë¨ íì (100ms - 1ì´):
- DB 쿼리 â In-memory DB ê³ ë ¤
- íì¼ ì½ê¸° â ìì íì¼ì ì¤ì ì¬ì©
â Mock í¼í기 (ë´ë¶ ê²½ê³):
- ê°ì 모ë ë´ í´ëì¤/í¨ì
- íë¡ì í¸ ë´ë¶ ìì¡´ì±
- ìì í¨ì
- ë¹ ë¥¸ ê³ì° (< 100ms)
Mock íì ì ìí¬íë¡ì°
- ì§ë¨: ì Mockì´ íìíì§ ë¶ì
- ì§ë¬¸: ì¬ì©ììê² êµ¬ì¡° ê°ì ìí¥ íì¸
- ì íì§ ì ì:
A. 구조 ê°ì (ê¶ì¥) - DI ì ì©, ì± ì ë¶ë¦¬ - ì¥ê¸°ì ì¼ë¡ í ì¤í¸ ì©ì´ì± í¥ì B. Mock ì¬ì© (ìì) - íì¬ êµ¬ì¡° ì ì§ - 기ì ë¶ì±ë¡ ê¸°ë¡ - ì¬ì©ì ê²°ì ì ë°ë¼ ì§í
Test Double ì í
- Dummy: ì ë¬ë§ ë¨ (ì¬ì©ëì§ ìì)
- Stub: 미리 ì ìë ìëµ ë°í
- Spy: í¸ì¶ ê¸°ë¡ + ì¤ì ëì
- Fake: ê°ë¨í 구í (In-memory DB ë±) â ê¶ì¥
- Mock: íì ê²ì¦ â 구조 ì§ë¨ í ì¬ì©
í ì¤í¸í기 ì´ë ¤ì´ 구조 ê°ì
Mockì´ ê³¼ë¤íê² íìí ì½ëë 구조 ê°ì ì¼ë¡ í´ê²°í ì ììµëë¤.
DI 미ì ì© í¨í´ (Bad)
# â ìì¡´ì±ì ì§ì ìì± â Mock íì
class OrderService:
def __init__(self):
self.payment = PaymentGateway() # ì§ì ìì±
self.email = EmailService() # ì§ì ìì±
self.logger = Logger() # ì§ì ìì±
def process(self, order):
self.payment.charge(order.total)
self.email.send(order.user, "주문 ìë£")
í ì¤í¸íë ¤ë©´ PaymentGateway, EmailService, Logger 모ë Mock íì.
DI ì ì© í¨í´ (Good)
# â
ìì¡´ì± ì£¼ì
â Fakeë¡ í
ì¤í¸ ê°ë¥
class OrderService:
def __init__(self, payment, email, logger):
self.payment = payment # 주ì
ë°ì
self.email = email # 주ì
ë°ì
self.logger = logger # 주ì
ë°ì
def process(self, order):
self.payment.charge(order.total)
self.email.send(order.user, "주문 ìë£")
# í
ì¤í¸
def test_process_order():
fake_payment = FakePaymentGateway()
fake_email = FakeEmailService()
fake_logger = FakeLogger()
service = OrderService(fake_payment, fake_email, fake_logger)
service.process(order)
assert fake_payment.charged_amount == 10000
assert fake_email.sent_to == "user@example.com"
ê°ì í¨ê³¼
| í목 | DI 미ì ì© | DI ì ì© |
|---|---|---|
| Mock íìì± | íì (3ê°) | ë¶íì (Fake ì¬ì©) |
| í ì¤í¸ ìë | Mock ì¤ì ì¤ë²í¤ë | ë¹ ë¦ |
| ìì¡´ì± ëª ìì± | ì¨ê²¨ì§ | ëª ìì |
| ì¬ì¬ì©ì± | ë®ì | ëì |
구조 ê°ì ì²´í¬ë¦¬ì¤í¸
- ìì¡´ì±ì´ ìì±ììì 주ì ëëê°?
- ì¸í°íì´ì¤/íë¡í ì½ë¡ ì¶ìíëìëê°?
- Fake 구íì´ ì¡´ì¬íëê°?
- ë¨ì¼ ì± ì ìì¹(SRP)ì ì¤ìíëê°?
íì§ ì²´í¬ë¦¬ì¤í¸
í ì¤í¸ 구조
- ìì ë¨ìë¶í° í ì¤í¸ (ìì í¨ì â í´ëì¤ â íµí©)
- AAA í¨í´ ëª í
- ë 립ì ì¤í ê°ë¥
- ëª íí í ì¤í¸ëª
- ë¨ì¼ ê´ì¬ì¬
- DAMP ìì¹ ì¤ì (ê° í ì¤í¸ê° ë 립ì ì¼ë¡ ì´í´ ê°ë¥)
- 1í ì¤í¸ = 1ë ¼ë¦¬ (í ì¤í¸ ë´ if/else, for ìì)
- í ì¤í¸ ë¹ì¨ ì ì (ë¨ì 80%, íµí©/E2E 20%)
í ì¤í¸ ì§ì ì± (â ï¸ ì¤ì)
- 주ì ì²ë¦¬ë í ì¤í¸ ìì
- skip/disabled í ì¤í¸ ìì
- ì¡°ê±´ë¶ assert ìì (if 문 ìì assert)
- ì¤ì ì¤í ê²ì¦
- ì미 ìë assert
- Mock ì°í ìì
ê±°ì§ ìì± ë°©ì§
- Mock í¸ì¶ íì/ì¸ì ê²ì¦ ìµìí
- private í¨ì ì§ì í ì¤í¸ ìì
- ê³µê° API를 íµí ê°ì ê²ì¦
- 리í©í ë§í´ë í ì¤í¸ íµê³¼
ì¬ì¬ì©ì±
- Fixture/Factory ì ì í ì¬ì©
- ê³µíµ Setup/Teardown ì 리
Mock ì¬ì©
- ìí¤í ì² ê²½ê³ììë§ ì¬ì© (Goldilocks Rule)
- ìì¡´ì± 3ê° ì´ì ì 구조 ì§ë¨ ìë£
- 구조 ê°ì vs Mock ì¬ì© ì íì§ ì ìí¨
- ë´ë¶ 모ë/í´ëì¤ë Mockíì§ ìì
í ì¤í¸ íì§
- ë¹ ë¥¸ ì¤í (< 1ì´)
- ëª íí ì¤í¨ ìì¸
- Edge case/error case ê²ì¦
- Flaky ìë (íì ê°ì ê²°ê³¼)
Examples
ì í ì¤í¸ ìì± (Mock ë¶íì)
User: "UserService í
ì¤í¸ ìì±í´ì¤"
â Workflow 1: ì½ë ë¶ì (ìì í¨ì ì주)
â ìì¡´ì± íì¸: ìì
â AAA í¨í´ì¼ë¡ í
ì¤í¸ ìì±
â ì¤í ë° ê²ì¦
Mock ê³¼ë¤ ê°ì§ â 구조 ê°ì ì ì
User: "OrderService í
ì¤í¸ ìì±í´ì¤"
â Workflow 1: ì½ë ë¶ì
â ìì¡´ì± íì¸: PaymentGateway, EmailService, Logger (3ê°)
â â ï¸ êµ¬ì¡° ì§ë¨: "ìì¡´ì± 3ê°, DI 미ì ì©"
â ì§ë¬¸: "구조 ê°ì (ê¶ì¥) vs Mock ì¬ì©(ìì)?"
â ì¬ì©ì ì íì ë°ë¼ ì§í
기존 í ì¤í¸ 리뷰 (Mock ê³¼ë¤ ë°ê²¬)
User: "í
ì¤í¸ ì½ë 리뷰í´ì¤"
â Workflow 2: í
ì¤í¸ íì¼ ë¶ì
â Mock 10ê° ë°ê²¬
â â ï¸ "Mock ê³¼ë¤ - Goldilocks Rule ìë°"
â 구조 ê°ì ì ì (DI ì ì©, ì±
ì ë¶ë¦¬)
Technical Details
ì¸ì´ë³ í ì¤í¸ ë구/문ë²ì rules 참조 (í ì¤í¸ íì¼ ìì ì í´ë¹ ruleì´ ì»¨í ì¤í¸ì ìë ì¶ê°ë¨):
- Python (
*_test.py,test_*.py):rules/python-test.md - TypeScript (
*.test.ts,*.spec.ts):rules/typescript-test.md