在過去幾天,我已經完成了多個 API 整合成生活助手 App 的開發。
今天主要是學習讓這些 API 在各種不同情況下都能正常運作,並學會如何找到問題、分析原因,最後修正錯誤。
這一天的重點是學習測試和除錯,透過這些方法,能讓專案變得更穩定,更符合專業水準。
今天我的目標是建立一個自動化測試腳本,用來測試我寫的三個 API 模組,分別是天氣、空氣品質和新聞 API。
我會測試兩個重要部分:
功能測試(Functional Tests):確認這三個 API 都能正常工作,回傳正確的資料。
負向測試(Negative Tests):故意輸入錯誤的參數,或模擬錯誤狀況,看看程式能不能正確地報錯,防止系統出錯時崩潰。
最後,我會把常見的錯誤訊息和對應的除錯方法整理出來,這樣以後遇到 API 問題時,可以更快找到原因並修正。這樣的練習能讓我的程式更穩定、更專業,也更容易維護。
準備環境
建立 day29 資料夾
確保可匯入前面 Day28 的三個模組(weather、aqi、news)
準備 .env
OWM_API_KEY=我們的金鑰
CITY=Taipei
NEWS_LIMIT=5
建立測試主程式
這支程式會自動呼叫三個 API,並顯示成功與失敗結果。
import os, json, requests
from pathlib import Path
from datetime import datetime
from dotenv import load_dotenv
# 直接使用 Day28 的三個模組
from weather_client import get_weather, WeatherError
from aqi_client import get_aqi_by_coord, AQIError
from news_client import get_news, NewsError
ROOT = Path(__file__).resolve().parent
def line(ch="—", n=54): print(ch * n)
def ok(msg): print(f"[OK] {msg}")
def fail(msg): print(f"[ERROR] {msg}")
def info(msg): print(f"[INFO] {msg}")
def preview(text: str, n=120):
"""回應內文只看前 n 字,避免刷屏。"""
s = text.strip().replace("\n", " ")
return (s[:n] + "…") if len(s) > n else s
def assert_key(d: dict, key: str):
if key not in d or d[key] is None:
raise AssertionError(f"缺少或為 None:{key}")
def test_weather_ok(city="Taipei", units="metric", lang="zh_tw"):
"""功能測試:可以拿到城市天氣與座標"""
w = get_weather(city, units=units, lang=lang)
assert_key(w, "name"); assert_key(w, "temp")
assert_key(w, "lat"); assert_key(w, "lon")
ok(f"[Weather OK] {w['name']} {w['temp']} ({w['desc']})")
return w
呼叫天氣 API,檢查關鍵欄位(城市、溫度、經緯度)皆存在;通過就回傳結果。
def test_weather_bad_city():
# 負向測試:不存在的城市要回報錯誤(404)
bad = "__NO_SUCH_CITY__"
try:
get_weather(bad)
fail("[Weather 404] 應該拋錯,但沒有")
except WeatherError as e:
ok(f"[Weather 404] 捕捉到預期錯誤:{e}")
用錯誤城市測試應產生 WeatherError
;若沒有拋錯即視為失敗。
def test_aqi_ok(lat, lon, key):
# 功能測試:用座標查 AQI
aqi = get_aqi_by_coord(lat, lon, key=key)
# aqi 可能偶爾沒有,但至少欄位存在
assert "aqi" in aqi and "pm2_5" in aqi and "pm10" in aqi
ok(f"[AQI OK] AQI={aqi['aqi']} PM2.5={aqi['pm2_5']} PM10={aqi['pm10']}")
return aqi
用經緯度+金鑰呼叫 AQI,檢查必備欄位在;通過則列印數值並回傳。
def test_news_ok(limit=5):
# 功能測試:能拿到新聞清單(免金鑰)
news = get_news(limit=limit)
assert "count" in news and "items" in news
ok(f"[News OK] 共 {news['count']} 則,Top1:{news['items'][0]['title'] if news['count'] else '無'}")
return news
最新新聞 N 則,確認 count/items
存在;印出總數與第一則標題。
def test_timeout():
# 負向測試:超時(用 httpbin 製造 5 秒延遲,timeout=1)
url = "https://httpbin.org/delay/5"
try:
requests.get(url, timeout=1)
fail("[Timeout] 應該超時,但沒有")
except requests.exceptions.Timeout:
ok("[Timeout] 成功捕捉超時例外")
透過 httpbin 製造延遲,驗證 Timeout
例外能被捕捉。
def test_non_json():
# 負向測試:非 JSON 回應(JSONDecodeError)
url = "https://httpbin.org/html"
r = requests.get(url, timeout=10)
info(f"[Non-JSON] Content-Type: {r.headers.get('Content-Type')}")
try:
r.json()
fail("[Non-JSON] 應該無法解析 JSON,但沒有錯")
except ValueError as e:
ok(f"[Non-JSON] 無法解析 JSON(預期):{type(e).__name__}")
取回 HTML 後嘗試 r.json()
應丟 ValueError
;否則視為失敗。
def test_http_404():
# 負向測試:HTTP 404
url = "https://httpbin.org/status/404"
r = requests.get(url, timeout=10)
try:
r.raise_for_status()
fail("[HTTP 404] 應該 raise,但沒有")
except requests.exceptions.HTTPError as e:
ok(f"[HTTP 404] 捕捉到預期錯誤:{e}")
def test_http_429():
# 負向測試:HTTP 429(太多請求)
url = "https://httpbin.org/status/429"
r = requests.get(url, timeout=10)
info(f"[HTTP 429] Status={r.status_code}, Retry-After={r.headers.get('Retry-After')}")
try:
r.raise_for_status()
fail("[HTTP 429] 應該 raise,但沒有")
except requests.exceptions.HTTPError as e:
ok(f"[HTTP 429] 捕捉到預期錯誤:{e}")
利用 httpbin 直接返回 404/429,檢查 raise_for_status()
會丟 HTTPError
。
def main():
line(); print("Day29 測試 & 除錯(功能 + 負向)"); line()
load_dotenv(ROOT / ".env")
# 1) 環境檢查
key = os.getenv("OWM_API_KEY")
if not key:
fail("找不到 OWM_API_KEY(請在 .env 設定)"); return
city = os.getenv("CITY", "Taipei")
# 2) 功能測試:Weather / AQI / News
try:
w = test_weather_ok(city=city)
except Exception as e:
fail(f"Weather 功能失敗:{e}")
return
try:
aqi = test_aqi_ok(lat=w["lat"], lon=w["lon"], key=w["api_key"])
except Exception as e:
fail(f"AQI 功能失敗:{e}")
try:
news = test_news_ok(limit=int(os.getenv("NEWS_LIMIT", "5")))
except Exception as e:
fail(f"News 功能失敗:{e}")
# 3) 負向測試:404 / Timeout / 非 JSON / 429
line()
try: test_weather_bad_city()
except Exception as e: fail(f"Weather 404 測試程式出錯:{e}")
try: test_timeout()
except Exception as e: fail(f"Timeout 測試程式出錯:{e}")
try: test_non_json()
except Exception as e: fail(f"Non-JSON 測試程式出錯:{e}")
try: test_http_404()
except Exception as e: fail(f"HTTP 404 測試程式出錯:{e}")
try: test_http_429()
except Exception as e: fail(f"HTTP 429 測試程式出錯:{e}")
line(); ok("測試完成")
if __name__ == "__main__":
main()
執行結果:
功能測試主要確認三個 API 能順利拿到正確資料:天氣部分可以取得城市名稱、溫度和座標(經緯度);空氣品質(AQI)部分需要回傳 AQI 數值、PM2.5 與 PM10 的含量;新聞部分要抓到每則新聞的標題和連結。
負向測試則是故意模擬錯誤狀況,測試程式能否正確處理並回報錯誤,包括:
輸入錯誤的城市名稱會導致 404(找不到資料)
模擬 API 請求超時(timeout)
API 回傳非 JSON 格式,造成資料格式錯誤
收到 HTTP 錯誤碼,例如 429(請求過多,被限制使用)
這些測試確保程式在正常和異常情況下都能穩定運作,並能幫助我們快速發現和修正問題。
今天我學會了怎麼為自己的 API 專案建立自動化測試系統。透過這個系統,可以自動確認程式功能是否正常運作,並且還能做負向測試,讓程式能在錯誤情況下正確回報錯誤,而不是直接崩潰。這次練習讓我了解到,好的程式不只是能跑,而且要能穩定運作並且值得信賴。明天我會進入最後的收尾階段,準備完成這 30 天的挑戰。這次的經驗讓我更有信心面對未來專案中的問題。