我剛開始讓 AI Agent 幫忙改程式時,最常看到的不是 bug,而是一長串解釋。
測試失敗了,它會猜可能是 PostgreSQL 版本不同、Redis 有舊資料、環境變數沒設,或某個服務還沒起來。這些猜測有時候是對的,但它沒有辦法靠同一組指令把現場重建一次,只能繼續猜。
人碰到這種環境已經很煩,Agent 更糟。它會把環境問題當程式問題,然後很勤奮地改壞原本沒錯的程式碼。
我後來花時間把開發與測試環境放進 Docker Compose。目的不是追流行,也不是覺得所有東西都該容器化。我只想給人、Agent 和 CI 一個共同的重來按鈕。
我真正想消掉的是隱藏狀態
「在我電腦上會過」背後通常不是玄學,而是有東西沒寫進專案:
某人的 PostgreSQL 是另一個版本。
Redis 留著昨天測試的 key。
.env多了一個沒人記得的設定。Migration 跑到一半,資料表狀態跟別人不同。
測試順序剛好避開了殘留資料。
Docker Compose 沒有自動解決這些問題,但它逼我把資料庫版本、服務相依、環境變數和健康檢查寫下來。至少出錯時,大家查的是同一份設定。
我常用的重置流程大致如下:
docker compose -f compose.test.yml down -v
docker compose -f compose.test.yml up -d --wait
docker compose run --rm db-seeder
docker compose run --rm test-runner dotnet test
這裡最重要的是第一行。它把測試 volume 一起移除,確保下一輪不是沿用上次的資料。也因為如此,這份 compose 必須是測試專用,不能指到要長期保存的資料。
Agent 需要的不是完整說明書,是穩定入口
我一開始把啟動資料庫、跑 migration、匯入 seed、跑測試的步驟全部寫進說明文件。人看得懂,Agent 每次執行還是可能少一步。
後來我把入口縮成 Makefile:
test-unit:
docker compose run --rm app dotnet test tests/Unit
test-e2e:
docker compose -f compose.e2e.yml up -d --wait
docker compose run --rm e2e-runner dotnet test tests/E2E
docker compose -f compose.e2e.yml down -v
test-all: test-unit test-e2e
Agent 只需要知道 make test-all。底下換資料庫版本或多一個服務時,我改 Makefile 和 compose,不必期待每個 Agent Session 都重新讀懂一份十幾段的操作手冊。
這個入口也讓 CI 用同一條路。差別只剩誰下指令,不是每個執行者各有一套環境。
單元測試其實不需要真的資料庫
把環境容器化後,我也重新整理了測試邊界。
單元測試要測的是 OrderService 的規則,就不該每次啟動 PostgreSQL。外部相依透過介面注入,測試換成 stub 或 mock:
public sealed class OrderService
{
private readonly IOrderRepository _orders;
private readonly ICacheService _cache;
public OrderService(IOrderRepository orders, ICacheService cache)
{
_orders = orders;
_cache = cache;
}
}
需要確認 SQL、migration 和交易行為時,再跑使用真實 PostgreSQL 容器的整合測試。E2E 則把 API、資料庫、Redis、RabbitMQ 和瀏覽器測試一起啟動。
Docker 的價值不是讓每一條測試都變重,而是需要真實相依時,我可以用固定方式拿到它。
卡最久的是「服務起來了,但還不能用」
有次 E2E 在本機偶爾過、CI 常常失敗。畫面上看到的是 API 連資料庫失敗,重跑一次又可能正常。
原因是 Compose 只保證容器程序啟動,不代表資料庫已經接受連線。API 比 PostgreSQL 更早開始,第一次 migration 就撞牆。
我補了健康檢查和相依條件:
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 2s
timeout: 3s
retries: 20
api:
build: .
depends_on:
db:
condition: service_healthy
這段在做的事很單純:PostgreSQL 回報可接受連線後,API 才啟動。--wait 也要搭配健康檢查才有意義,不是命令列多一個參數就會自動知道服務是否可用。
另一個問題是 teardown 沒有在測試失敗時執行。Makefile 中間一行回傳非零,最後的 down -v 就被跳過,下一輪吃到髒資料。後來我把啟動、測試與清理包進 CI 的 post/finally,確保失敗也會收尾。
Docker 只能縮小環境問題,不能消滅它
我不會把容器當成確定性的保證。映像如果只寫 latest,今天和下週拉到的內容可能不同;基底映像更新、CPU 架構、網路和 Docker Engine 版本也可能造成差異。
要讓測試訊號比較可信,我還會做這些事:
映像釘住明確版本,重要時連 digest 一起固定。
Seed 可以重跑,不能依賴某個人的資料庫備份。
測試不連正式服務,外部 API 用測試替身。
每次測試輸出容器 Log,失敗後保留診斷資料。
本機和 CI 使用同一個 compose 檔及同一個入口。
做到這裡,測試失敗仍可能是環境問題,但範圍已經小很多,而且有設定、Log 和重現步驟可以查。
我願意讓 AI Agent 自己跑「修改、測試、讀結果、再修正」的迴圈,不是因為它突然懂了整個系統,而是因為我把可做的動作縮成幾條明確指令,也把失敗變成比較可靠的訊號。
這些基礎工作不酷,花的時間也比 Demo 多。可是沒有共同的重來按鈕,Agent 跑得越勤快,我反而越不放心。