Debug + Test + Reality
ใน lab — code รันได้ · ใน production — code พัง · สัปดาห์นี้แยกผู้พัฒนามืออาชีพออกจากนักศึกษาปี 1 · เรียน "ระเบียบวิธีหา bug" · เขียน test ที่ ทดสอบจริง (ไม่ใช่ test ที่แค่ผ่าน) · และ ตรวจ test ที่ AI สร้าง ก่อนเชื่อ
เป้าหมายสัปดาห์นี้
- ใช้ scientific debugging 5 ขั้น (reproduce → isolate → hypothesize → test → fix)
- สร้าง Minimum Reproducible Example (MRE) เมื่อขอความช่วยเหลือ
- อ่าน stack trace + ใช้ debugger + write assert + pytest
- เขียน test ตาม AAA pattern · ใช้ mocking · วัด coverage
- ตรวจ test ที่ AI สร้าง ตาม checklist 8 ข้อ
- จับ "tests that pass but don't test" — bug ใหญ่ที่สุดของ AI-era
- เซ็ต CI/CD เบื้องต้น (GitHub Actions)
- ฝึก mocking สำหรับ hardware / sensor (engineering)
🔬 Scientific Debugging — 5 ขั้นไม่ "เดามั่ว"
นักศึกษาปี 1 เจอ bug → "ลองเปลี่ยนนู่นนี่ดู" · "wiggle and hope" · มืออาชีพใช้วิธีวิทยาศาสตร์ — เร็วกว่าและเรียนรู้ทุกครั้ง
ทำให้พังซ้ำได้] R --> I[2. Isolate
หาขอบเขตที่พัง] I --> H[3. Hypothesize
เดาสาเหตุ] H --> T[4. Test hypothesis
เปลี่ยน 1 อย่าง] T -->|ตรง| F[5. Fix + Test
+ regression test] T -->|ไม่ตรง| H F --> done[✅ done] classDef step fill:#1e3a5f,stroke:#3776ab,color:#fff classDef bad fill:#5c1818,stroke:#ef4444,color:#fff classDef good fill:#064e3b,stroke:#34d399,color:#fff class bug bad class R,I,H,T step class F,done good
📋 5 ขั้น กับตัวอย่าง
| ขั้น | ถาม | ตัวอย่าง |
|---|---|---|
| 1. Reproduce | "ทำให้พังซ้ำได้มั้ย?" | "feed Mochi 4 ครั้ง → hunger เป็น -2" · ทุกครั้งที่ทำ = พังเหมือนเดิม |
| 2. Isolate | "พังที่ไหน?" | print หรือ debugger ดู: feed ครั้งที่ 4 → hunger = 1, ลบ 3 → -2 · มาจาก feed() ไม่มี max(0, ...) |
| 3. Hypothesize | "น่าจะเป็นเพราะ..." | "hunger ไม่มี lower bound · ขั้นเดียวที่ลด hunger คือ feed → ต้องใส่ max(0, ...)" |
| 4. Test hypothesis | "ถ้าถูก จะเห็น Y · ลองดู" | เพิ่ม self.hunger = max(0, self.hunger - 3) · feed 4 ครั้ง → hunger = 0 ✅ |
| 5. Fix + regression test | "แก้แล้ว · ป้องกันไม่ให้กลับมา" | เพิ่ม test_hunger_never_negative() ใน pytest · commit |
🔍 Minimum Reproducible Example (MRE)
เมื่อขอความช่วยเหลือ (จาก AI, เพื่อน, Stack Overflow) — ต้อง "ย่อ bug ให้เล็กที่สุด" · MRE = code ที่ สั้นที่สุดที่ยังพังเหมือนเดิม
🧰 Debugging Toolbox — เครื่องมือเพิ่มเติม
| วิธี | ใช้เมื่อ | ทำยังไง |
|---|---|---|
| 🦆 Rubber Duck Debugging | ตอนคิดติด | อธิบาย code ของคุณให้ ตุ๊กตาเป็ด / เพื่อน / AI ฟังทีละบรรทัด · 70% จะเจอ bug ระหว่างอธิบาย |
| ⚖️ Binary Search Debugging | ไม่รู้ว่า bug ใน code ส่วนไหน | comment ครึ่ง code · ดูว่ายังพังมั้ย · ถ้าพัง → bug ในที่ยังเหลือ · ถ้าหาย → bug ในที่ comment |
| 🌳 git bisect | เคยใช้ได้ · ตอนนี้พัง · ไม่รู้ commit ไหนเป็นต้นเหตุ | git bisect start · mark good/bad · git ค้นเองด้วย binary search |
| 📖 อ่าน docs (จริง ๆ) | library ไม่ทำงานตามที่คาด | เปิด official docs · อ่าน method signature · เช็ค type ที่คืน · 50% ของ bug มาจากเข้าใจ API ผิด |
| 🪄 ส่ง bug ให้ AI | หลังทำ MRE แล้ว · ยังไม่เข้าใจ | paste MRE + error + "ฉันคาดว่า X · ได้ Y · ทำไม?" — AI ตอบเร็วและมักถูก |
🐞 อ่าน Stack Trace ให้เป็น
Traceback (most recent call last):
File "main.py", line 42, in <module>
process_students(students)
File "main.py", line 28, in process_students
grade = calc_grade(s["score"])
File "grading.py", line 8, in calc_grade
return rules[avg]
TypeError: list indices must be integers or slices, not str
วิธีอ่าน — "ล่างขึ้นบน"
- TypeError: list indices must be integers — error คือใช้ str เป็น index ของ list
- grading.py line 8 — เกิดที่
return rules[avg] - ดู context ขึ้นไป —
avgมาจาก parameter ของcalc_grade - ขั้นถัดไป —
main.py line 28ส่งs["score"]เข้าไป - แต่
s["score"]เป็น string (จาก CSV ที่ยังไม่แปลง int)
🧪 Print Debugging — เริ่มที่นี่
เครื่องมือ debug แรกที่นักศึกษาควรใช้ — เพิ่ม print() ในจุดสำคัญ
def calc_grade(score):
print(f"DEBUG calc_grade: score={score} type={type(score)}") # ✨ เพิ่มชั่วคราว
if score >= 80:
return "A"
...
# รัน → จะเห็น "DEBUG calc_grade: score=85 type=<class 'str'>"
# พบทันทีว่า score เป็น str ไม่ใช่ int
logging module แทน — มี level (DEBUG / INFO / WARNING / ERROR) เปิด-ปิดได้
🔍 Cursor / VS Code Debugger
ขั้นสูงกว่า print — "หยุด code กลางทาง · ดูค่าตัวแปร · เดินทีละบรรทัด"
- เปิดไฟล์ Python ใน Cursor
- คลิกซ้ายของเลขบรรทัด → ขึ้นจุดแดง (breakpoint)
- กด F5 หรือ Run → Start Debugging
- โปรแกรมจะหยุดที่ breakpoint — ดูค่าตัวแปรในแถบซ้าย
- กด F10 (step over) หรือ F11 (step into) เดินทีละบรรทัด
✅ Assert — Test แบบเร็ว
def calc_grade(score):
if score >= 80: return "A"
elif score >= 70: return "B"
elif score >= 60: return "C"
else: return "F"
# ทดสอบทันทีในไฟล์เดียวกัน
assert calc_grade(85) == "A"
assert calc_grade(75) == "B"
assert calc_grade(65) == "C"
assert calc_grade(45) == "F"
print("✅ All tests passed")
ถ้า assert ผิด — โปรแกรมหยุด พร้อม AssertionError · ใช้ระหว่าง dev เพื่อ catch bug เร็ว ๆ
🧬 pytest — Test framework
เมื่อ test เริ่มเยอะ ใช้ pytest · ติดตั้ง: pip install pytest
tests/test_grading.py
from grading import calc_grade
def test_a_grade():
assert calc_grade(85) == "A"
assert calc_grade(80) == "A"
def test_b_grade():
assert calc_grade(75) == "B"
def test_f_grade():
assert calc_grade(40) == "F"
assert calc_grade(0) == "F"
def test_edge_zero():
assert calc_grade(0) == "F"
def test_negative():
# spec ไม่ได้บอก แต่เราอยากให้ behavior ชัด
assert calc_grade(-1) == "F" # หรือ raise error?
รัน: pytest ใน terminal · จะเห็นผลทุก test
$ pytest
========================= test session starts =========================
collected 5 items
tests/test_grading.py ..... [100%]
========================== 5 passed in 0.02s ==========================
parametrize — test หลาย case ใน function เดียว
import pytest
@pytest.mark.parametrize("score,expected", [
(100, "A"), (85, "A"), (80, "A"),
(75, "B"), (70, "B"),
(65, "C"), (60, "C"),
(50, "D"), (40, "F"), (0, "F"),
])
def test_calc_grade(score, expected):
assert calc_grade(score) == expected
🚨 Error Handling — try/except/finally/raise
W04-W05 สอน "อ่าน error" · W13 ก่อนหน้านี้สอน "test ว่า error เกิด" · แต่ "เขียน code ที่ handle error ดี" ยังไม่ครอบ · นี่คือ producer-side ของ error
🌳 Exception Hierarchy ใน Python
Python มี exception class ที่สืบทอด · จับ class แม่ = จับลูกหลานทั้งหมด
BaseException
└── Exception ← จับตรงนี้ ครอบเกือบทุกอย่าง
├── ArithmeticError
│ ├── ZeroDivisionError
│ └── OverflowError
├── LookupError
│ ├── KeyError ← dict["x"] · "x" ไม่มี
│ └── IndexError ← list[100] · เกินขนาด
├── OSError
│ ├── FileNotFoundError
│ └── PermissionError
├── ValueError ← int("abc")
├── TypeError ← 1 + "two"
├── AttributeError ← obj.no_such_method()
└── ImportError
🔧 try/except/else/finally — โครงสร้างเต็ม
| Block | เมื่อทำ | ใช้สำหรับ |
|---|---|---|
try | เสมอ | code ที่อาจพัง |
except | เมื่อ error เกิด | handle / fallback |
else | เมื่อ try ไม่พัง | "happy path follow-up" |
finally | เสมอ (พังหรือไม่ก็ทำ) | cleanup (ปิดไฟล์, ปิด connection) |
📛 raise — ส่ง error ของตัวเอง
🎨 Custom Exception — สร้าง error class ของตัวเอง
class GradingError(Exception):
"""Base exception สำหรับ Grading App"""
pass
class InvalidScoreError(GradingError):
"""Score อยู่นอกช่วง 0-100"""
pass
class IncompleteSubmissionError(GradingError):
"""ส่งงานไม่ครบ"""
def __init__(self, missing: list):
self.missing = missing
super().__init__(f"ขาด: {', '.join(missing)}")
# ใช้
def grade(submission):
if not submission.get("drawing"):
raise IncompleteSubmissionError(["drawing"])
if submission["score"] < 0:
raise InvalidScoreError(...)
# caller จับ class แม่ = ครอบทุก subclass
try:
grade(sub)
except GradingError as e:
log.error(f"Grading failed: {e}")
📋 Best Practices — Error Handling Rules
| กฎ | เพราะ |
|---|---|
| 1. จับเฉพาะ error ที่รู้ว่าจะเกิด | การจับ except: เปล่า = ซ่อน bug ที่เราไม่รู้ |
2. ห้าม except: pass |
"swallow error" = ที่มาของ bug ที่ debug ไม่เจอ · log ทุกครั้งที่จับ |
| 3. raise ใหม่ดีกว่าเปลี่ยน error type | ใช้ raise NewError(...) from e · เก็บ original cause |
| 4. validate input ที่ boundary | raise ที่จุดรับ input · ไม่ปล่อยให้ error เกิดลึก ๆ ใน code |
| 5. ใช้ specific exception | ValueError("score ต้อง 0-100") > Exception("bad") |
| 6. finally สำหรับ cleanup เท่านั้น | ปิด file, db connection, network · ไม่ใช่ business logic |
7. ใช้ context manager (with) |
with open(f) = auto-close · ดีกว่า try/finally manual |
# ❌ AI ชอบทำ — bug ที่เลวร้ายที่สุด
try:
result = call_api()
except Exception:
result = None # error หาย · ไม่มี log
# ✅ ทำให้ดี
try:
result = call_api()
except requests.Timeout:
log.warning("API timeout · retry...")
result = None
except requests.RequestException as e:
log.error(f"API failed: {e}")
raise # re-raise ถ้า caller ต้องรู้
🅰️🅰️🅰️ AAA Pattern — โครงสร้างของ test ที่ดี
ทุก test ที่ดี = 3 ส่วน Arrange · Act · Assert — แยกบรรทัดให้ชัด · อ่านง่าย · ดูออกว่า test "พิสูจน์อะไร"
def test_feed_reduces_hunger():
# 🟢 ARRANGE — เตรียม object + ข้อมูล
pet = Pet("Mochi")
pet.hunger = 8
# 🟡 ACT — เรียก behavior ที่ทดสอบ
pet.feed("แครอท")
# 🔴 ASSERT — เช็คผล
assert pet.hunger == 5 # หิวลดลง 3
📝 Test naming — บอก behavior ไม่ใช่ implementation
| ❌ ชื่อแบบเทคนิค | ✅ ชื่อแบบ behavior |
|---|---|
test_feed | test_feed_reduces_hunger_by_three |
test_grade | test_score_80_returns_grade_A |
test_validate | test_negative_score_raises_value_error |
test_when_X_then_Y
อ่านชื่อ → รู้ทันทีว่า test อะไร · เมื่อ test fail → ชื่อบอกปัญหาทันที ·
ดีกว่าเห็น "test_feed failed"
🎭 Mocking — แทนของจริงตอน test
ปัญหา: test ต้องเรียก IQAir API จริง → ช้า · ใช้ key · network แตก · ผลไม่แน่นอน · ทางแก้: mock — ปลอม response ให้
💡 เมื่อไหร่ใช้ Mock
| สิ่งที่ควร mock | เพราะ |
|---|---|
| External API (IQAir, LINE, Claude) | ช้า · ใช้ token · ผลไม่คงที่ |
| Database | state กระทบ test อื่น |
เวลา (datetime.now()) | test ไม่ deterministic |
| Random | ผลเปลี่ยนทุก run |
| File I/O | กระทบ filesystem จริง |
| Hardware (sensor, motor) | ไม่มีอุปกรณ์ใน CI |
🐾 ตัวอย่าง: Mock IQAir API
# aqi.py — production code
import requests
def fetch_pm25(city):
r = requests.get(f"https://api.airvisual.com/.../{city}", timeout=10)
r.raise_for_status()
return r.json()["data"]["current"]["pollution"]["aqius"]
def alert_if_unhealthy(city):
pm = fetch_pm25(city)
if pm > 100:
return f"⚠️ {city}: PM2.5 = {pm}"
return None
# test_aqi.py
from unittest.mock import patch
import aqi
@patch("aqi.fetch_pm25") # 🎭 mock fetch_pm25
def test_alert_when_unhealthy(mock_fetch):
mock_fetch.return_value = 150 # ปลอม PM2.5 = 150
result = aqi.alert_if_unhealthy("Bangkok")
assert "PM2.5 = 150" in result
mock_fetch.assert_called_once_with("Bangkok")
@patch("aqi.fetch_pm25")
def test_no_alert_when_safe(mock_fetch):
mock_fetch.return_value = 50
result = aqi.alert_if_unhealthy("Bangkok")
assert result is None
- Mock มากเกินไป — test แค่ "mock ทำงาน" ไม่ได้ test logic จริง
- Mock ที่ตำแหน่งผิด —
@patch("requests.get")❌ vs@patch("aqi.requests.get")✅ (mock ที่ "ที่ที่ใช้" ไม่ใช่ "ที่ที่ define") - ไม่ assert ว่า mock ถูกเรียก — code อาจไม่ได้เรียก fetch_pm25 เลย
📊 Test Coverage — วัดว่า test ครอบคลุมแค่ไหน
pytest-cov รัน test แล้วบอกว่า "บรรทัดไหนของ code ที่ test ไม่ได้แตะ"
# ติดตั้ง
pip install pytest-cov
# รัน
pytest --cov=src --cov-report=term-missing
# output:
Name Stmts Miss Cover Missing
-------------------------------------------------
src/grading.py 12 2 83% 18-19 ← บรรทัดที่ test ไม่แตะ
src/pet.py 20 0 100%
-------------------------------------------------
TOTAL 32 2 94%
- < 50% — ส่วนใหญ่ไม่ได้ test · เสี่ยง
- 60-80% — พอใช้สำหรับ project นักศึกษา · happy path + edge cases หลัก ๆ ครบ
- 80-90% — ดี · production-ready
- > 95% — ระวัง! · มักทำได้ด้วยการ test "บรรทัด" ไม่ใช่ "behavior" · false confidence
| Category | ตัวอย่าง |
|---|---|
| Empty / Zero | list ว่าง, string ว่าง, 0, None |
| Boundary | ค่าที่อยู่ตรง threshold (เช่น 80 พอดี, 79.999) |
| Negative / Out of range | -1, > 100, > max_size |
| Wrong type | ใส่ string ใน int parameter |
| Special characters | ภาษาไทย, emoji, quote, < > |
| Duplicate | เพิ่ม id เดียวกัน 2 ครั้ง |
| Concurrent | 2 คนเขียน file พร้อมกัน |
| Network | API ช้า, timeout, return 500 |
| Time zone | UTC vs ICT, daylight saving |
| Large input | file 1 GB, list 1 ล้านตัว |
🤖 AI Test Review — Checklist 8 ข้อ ก่อนเชื่อ Test ที่ AI สร้าง
สถานการณ์: คุณบอก AI "เขียน test สำหรับ calc_grade" · AI สร้างให้ 10 test
ทั้งหมดผ่าน · หยุดก่อน! · test ที่ผ่าน ≠ test ที่ดี ·
ตรวจ 8 ข้อนี้ก่อนเชื่อ
✅ Checklist 8 ข้อ
| ข้อ | คำถาม | วิธีตรวจ |
|---|---|---|
| 1. Test test "behavior" หรือ "implementation"? | Test ดูว่า "result ถูก" หรือ "เรียก function X ภายใน"? | ลอง refactor implementation ขณะ test เดิม — ถ้า test fail = test implementation (ไม่ดี) |
| 2. ครอบ edge cases มั้ย? | 0 · empty · None · negative · max · type ผิด | เช็คจาก "Edge Cases ต้องคิด" table · อย่างน้อย 5 หมวด |
| 3. ทุก test "fail ตอน code ผิด" จริงมั้ย? | เปลี่ยน code ผิดเล็ก ๆ · test ควร fail | Mutation testing — เปลี่ยน >= 80 เป็น >= 81 ·
ถ้าไม่มี test fail = test ไม่จริง |
| 4. Test มี hidden assumption มั้ย? | Test ทำงานเพราะ "เผอิญ" หรือ "ตามที่ควร"? | เปลี่ยน input คล้าย ๆ — เช่น 85 → 84.9 · ผลควรชัดเจน |
| 5. Mock realistic มั้ย? | Mock return value เป็น format จริงของ API มั้ย | เทียบ mock value กับ response จริงของ API · ใช้ response.json() ของจริง 1 รอบเป็น reference |
| 6. Test อิสระจากกันมั้ย? | รัน test แค่อันใดอันหนึ่งได้มั้ย · order matter มั้ย | pytest tests/test_x.py::test_specific · ถ้า fail = ไม่ isolated |
| 7. Test ไม่ผิดจาก "การ copy-paste" มั้ย? | AI ก็อปแล้วเปลี่ยนแค่ตัวเลข · ลืมเปลี่ยน expected | อ่านทุก assert · เปรียบเทียบกับ test ก่อนหน้า |
| 8. มี "regression test" สำหรับ bug ที่เจอมั้ย? | หลังแก้ bug · มี test ป้องกัน bug นั้นกลับมามั้ย | ทุก bug ที่แก้ → ต้องมี test เพิ่ม · test ชื่อ test_regression_issue_X |
🐛 Tests That Pass But Don't Test — false confidence
นี่คือปัญหาที่ใหญ่ที่สุดของ AI-generated tests · test ผ่าน 100% — แต่ ไม่ได้พิสูจน์อะไร
🚩 Pattern 1: Test แค่ "code รันได้ไม่ crash"
🚩 Pattern 2: Test สะท้อนความผิดของ code
# ❌ Code มี bug
def add(a, b):
return a - b # BUG: ลบแทนบวก
# ❌ AI เขียน test โดยรัน code ก่อน แล้วใช้ผลเป็น expected
def test_add():
assert add(5, 3) == 2 # ✅ ผ่าน · แต่ผิดจาก spec
# Test ผ่าน 100% · ทั้งที่ code ผิด!
# ✅ Test ที่ดี — เขียนจาก spec ก่อนรู้ผล
def test_add():
# spec บอก: add(a, b) = a + b
assert add(5, 3) == 8 # ❌ FAIL — เห็น bug!
🚩 Pattern 3: Test ที่ทดสอบ mock ตัวเอง
@patch("aqi.fetch_pm25")
def test_alert(mock_fetch):
mock_fetch.return_value = 150
result = aqi.alert_if_unhealthy("Bangkok")
# ❌ Bad — test แค่ว่า fetch_pm25 ถูกเรียก
mock_fetch.assert_called_once()
# → ไม่ได้ check ผลลัพธ์ของ alert_if_unhealthy เลย
# ✅ Good — check behavior ที่อยากให้เกิด
assert "150" in result
assert "⚠️" in result
🚩 Pattern 4: Test ไม่มี edge case
# ❌ AI เขียน — ครอบ happy path เท่านั้น
def test_grade():
assert calc_grade(85) == "A"
assert calc_grade(75) == "B"
assert calc_grade(65) == "C"
assert calc_grade(45) == "F"
# ✅ Test ที่ดี — มี boundary + edge case
def test_grade():
# happy path
assert calc_grade(85) == "A"
# boundaries (ตรงเกณฑ์)
assert calc_grade(80) == "A"
assert calc_grade(79) == "B" # ← จุดอ่อนของ off-by-one
# edge cases
assert calc_grade(0) == "F"
assert calc_grade(100) == "A"
# invalid
import pytest
with pytest.raises(ValueError):
calc_grade(-1)
🧬 Mutation Testing — ทดสอบ "ว่า test จริง" ด้วยมือ
Idea: ตั้งใจ "ทำลาย code" ทีละจุด · ดูว่า test fail มั้ย · ถ้า test ไม่ fail = test ไม่ test ส่วนนั้นจริง
| Mutation | เปลี่ยนใน code | Test ที่ดีต้อง fail |
|---|---|---|
| เปลี่ยน operator | >= → > | ใช่ — boundary test ต้องจับ |
| เปลี่ยน threshold | 80 → 81 | ใช่ |
| swap arguments | divide(a, b) → divide(b, a) | ใช่ |
| negate boolean | if x → if not x | ใช่ |
| ลบบรรทัด validation | ลบ if score < 0: raise | ใช่ |
🤥 จับ AI Hallucination — ระดับ Medium
| รูปแบบ | วิธีจับ |
|---|---|
Function ที่ไม่มีจริง — เช่น pandas.smart_clean() | รัน → AttributeError |
Parameter ที่ไม่มี — requests.get(url, retry=3) | ตรวจ docs / รัน → TypeError |
| Logic ผิดแบบ subtle — off-by-one, <= แทน < | test boundary case |
| "ใช้ได้แล้ว!" ทั้งที่ยังไม่ test | รันด้วยตัวเอง |
| เพิ่ม feature ที่ไม่ขอ | diff กับ spec บรรทัดต่อบรรทัด |
| เปลี่ยน behavior ที่ไม่ขอ | compare กับ test cases เดิม (regression) |
| Library version ไม่ตรง — ใช้ method ที่ deprecate ไป | เช็ค requirements.txt + docs |
| Confidence สูง คำตอบมั่ว | verify ทุกครั้ง · "show me, don't tell me" |
📋 "Works on My Machine" — ปัญหาคลาสสิก
code รันได้บนเครื่องคุณ · ไม่ทำงานบนเครื่องเพื่อน · เพราะ:
- Library version ต่าง — ใส่ใน
requirements.txt+ เลข version - Python version ต่าง — บอกใน README ว่าใช้ Python 3.11+
- OS ต่าง — path มี
\vs/— ใช้pathlib.Pathเสมอ - Locale / encoding — เปิดไฟล์ Thai ต้อง
encoding="utf-8" - Environment variable หาย — ใส่ใน
.env.example - Database state — ลืม migration / seed data
- External service — API key ของคุณคนเดียว · เพื่อนไม่มี
1. clone repo ลงเครื่องใหม่ / folder ใหม่
2. python -m venv .venv && activate
3. pip install -r requirements.txt
4. cp .env.example .env (เติมค่า)
5. python main.py
ถ้าทำงานได้ = พอใช้
📊 Logging แทน print
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s",
)
log = logging.getLogger(__name__)
def fetch_aqi(city):
log.info(f"Fetching AQI for {city}")
try:
r = requests.get(URL, timeout=10)
r.raise_for_status()
log.debug(f"Response: {r.json()}")
return r.json()
except requests.RequestException as e:
log.error(f"API failed: {e}")
return None
⚙️ CI/CD with GitHub Actions — รัน test ทุก push อัตโนมัติ
CI (Continuous Integration) = ทุกครั้งที่ push code · server รัน test ให้อัตโนมัติ · ถ้า test fail → แสดง ❌ บน GitHub · ป้องกัน merge code ที่พัง · ฟรีสำหรับ public repo
📝 Setup ใน 3 นาที
# สร้างไฟล์: .github/workflows/test.yml
name: Python Tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install deps
run: |
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run tests
run: pytest --cov=src --cov-report=term-missing
- name: Lint
run: |
pip install ruff
ruff check .
หลังจาก push:
- เปิด GitHub → tab "Actions" → ดูผล
- ✅ = test ผ่าน · ❌ = fail · เห็น log เต็มได้
- เพิ่ม badge ใน README:

