首页 / 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 你自己的模块。

适合场景

做新功能和修 bug,并希望测试在重构后仍然有效。

需要了解

重构被刻意排除在循环之外,属于评审阶段。它提到作者的 codebase-design skill 用于接口设计,但只是可选参考。

说明与风险

纯指令文件:没有脚本、不联网。 Skill 会让代理在你的仓库里写测试和实现代码,并运行你的测试命令。 压缩包里另有原仓库的 MIT 协议文件 LICENSE 和 agents/openai.yaml(供 Codex 使用的显示名)。