測試與除錯
模組寫到一定規模,靠手動點畫面驗證很快就會漏東漏西:改了 A 功能,B 悄悄壞掉。本頁教兩件事:用自動化測試守住商業規則,以及出錯時怎麼快速定位。
自動化測試
檔案位置
測試放在模組的 tests/ 目錄,檔名以 test_ 開頭,並在 tests/__init__.py 匯入(沒匯入就不會被執行):
寫測試
from odoo.exceptions import UserError, ValidationError
from odoo.tests.common import TransactionCase, tagged
@tagged("post_install", "-at_install")
class TestLoan(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.book = cls.env["library.book"].create({"name": "測試用書", "isbn": "TEST-001"})
cls.partner = cls.env["res.partner"].create({"name": "測試讀者"})
def _new_loan(self):
return self.env["library.loan"].create({
"book_id": self.book.id,
"borrower_id": self.partner.id,
})
def test_borrow_and_return(self):
loan = self._new_loan()
self.assertEqual(loan.state, "draft")
self.assertTrue(loan.name.startswith("LN/"))
loan.action_borrow()
self.assertFalse(self.book.is_available)
loan.action_return()
self.assertTrue(self.book.is_available)
self.assertTrue(loan.date_returned)
def test_due_date(self):
loan = self._new_loan()
self.assertEqual((loan.date_due - loan.date_borrowed).days, 14)
def test_cannot_lend_same_book_twice(self):
self._new_loan().action_borrow()
with self.assertRaises(ValidationError):
self._new_loan().action_borrow()
def test_loan_days_must_be_positive(self):
with self.assertRaises(ValidationError):
self.env["library.loan"].create({
"book_id": self.book.id,
"borrower_id": self.partner.id,
"loan_days": 0,
})
def test_partner_loan_count_and_unlink(self):
self.assertEqual(self.partner.loan_count, 0)
loan = self._new_loan()
loan.action_borrow()
self.assertEqual(self.partner.loan_count, 1)
with self.assertRaises(UserError):
self.partner.unlink()
重點:
| 寫法 | 說明 |
|---|---|
TransactionCase |
每個測試方法各在一個交易中執行,結束後自動回滾,測試不會弄髒資料庫,方法之間互不影響 |
setUpClass |
整個類別只準備一次的資料(書、讀者)。self.env 在測試中直接可用 |
test_ 開頭 |
只有這樣命名的方法才會被當作測試 |
assertRaises |
驗證「應該出錯」的情況。商業規則的邊界條件(不能重複借、借期為 0)最值得測 |
@tagged("post_install", "-at_install") |
見下表 |
@tagged 決定測試何時執行:
| 標籤 | 時機 |
|---|---|
at_install(預設) |
模組剛安裝完就跑,這時之後才安裝的其他模組還不存在 |
post_install |
所有模組都安裝完才跑,適合依賴其他模組行為的測試 |
-at_install 的減號表示「移除」預設標籤。我們的測試用到聯絡人(base),且擴充了它,用 post_install 較穩。
執行測試
--test-tags /library 限定只跑 library 模組的測試,格式與更多寫法見 odoo-bin 指令。實際輸出:
odoo.service.server: Starting post tests
odoo.addons.library.tests.test_loan: Starting TestLoan.test_borrow_and_return ...
odoo.addons.library.tests.test_loan: Starting TestLoan.test_cannot_lend_same_book_twice ...
odoo.addons.library.tests.test_loan: Starting TestLoan.test_due_date ...
odoo.addons.library.tests.test_loan: Starting TestLoan.test_loan_days_must_be_positive ...
odoo.addons.library.tests.test_loan: Starting TestLoan.test_partner_loan_count_and_unlink ...
odoo.tests.result: 0 failed, 0 error(s) of 5 tests when loading database 'mydb'
看最後一行:0 failed, 0 error(s) 才是通過。失敗時會有 FAIL: 與斷言差異:
ERROR odoo.addons.library.tests.test_loan: FAIL: TestLoan.test_partner_loan_count_and_unlink
self.assertEqual(self.partner.loan_count, 1)
AssertionError: 0 != 1
不要在全新資料庫加 --test-enable
-i library --test-enable 在全新資料庫會連 base、web 等所有一起安裝的模組的測試都跑,實測要近兩分鐘。請用 --test-tags /library。
單獨跑一個測試更快:
測試該測什麼
測「商業規則」與「邊界條件」:不能重複借書、借期必須大於 0、刪除有未還書的聯絡人被擋下。不必測 Odoo 本身的功能(例如 create 能不能建立記錄)。
用 shell 除錯
懷疑某段 ORM 寫法有問題時,最快的方式是開 shell 直接試:
>>> loan = env["library.loan"].search([("state", "=", "borrowed")], limit=1)
>>> loan.date_due
datetime.date(2026, 10, 3)
>>> loan.book_id.is_available
False
>>> env["library.loan"].search([("date_due", "<", "2026-01-01")])
library.loan()
shell 預設離開時回滾,可以放心亂試。要保留變更才呼叫 env.cr.commit()。
日誌
在程式裡用標準的 logging 模組寫日誌,比 print 好:有等級、有時間、有模組來源,還能用選項開關。
import logging
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class LibraryLoan(models.Model):
...
def action_return(self):
_logger.info("歸還借閱單 %s", self.mapped("name"))
self.write({
"state": "returned",
"date_returned": fields.Date.context_today(self),
})
__name__ 會是 odoo.addons.library.models.library_loan,所以可以用 odoo.addons.library 這個前綴控制整個模組的日誌等級。訊息用 %s 加參數,不要用 f-string 先拼好,這樣沒印出來時不會白費運算。
啟動時把自己的模組調到 DEBUG,其他模組維持 INFO:
要看 ORM 實際下了哪些 SQL(找 N+1 查詢很有用):
用法與更多選項見 odoo-bin 指令:日誌。
要中斷偵錯,在程式裡寫 breakpoint(),然後在前景執行伺服器(不要開 --workers),終端機就會進入 pdb 互動介面。
真實錯誤速查
下面的訊息都是實際觸發後複製下來的。
| 日誌訊息 | 原因與處理 |
|---|---|
ParseError: while parsing .../library_book_views.xml:2,接著 Field "isbnn" does not exist in model "library.book" |
檢視引用的欄位不存在,或模型檔沒被匯入。訊息會指出哪個檔案、哪個欄位,照著改 |
The models ['library.book', 'library.loan'] have no access rules in module library |
沒寫 ir.model.access.csv,或沒列進 manifest 的 data |
Non-stored field res.partner.loan_count cannot be searched. |
對非儲存的計算欄位搜尋。改 store=True 或提供 search= 方法 |
invalid module names, ignored: library |
模組名稱打錯,或 --addons-path 沒指到模組上一層目錄 |
AssertionError: 0 != 1(計算欄位) |
漏寫或少寫 @api.depends,值被快取 |
ValidationError/UserError 對話框 |
這是你自己寫的驗證。ValidationError 用於資料不合法,UserError 用於一般的操作被擋 |
| 改了 XML 沒反應 | 沒 -u,或瀏覽器快取。用 --dev=xml,並硬性重新整理(Ctrl/Cmd + Shift + R) |
-u 完成但新欄位不見 |
檢視沒列進 data,或忘了 from . import |
除錯順序建議
- 先看日誌最後的
ERROR/CRITICAL,往上找第一個Traceback,最下面一行通常就是原因。 - 看是哪個階段出錯:
loading library/views/...出錯是檔案內容問題,creating or updating database tables出錯是欄位定義問題。 - 縮小範圍:在 shell 裡重現,或寫成一個最小的測試。
- 一次只改一件事,改完立刻
-u驗證。
開發檢查清單
- 模型有
_description,欄位有string - 每個計算欄位都有
@api.depends - 每個模型都有
ir.model.access.csv的列 - manifest 的
data順序:權限 → 資料 → 檢視 → 選單 - 覆寫方法都有
super() -
_("...")包住要給使用者看的訊息 - 至少為商業規則寫了測試,並用
--test-tags /模組名跑過 - 用
--without-demo=all的資料庫也能正常安裝