- เพื่อน clone repo → push pull request → คุณรู้ทันทีว่า test ผ่านมั้ย
- "fresh clone test" อัตโนมัติ — works on my machine ไม่หลุดเข้า main
- Portfolio: "repo ของฉันมี CI" = สัญญาณว่า dev มืออาชีพ
🎯 Common CI strategies
| Strategy | เมื่อไหร่ใช้ |
|---|---|
| Run all tests | project เล็ก-กลาง · test < 5 นาที |
| Run on push to main only | ประหยัด minutes |
| Run on PR | ก่อน merge — บังคับให้ test ผ่าน |
| Matrix test (multi Python) | library ที่ support หลาย version |
| Cache deps | เร็วขึ้น 30-60s |
| Run smoke tests on schedule | ตรวจว่า external API ยังทำงาน |
🔧 Engineering Testing — Sensor / Hardware patterns
วิศวกรรม ≠ pure software · มี hardware · sensor มี noise · serial port ไม่เสถียร · ทดสอบยังไงไม่ต้องเอาเครื่องจริงทุกครั้ง?
🎭 Pattern 1: Stub sensor — ใช้ class ที่ทำตัวเหมือน sensor
# production
class TempSensor:
def __init__(self, port="/dev/ttyUSB0"):
import serial
self.serial = serial.Serial(port, 9600)
def read(self):
line = self.serial.readline().decode()
return float(line.strip())
# test stub — ทำตัวเหมือน sensor
class FakeTempSensor:
def __init__(self, values):
self.values = iter(values)
def read(self):
return next(self.values)
# code ที่ใช้ — รับ sensor เป็น dependency
class TempLogger:
def __init__(self, sensor):
self.sensor = sensor
self.history = []
def log(self):
t = self.sensor.read()
self.history.append(t)
return t
# test ใช้ Fake — ไม่ต้องมี hardware
def test_logger_tracks_history():
fake = FakeTempSensor([25.0, 26.5, 27.0])
logger = TempLogger(fake)
logger.log()
logger.log()
logger.log()
assert logger.history == [25.0, 26.5, 27.0]
def test_logger_handles_overheat():
fake = FakeTempSensor([95.0])
logger = TempLogger(fake)
t = logger.log()
assert t > 90 # alert range
🎬 Pattern 2: Replay test — บันทึก data จริง · เล่นซ้ำใน test
# บันทึก 1 ครั้ง — ตอนวัดจริง
import csv, datetime
with open("recorded_data.csv", "w") as f:
w = csv.writer(f)
w.writerow(["ts", "temp", "humidity"])
for _ in range(1000):
w.writerow([datetime.datetime.now(), sensor.read(), hum.read()])
# test — เล่นซ้ำจาก CSV ไม่ใช้ sensor จริง
class ReplayDataSource:
def __init__(self, csv_path):
import csv
with open(csv_path) as f:
self.rows = list(csv.DictReader(f))
self.idx = 0
def read(self):
row = self.rows[self.idx]
self.idx += 1
return float(row["temp"])
def test_analyze_24h_data():
src = ReplayDataSource("test_data/24h_normal.csv")
result = analyze_period(src, hours=24)
assert result["max_temp"] < 30
assert result["alerts"] == 0
🛠 Pattern 3: Calibration test
# Sensor มี noise — test ใช้ "ช่วง" ไม่ใช่ "ค่าตรง"
def test_temp_reading_within_tolerance():
sensor = TempSensor("/dev/ttyUSB0")
readings = [sensor.read() for _ in range(10)]
mean = sum(readings) / len(readings)
# known-good range สำหรับห้องที่ 25°C ± 2
assert 23 <= mean <= 27, f"temp out of expected range: {mean}"
# standard deviation ของ noise
import statistics
std = statistics.stdev(readings)
assert std < 0.5, f"noisy sensor: stdev = {std}"
🛡 Pattern 4: Safety test — ทดสอบ failure mode
def test_motor_stops_on_emergency():
motor = FakeMotor()
controller = SafetyController(motor)
controller.start()
assert motor.is_running()
controller.emergency_stop()
assert not motor.is_running()
assert motor.stop_time_ms < 100 # ต้องหยุดใน 100ms
def test_sensor_disconnect_triggers_alarm():
sensor = FakeTempSensor([25.0, None, None, None]) # disconnect
controller = ProcessController(sensor)
for _ in range(4):
controller.tick()
assert controller.alarm_state == "SENSOR_FAILURE"
- Sensor มี noise → test ช่วง · ไม่ test ค่าตรง
- Hardware ไม่มี → ใช้ Fake/Stub · inject เป็น dependency
- Data จริง → บันทึก replay · test ไม่ต้องต่อ hardware
- Safety test สำคัญที่สุด — emergency stop, sensor failure, power loss
🧪 Workshop — Test Suite for "Smart Lab Monitor"
เอา project จาก W12 มา เพิ่ม tests + handle error ดีขึ้น
-
สร้าง folder
tests/+tests/test_aqi.py+tests/test_storage.py+tests/test_alert.py -
เขียน test สำหรับ pure functions ตาม AAA pattern
— function ที่ไม่ต้อง network · เช่น
parse_response,format_alert,calc_average· ใช้test_when_X_then_Ynaming -
Mock external API
— ใช้
unittest.mock.patch("aqi.requests.get")· mock IQAir + LINE + Claude · เช็คทั้ง "called with correct args" และ "result ถูก" - เพิ่ม edge cases ครบ 5 หมวด — empty/zero · boundary · negative · wrong type · network failure (timeout, 500, malformed JSON)
- 🤖 ใช้ AI ช่วย generate test เพิ่ม — paste code + spec → "เขียน pytest 10 cases · มี edge case · ใช้ AAA pattern"
-
🔍 ตรวจ AI test ตาม Checklist 8 ข้อ
— เช็คทุก test ของ AI · ลอง mutation testing manual 3 จุด · บันทึกใน
ai-review.md -
📊 Run coverage
—
pytest --cov=src --cov-report=term-missing· ตั้งเป้า ≥ 70% · เพิ่ม test สำหรับบรรทัดที่ขาด -
⚙️ เพิ่ม GitHub Actions
—
.github/workflows/test.yml· push และดูว่า test ผ่าน · เพิ่ม badge ใน README - 🐛 Break test ตัวเอง — เปลี่ยน threshold ใน production code · ดูว่า test fail ที่ตรงไหน · ถ้าไม่ fail = test อ่อน
-
📝 Bug log
— บันทึก 3 bug ที่ test จับได้ใน
error-log.md· ใช้ format Scientific Debugging
🎯 Test Pyramid — ออกแบบ test ที่สมดุล
E2E
few · slow
"คนใช้จริงคลิกได้"
Integration
some · medium
"function หลายตัวคุยกัน"
Unit Tests
many · fast
"แต่ละ function ทำงานถูก"
เริ่มจาก unit test เยอะ ๆ ก่อน · integration น้อยกว่า · E2E น้อยที่สุด
ข้อผิดที่พบบ่อย
ส่งงานสัปดาห์นี้
- 📁
tests/folder ที่มี ≥ 15 test cases · ใช้ AAA pattern + naming - 🎭 ใช้ mock สำหรับ external API · มี test ที่ใช้ pre-recorded data (replay)
- 📊 Coverage ≥ 70% + screenshot output ของ
pytest --cov - 🤖
ai-review.md— review AI's tests ตาม checklist 8 ข้อ · บันทึก mutation testing 3 จุด - ⚙️ GitHub Actions setup · badge ใน README · ผ่านสีเขียว
- 📋
error-log.mdรวม 3 bug ที่เจอ + Scientific Debugging 5 ขั้น - 📝
edge-cases.mdครอบคลุม 5 หมวด - 📝 README อัพเดต วิธีรัน test + coverage badge
Reference จาก slide เดิม
เนื้อหานี้ ไม่มี ใน slide เดิม — เป็น Mainidea Foundation 8 (Debug) + 9 (Testing) + 10 (AI Operation) ที่ขาดไม่ได้ในยุค AI