Skip to content
White's Tech Notes
Go back

同一語意寫成兩份實作、只有一份有測試,會靜默分岔

Engineering PracticesEdit page

問題

台股選股工具的月營收資料永遠落後一期。

8 月營收 9/10 公布,9/12 查 DB 還是只有 7 月——而且不只一檔,全 watchlist 298 檔都沒進來,monthly_revenue 最後寫入停在 9/1。

最難發現的地方:排程 log 每小時都印 [fetch-financials] 完成,exit code 0,沒有任何錯誤。

原因

抓取前會先查 DB 決定哪些要抓(skip-if-fresh)。門檻由 expected_latest() 算:

# financial_check.py:93
last_m_yr, last_m_mo = (yr, mo - 2) if mo > 2 else (yr - 1, 12 + mo - 2)
month_threshold = last_m_yr * 100 + last_m_mo   # 永遠「上上月」

9 月 → 門檻 202607。DB 已有 7 月 → monthly_ok = True → 整個 9 月都不抓 8 月。 等到 10 月,門檻才進位到 202608 去抓 8 月——但那時 9 月營收又已公布。

不是偶發延遲,是系統性落後一期。

而同一個檔案裡,latest_available_monthly() 早就有正確規則:

# financial_check.py:115
# 台股月營收法定每月 10 日前公布上月,保守抓「11 日(含)後可用上月」
if day >= 11:
    last_yr, last_mo = (yr, mo - 1) if mo > 1 else (yr - 1, 12)

它有 5 個邊界測試(跨年、2 月、公布日前後)。expected_latest() 一個測試都沒有。

兩份邏輯表達的是同一件事——「哪一期資料已經可以拿到」——各寫各的,只有一份被測試釘住。

解法

刪掉硬算的那份,沿用有測試的那份:

month_threshold = latest_available_monthly(today)

TDD 走完:先寫測試,看它以 assert 202607 == 202608 失敗(失敗訊息本身就是 bug),再改實作。

補的三個測試裡,最關鍵的是這個:

def test_expected_latest_monthly_matches_latest_available():
    for d in (date(2026, 1, 10), date(2026, 1, 11), date(2026, 2, 10),
              date(2026, 5, 11), date(2026, 9, 12), date(2026, 12, 31)):
        assert expected_latest(d)[0] == latest_available_monthly(d), d

它不驗證任何業務規則,只釘住「這兩個函式不准再分岔」。跨年、2 月、公布日前後的邊界一次涵蓋。

307 → 310 passed。修後補回 8 月營收 291 檔。

關鍵洞察

同一個語意在程式裡出現第二份實作,就一定會分岔。 分岔之後,有測試的那份是對的,沒測試的那份靜默錯下去,而且錯的通常是被更多人呼叫的那份。

與其靠 code review 抓,不如寫一個「斷言兩者相等」的測試。這種測試不驗證業務規則,只釘住不變量,成本三行。

第二條:批次任務印「完成」不等於有做事。

skip-if-fresh 這類流程,log 的「完成」只證明流程跑完,不證明資料進來了。同一個專案上個月才踩過另一版:MOPS 重大訊息的下游過濾寫錯,API 抓回全市場快照卻只留 30 檔,一樣沒報錯,一樣漏了 94% 的資料。

兩次都是下游判斷錯誤造成的資料遺失,上游不會報錯。

驗證方式不能是讀 log,要直接查資料:

SELECT MAX(fetched_at) FROM monthly_revenue;
SELECT year, month, COUNT(*) FROM monthly_revenue WHERE year=2026 GROUP BY year, month;

排程類的健康檢查應該盯「最新資料的時間戳」,不是盯 exit code。



Previous Post
flock 的 fd 會被子進程繼承,讓 cron 監控靜默死亡
Next Post
instaloader 的 get_posts() 看不到 Reels 分頁