說到 DD...筆者突然想起 DD 家族
| 名稱 | 全名 | 核心概念 | 常見用途 |
|---|---|---|---|
| TDD | Test-Driven Development(測試驅動開發) | 先寫測試、再寫實作。循環為:Red → Green → Refactor。 | 提高程式品質、減少 bug。 |
| BDD | Behavior-Driven Development(行為驅動開發) | 從「使用者行為/需求」撰寫測試案例(如 Given-When-Then)。 | 更貼近業務邏輯、促進溝通。 |
| DDD | Domain-Driven Design(領域驅動設計) | 聚焦業務核心領域,用「通用語言(Ubiquitous Language)」建模。 | 複雜系統架構設計。 |
| FDD | Feature-Driven Development(功能驅動開發) | 以功能(feature)為單位逐步開發、設計與交付。 | 適合大型團隊協作。 |
| CDD | Context-Driven Development(情境驅動開發) | 根據實際情境與專案需求調整流程與測試策略。 | 軟體測試與品質保證。 |
| SDD | Specification-Driven Development(規格驅動開發) | 以規格(specification)撰寫為基礎,產生自動化測試。 | 強化文件與測試整合。 |
| MDD / MDA | Model-Driven Development / Architecture(模型驅動開發/架構) | 用高階模型自動產生程式碼或系統結構。 | 工業級軟體、生成式開發。 |
| RDD | Responsibility-Driven Design(責任驅動設計) | 聚焦物件的「責任與協作」而非類別結構。 | 面向物件分析與設計。 |
| UDD | User-Driven Development(使用者驅動開發) | 依據使用者行為與回饋循環快速調整功能。 | 使用者體驗導向產品。 |
| Data-DD | Data-Driven Development(資料驅動開發) | 以數據分析結果作為開發與決策依據。 | AI / Big Data 專案常用。 |
TDD(Test-Driven Development,測試驅動開發)是一種以「先寫測試、後寫程式」為核心的開發方法。傳統開發往往先撰寫功能,再事後補上測試;而在 TDD 中,開發流程被刻意反轉。
先想清楚「期望程式行為」並以測試程式的方式描述它,然後再讓開發程式碼去通過這個測試。這個循環通常簡稱為 Red–Green–Refactor:
| 階段 | 動作 | 結果 |
|---|---|---|
| 紅燈 | 寫完測試、執行 pytest | 測試失敗,確認測試邏輯正確 |
| 綠燈 | 新增最小程式碼 | 所有測試通過 |
| 藍燈 | 重構程式、移除重複 | 維持通過狀態,程式更乾淨 |
TDD 的價值不僅在於提升測試覆蓋率,更重要的是它能引導開發者以「需求」為中心思考設計。因為測試事先定義了介面與期望結果,開發者在實作階段能明確知道「什麼是足夠的功能」,也能更容易拆分問題、重構程式,減少未來維護的難度。
以資料庫功能為例,TDD 會先定義出資料的 CRUD 行為測試(例如「新增使用者後能成功查詢出來」),再逐步實現每個資料操作。這種流程讓開發者能及早發現邏輯錯誤、結構不合理或資料設計問題。
| 類型 | 典型順序 | 說明 |
|---|---|---|
| 一般開發流程 | 實作功能 → 撰寫單元測試 → 驗證功能 | 先把功能寫出來,再補測試 |
| TDD(測試驅動開發)流程 | 撰寫單元測試 → 實作功能 → 重構 | 先寫測試,再寫讓測試通過的最小實作 |
在 TDD 模式下,每個設計決策都須具備「可測試性」。
以下為本專案設計的三大原則:
簡潔性(KISS)
nutrients_json。可測試性(Testability)
sqlite:///:memory:,確保每個測試函式皆獨立、快速且不干擾其他案例。最小可行實作(MVP)
AnalysisRecord此模型對應一筆 AI 食物分析結果,包含原始資料、摘要、營養成分等欄位。
| 欄位名稱 | 型別 | 說明 |
|---|---|---|
id |
Integer, Primary Key | 唯一識別碼 |
image_path |
String(255), Unique, Not Null | 圖片路徑(避免重複上傳) |
created_at |
DateTime (timezone-aware) | 建立時間 |
raw_analysis |
Text, Not Null | AI 回傳的原始文字或 JSON |
summary |
Text | 食物摘要描述 |
nutrients_json |
Text | 儲存營養細項(JSON 字串) |
此外,會提供對應的 nutrients 屬性,
讓開發者可直接以 Python 結構(list/dict)操作,而非手動處理 JSON 字串。
tests/conftest.py 與 tests/test_models.py。models.py 內的最小邏輯(nutrients property、timezone-aware 時間欄位)。Model.query.get → db.session.get)。測試案例需驗證以下行為:
| 測試項目 | 驗證內容 |
|---|---|
| 建立紀錄 | 可建立一筆 AnalysisRecord,並寫入 nutrients(Python 結構) |
| JSON 正確儲存 | 確認 nutrients_json 內為合法 JSON 字串 |
| 查詢行為 | 能正確讀回同筆紀錄與對應欄位 |
| 更新行為 | 更新 summary 後可正確讀回新值 |
| 刪除行為 | 可刪除該筆紀錄並驗證資料消失 |
| 進階測試 | created_at 欄位自動生成且為 timezone-aware |
tests/conftest.py)此檔案負責建立 Flask 測試應用與測試資料庫。
import pytest
from flask import Flask
try:
from app import create_app as _create_app
except Exception:
_create_app = None
try:
from models import db as _db
except Exception:
_db = None
@pytest.fixture(scope="function")
def app():
"""建立測試用 Flask App,使用 in-memory SQLite"""
config = {
"TESTING": True,
# 使用 `sqlite:///:memory:` 讓測試資料存在記憶體中。
"SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:",
"SQLALCHEMY_TRACK_MODIFICATIONS": False,
}
if _create_app is not None:
app = _create_app(config)
else:
app = Flask(__name__)
app.config.update(config)
if _db is None:
raise RuntimeError("找不到 models.db,請確認 models.py 有定義 db = SQLAlchemy()")
_db.init_app(app)
with app.app_context():
_db.create_all()
yield app
_db.session.remove()
_db.drop_all()
@pytest.fixture
def db(app):
"""提供資料庫 session 給測試案例"""
return _db
tests/test_models.py)import json
from models import AnalysisRecord, db
def test_analysisrecord_crud_and_nutrients_json(db):
"""測試 CRUD 與 JSON 欄位行為"""
rec = AnalysisRecord(
image_path="uploads/test_apple_pie.jpg",
raw_analysis="{\"ai\":\"result\"}",
summary="蘋果派 測試"
)
rec.nutrients = [
{"nutrient": "calories", "value": 250},
{"nutrient": "fat", "value": 12.5}
]
db.session.add(rec)
db.session.commit()
# 查詢
found = AnalysisRecord.query.filter_by(image_path="uploads/test_apple_pie.jpg").first()
assert found is not None
assert found.summary == "蘋果派 測試"
assert isinstance(found.nutrients, list)
assert found.nutrients[0]["nutrient"] == "calories"
# 驗證 JSON 結構
parsed = json.loads(found.nutrients_json)
assert parsed[1]["value"] == 12.5
# 更新
found.summary = "更新摘要"
db.session.commit()
updated = db.session.get(AnalysisRecord, found.id)
assert updated.summary == "更新摘要"
# 刪除
db.session.delete(updated)
db.session.commit()
assert db.session.get(AnalysisRecord, found.id) is None
明天再繼續...