홈 / Skills / 개발 생산성 / TDD 테스트 주도 개발
개발 생산성

TDD 테스트 주도 개발

Matt PocockMITSkill 언어: 영어
라이선스 확인됨스크립트 없음
데모실행 기록 · claude-opus-5-5 · 2026년 10월
Add discount codes to the cart, test-first. `SAVE10` takes 10% off the subtotal, `FREESHIP` removes the 5.00 shipping fee, and an unknown code raises `InvalidCode`. Only one code can be applied per cart. We've agreed the seam: test through the public `Cart` API only, `cart.apply_code(code)` and `cart.total()`. Tests run with `python3 -m pytest -q`.
Skill: tdd
Bash: git ls-files && cat GLOSSARY.md 2>/dev/null; for f in $(git ls-files | grep -v '^.claude'); do echo "== $f"; c…
Bash: cat >> tests/test_cart.py <<'EOF'…
Bash: python3 - <<'EOF'…
Bash: cat >> tests/test_cart.py <<'EOF'…
Edit: shop/cart.py
Bash: python3 -m pytest -q 2>&1 | tail -1
Bash: python3 - <<'EOF'…
Bash: python3 - <<'EOF'…
Bash: python3 - <<'EOF'…
Bash: python3 - <<'EOF'…

Discount codes are now in the cart, and all 5 tests pass (python3 -m pytest -q). I built it one test at a time, writing each test first and watching it fail before adding the code to pass it. Every test uses only apply_code and total().

Tests added (tests/test_cart.py); the cart in each is 2 × 12.50, so the subtotal is 25.00:

  1. SAVE10 gives a total of 27.50 (22.50 after the discount, plus 5.00 shipping).
  2. FREESHIP gives a total of 25.00.
  3. An unknown code raises InvalidCode, and the total stays at 30.00.
  4. After one code is applied, a second apply_code raises CodeAlreadyApplied and the first code stays in effect (total stays 27.50).

Code changes (shop/cart.py): a list of known codes, the InvalidCode and CodeAlreadyApplied errors, a code field on Cart, and apply_code. total() now applies the 10% discount to the subtotal or drops the shipping fee, depending on the code.

Decisions for you:

  • What a second code does: "only one code per cart" could mean the second code is rejected or that it replaces the first. I made it reject, using the new CodeAlreadyApplied error. If you'd rather the second code replace the first, that's a one-line change.
  • Rounding: nothing rounds the SAVE10 result yet. A subtotal like 12.35 would give a total of 16.115. I left it alone because rounding wasn't in the spec; tell me the rule (e.g. half-up to 0.01) and I'll add a test for it first.

I haven't committed anything.

Code changes recorded in the trial sandbox (git diff):

diff --git a/shop/cart.py b/shop/cart.py
index 937c5d1..dbc57bd 100644
--- a/shop/cart.py
+++ b/shop/cart.py
@@ -2,6 +2,15 @@ from dataclasses import dataclass, field
 from decimal import Decimal
 
 SHIPPING_FEE = Decimal("5.00")
+CODES = {"SAVE10", "FREESHIP"}
+
+
+class InvalidCode(ValueError):
+    pass
+
+
+class CodeAlreadyApplied(ValueError):
+    pass
 
 
 @dataclass
@@ -14,6 +23,7 @@ class LineItem:
 @dataclass
 class Cart:
     items: list[LineItem] = field(default_factory=list)
+    code: str | None = None
 
     def add(self, sku: str, price: str, qty: int = 1) -> None:
         self.items.append(LineItem(sku, Decimal(price), qty))
@@ -21,5 +31,16 @@ class Cart:
     def subtotal(self) -> Decimal:
         return sum((i.price * i.qty for i in self.items), Decimal("0"))
 
+    def apply_code(self, code: str) -> None:
+        if code not in CODES:
+            raise InvalidCode(code)
+        if self.code is not None:
+            raise CodeAlreadyApplied(self.code)
+        self.code = code
+
     def total(self) -> Decimal:
