跳轉至

建立 2026-09-20 更新 2026-09-20

測試與除錯

模組寫到一定規模,靠手動點畫面驗證很快就會漏東漏西:改了 A 功能,B 悄悄壞掉。本頁教兩件事:用自動化測試守住商業規則,以及出錯時怎麼快速定位。

範例延續 實作:圖書借閱模組繼承與擴充

自動化測試

檔案位置

測試放在模組的 tests/ 目錄,檔名以 test_ 開頭,並在 tests/__init__.py 匯入(沒匯入就不會被執行):

library/
└─ tests/
   ├─ __init__.py
   └─ test_loan.py
tests/__init__.py
from . import test_loan

寫測試

tests/test_loan.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 較穩。

執行測試

odoo-bin -d mydb -u library --test-tags /library --stop-after-init

--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 在全新資料庫會連 baseweb 等所有一起安裝的模組的測試都跑,實測要近兩分鐘。請用 --test-tags /library

單獨跑一個測試更快:

odoo-bin -d mydb -u library --test-tags /library:TestLoan.test_due_date --stop-after-init

測試該測什麼

測「商業規則」與「邊界條件」:不能重複借書、借期必須大於 0、刪除有未還書的聯絡人被擋下。不必測 Odoo 本身的功能(例如 create 能不能建立記錄)。

用 shell 除錯

懷疑某段 ORM 寫法有問題時,最快的方式是開 shell 直接試:

odoo-bin shell -d mydb
>>> 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 好:有等級、有時間、有模組來源,還能用選項開關。

models/library_loan.py(節錄)
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

odoo-bin -d mydb --log-handler=odoo.addons.library:DEBUG

要看 ORM 實際下了哪些 SQL(找 N+1 查詢很有用):

odoo-bin -d mydb --log-sql

用法與更多選項見 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,值被快取
ValidationErrorUserError 對話框 這是你自己寫的驗證。ValidationError 用於資料不合法,UserError 用於一般的操作被擋
改了 XML 沒反應 -u,或瀏覽器快取。用 --dev=xml,並硬性重新整理(Ctrl/Cmd + Shift + R)
-u 完成但新欄位不見 檢視沒列進 data,或忘了 from . import

除錯順序建議

  1. 先看日誌最後的 ERRORCRITICAL,往上找第一個 Traceback,最下面一行通常就是原因。
  2. 看是哪個階段出錯loading library/views/... 出錯是檔案內容問題,creating or updating database tables 出錯是欄位定義問題。
  3. 縮小範圍:在 shell 裡重現,或寫成一個最小的測試。
  4. 一次只改一件事,改完立刻 -u 驗證。

開發檢查清單

  • 模型有 _description,欄位有 string
  • 每個計算欄位都有 @api.depends
  • 每個模型都有 ir.model.access.csv 的列
  • manifest 的 data 順序:權限 → 資料 → 檢視 → 選單
  • 覆寫方法都有 super()
  • _("...") 包住要給使用者看的訊息
  • 至少為商業規則寫了測試,並用 --test-tags /模組名 跑過
  • --without-demo=all 的資料庫也能正常安裝

進一步學習