ホーム / 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.

できること

エージェントを「赤から緑へ」のループに沿って進めるだけでなく、残す価値のあるテストとは何かを示します。良いテストと悪いテスト、モックすべき場面という 2 つの参考ノートが付いています。

仕組み

  1. まず接点を合意: テストを書く前に、どの公開インターフェースを通してテストするかを挙げ、あなたに確認します。確認していない接点ではテストを書きません。
  2. 1 切れずつ: テスト 1 つ、最小の実装 1 つ、を繰り返します。先に全テストを書き切ることはしません。
  3. 赤の次に緑: 失敗するテストが先、その後それを通す分だけのコードを書きます。
  4. 3 つのアンチパターンを避ける: 実装の詳細に結びついたテスト、期待値がコードと同じ方法で計算されていて必ず通る「自己証明」テスト、全テストを先に書く水平スライス。
  5. モックはシステムの境界だけ: 外部 API、時間、乱数など。自分のモジュールはモックしません。

向いている場面

新機能やバグ修正で、リファクタリングに耐えるテストがほしいとき。

知っておきたいこと

リファクタリングは意図的にループの外に置かれ、レビューの段階で行います。インターフェース設計について作者の codebase-design skill に触れていますが、あくまで任意の参考です。

補足とリスク

指示だけのファイルで、スクリプトはなく、ネットワーク接続も行いません。 Skill はエージェントにリポジトリ内でテストと実装コードを書かせ、テストコマンドを実行させます。 パッケージには元リポジトリの MIT ライセンス(LICENSE)と agents/openai.yaml(Codex 用の表示名)も含まれます。