-        return self.subtotal() + SHIPPING_FEE
+        subtotal = self.subtotal()
+        if self.code == "SAVE10":
+            subtotal -= subtotal * Decimal("0.10")
+        shipping = Decimal("0") if self.code == "FREESHIP" else SHIPPING_FEE
+        return subtotal + shipping
diff --git a/tests/test_cart.py b/tests/test_cart.py
index e0c976c..199e238 100644
--- a/tests/test_cart.py
+++ b/tests/test_cart.py
@@ -1,9 +1,42 @@
 from decimal import Decimal
 
-from shop.cart import Cart
+import pytest
+
+from shop.cart import Cart, CodeAlreadyApplied, InvalidCode
 
 
 def test_total_adds_shipping():
     cart = Cart()
     cart.add("mug", "12.50", qty=2)
     assert cart.total() == Decimal("30.00")
+
+
+def test_save10_takes_ten_percent_off_subtotal():
+    cart = Cart()
+    cart.add("mug", "12.50", qty=2)
+    cart.apply_code("SAVE10")
+    assert cart.total() == Decimal("27.50")
+
+
+def test_freeship_removes_shipping_fee():
+    cart = Cart()
+    cart.add("mug", "12.50", qty=2)
+    cart.apply_code("FREESHIP")
+    assert cart.total() == Decimal("25.00")
+
+
+def test_unknown_code_raises_invalid_code():
+    cart = Cart()
+    cart.add("mug", "12.50", qty=2)
+    with pytest.raises(InvalidCode):
+        cart.apply_code("BOGUS")
+    assert cart.total() == Decimal("30.00")
+
+
+def test_only_one_code_per_cart():
+    cart = Cart()
+    cart.add("mug", "12.50", qty=2)
+    cart.apply_code("SAVE10")
+    with pytest.raises(CodeAlreadyApplied):
+        cart.apply_code("FREESHIP")
+    assert cart.total() == Decimal("27.50")

Real run recorded with claude-code / claude-opus-5-5. Run in a temporary directory containing a sample project, with Edit and Bash enabled. Tool-call lines show only the first line of each command. The last block is the git diff recorded when the trial ended. Both model outputs are unmodified.

하는 일

에이전트를 '빨강에서 초록으로' 가는 루프로 이끌고, 더 중요하게는 어떤 테스트가 남길 만한지를 알려 줍니다. 좋은 테스트와 나쁜 테스트, 언제 mock을 쓸지에 대한 참고 노트 두 개가 함께 들어 있습니다.

작동 방식

  1. 먼저 접점을 합의: 테스트를 쓰기 전에 어떤 공개 인터페이스를 통해 테스트할지 정리해 사용자에게 확인받습니다. 확인되지 않은 접점에서는 테스트를 쓰지 않습니다.
  2. 한 번에 한 조각: 테스트 하나, 최소 구현 하나를 반복합니다. 모든 테스트를 미리 다 쓰지 않습니다.
  3. 빨강 다음 초록: 실패하는 테스트가 먼저, 그다음 그것을 통과시킬 만큼만 코드를 씁니다.
  4. 세 가지 안티패턴 피하기: 구현 세부 사항에 묶인 테스트, 기대값을 코드와 같은 방식으로 계산해 항상 통과하는 '자기 증명' 테스트, 모든 테스트를 먼저 쓰는 수평 슬라이싱.
  5. 시스템 경계에서만 mock: 외부 API, 시간, 난수 등. 자기 모듈은 mock하지 않습니다.

이럴 때 좋습니다

새 기능과 버그 수정에서 리팩터링 뒤에도 살아남는 테스트를 원할 때.

알아 둘 점

리팩터링은 의도적으로 루프 밖에 두고 리뷰 단계에서 합니다. 인터페이스 설계를 위해 작성자의 codebase-design skill을 언급하지만 선택적인 참고일 뿐입니다.

참고 및 위험

지시문만 담긴 파일로, 스크립트가 없고 네트워크에 연결하지 않습니다. Skill은 에이전트에게 저장소에 테스트와 구현 코드를 작성하고 테스트 명령을 실행하게 합니다. 패키지에는 원본 저장소의 MIT 라이선스(LICENSE)와 agents/openai.yaml(Codex용 표시 이름)도 들어 있습니다.