使用單元測試保護您的代碼?

重要

本教程是 開始 教程的擴展。請確保您已經完成了該教程,并使用您構建的 estate 模塊作為本教程中練習的基礎。如果您想從一個干凈的基礎開始,請從 technical-training-solutions 存儲庫中獲取 16.0-core 分支。

參考 : `Odoo 測試框架:學習最佳實踐 <https://www.youtube.com/watch?v=JEIscps0OOQ>`__(Odoo Experience 2020)在 YouTube 上。

編寫測試是必要的,原因有很多。以下是一個非詳盡列表:

  • 確保代碼不會在未來出現問題

  • 定義你的代碼范圍

  • 給出使用案例的例子

  • 這是一種技術上記錄代碼的方式

  • 在開始工作之前,通過定義目標來幫助你的編碼工作

運行測試?

在學習如何編寫測試之前,我們需要知道如何運行它們。

$ odoo-bin -h
Usage: odoo-bin [options]

Options:
--version             show program's version number and exit
-h, --help            show this help message and exit

[...]

Testing Configuration:
  --test-file=TEST_FILE
                      Launch a python test file.
  --test-enable       Enable unit tests.
  --test-tags=TEST_TAGS
                      Comma-separated list of specs to filter which tests to
                      execute. Enable unit tests if set. A filter spec has
                      the format: [-][tag][/module][:class][.method] The '-'
                      specifies if we want to include or exclude tests
                      matching this spec. The tag will match tags added on a
                      class with a @tagged decorator (all Test classes have
                      'standard' and 'at_install' tags until explicitly
                      removed, see the decorator documentation). '*' will
                      match all tags. If tag is omitted on include mode, its
                      value is 'standard'. If tag is omitted on exclude
                      mode, its value is '*'. The module, class, and method
                      will respectively match the module name, test class
                      name and test method name. Example: --test-tags
                      :TestClass.test_func,/test_module,external  Filtering
                      and executing the tests happens twice: right after
                      each module installation/update and at the end of the
                      modules loading. At each stage tests are filtered by
                      --test-tags specs and additionally by dynamic specs
                      'at_install' and 'post_install' correspondingly.
  --screencasts=DIR   Screencasts will go in DIR/{db_name}/screencasts.
  --screenshots=DIR   Screenshots will go in DIR/{db_name}/screenshots.
                      Defaults to /tmp/odoo_tests.

$ # run all the tests of account, and modules installed by account
$ # the dependencies already installed are not tested
$ # this takes some time because you need to install the modules, but at_install
$ # and post_install are respected
$ odoo-bin -i account --test-enable
$ # run all the tests in this file
$ odoo-bin --test-file=addons/account/tests/test_account_move_entry.py
$ # test tags can help you filter quite easily
$ odoo-bin --test-tags=/account:TestAccountMove.test_custom_currency_on_account_1

集成機器人?

注解

本節僅適用于Odoo員工和為 github.com/odoo 做出貢獻的人員。否則,我們強烈建議您擁有自己的CI。

編寫測試時,重要的是確保在對源代碼進行修改時始終通過測試。為了自動化這個任務,我們使用稱為持續集成(CI)的開發實踐。這就是為什么我們有一些機器人在不同的時刻運行所有測試的原因。無論您是在Odoo工作還是不在,如果您正在嘗試合并 odoo/odoo , odoo/enterprise , odoo/upgrade 或在odoo.sh上的任何內容,您都必須通過CI。如果您正在另一個項目上工作,您應該考慮添加自己的CI。

運行機器人?

參考 : 有關此主題的文檔可以在 Runbot FAQ 中找到。

大多數測試都在每次提交到GitHub時在 `Runbot <https://runbot.odoo.com>`__上運行。

您可以通過在runbot儀表板上進行篩選來查看提交/分支的狀態。

每個分支都創建了一個 bundle 。一個bundle由配置和批次組成。

批次 是一組構建,取決于捆綁包的參數。如果所有構建都是綠色的,則批次是綠色的(即通過測試)。

構建 是指我們啟動服務器的過程。它可以分為子構建。通常有社區版本的構建、企業版本的構建(僅在有企業分支時才能強制構建)以及分支的遷移構建。如果每個子構建都是綠色的,則構建是綠色的。

子構建 只執行完整構建的某些部分。它用于加快CI過程。通常用于將安裝后測試分成4個并行實例。如果所有測試都通過且沒有記錄錯誤/警告,則子構建為綠色。

注解

  • 無論做了哪些修改,所有測試都會運行。糾正錯誤消息中的拼寫錯誤或重構整個模塊都會觸發相同的測試。所有模塊也將被安裝。這意味著即使Runbot是綠色的,也可能會出現問題,即您的更改依賴于模塊,而這些模塊不依賴于更改所在的模塊。

  • Runbot 上沒有安裝本地化模塊(即特定于某個國家/地區的模塊),除了通用模塊。一些具有外部依賴關系的模塊也可能被排除在外。

  • 每晚都會運行額外的測試:模塊操作、本地化、單模塊安裝、多構建用于非確定性錯誤等。這些測試不會保留在標準 CI 中,以縮短執行時間。

您也可以登錄到Runbot構建的版本。有3個可用的用戶: admin , demoportal 。密碼與登錄名相同。這對于在不必本地構建的情況下快速測試不同版本的功能非常有用。完整的日志也可用;這些用于監控。

機器人杜?

在你獲得召喚robodoo的權利之前,你很可能需要獲得更多的經驗,但這里還是有一些評論。

Robodoo 是那個在你的 PR 上以標簽形式轟炸 CI 狀態的家伙,但他也是那個將你的提交友好地集成到主要倉庫中的人。

當最后一個批次變為綠色時,評審員可以要求 robodoo 合并您的 PR(這更像是一個 rebase 而不是一個 merge )。然后它將進入合并機器人。

合并機器人?

Mergebot 是合并 PR 前的最后一個測試階段。

它將獲取您分支中尚未在目標上出現的提交,對其進行分段并重新運行測試一次,即使您只是在社區中更改某些內容,也會包括企業版。

此步驟可能會失敗并顯示“暫存失敗”錯誤消息。這可能是由于

  • 如果您是Odoo員工,您可以在此處檢查已經在目標上的非確定性錯誤:https://runbot.odoo.com/runbot/errors

  • 你引入的一個不確定性錯誤,在 CI 中之前沒有被檢測到

  • 在嘗試合并之前,與另一個提交合并的不兼容性

  • 如果您只在社區存儲庫中進行更改,則可能會與企業存儲庫不兼容

在要求合并機器人重試之前,請始終檢查問題是否來自您:將您的分支重新基于目標并在本地重新運行測試。

模塊?

由于Odoo是模塊化的,因此測試也需要是模塊化的。這意味著測試應該在添加功能的模塊中定義,而且測試不能依賴于您的模塊不依賴的模塊提供的功能。

參考 : 有關此主題的文檔可以在 特殊標記 中找到。

from odoo.tests.common import TransactionCase
from odoo.tests import tagged

# The CI will run these tests after all the modules are installed,
# not right after installing the one defining it.
@tagged('post_install', '-at_install')  # add `post_install` and remove `at_install`
class PostInstallTestCase(TransactionCase):
    def test_01(self):
        ...

@tagged('at_install')  # this is the default
class AtInstallTestCase(TransactionCase):
    def test_01(self):
        ...

如果您想要測試的行為可以通過安裝另一個模塊來更改,您需要確保設置了標簽 at_install ;否則,您可以使用標簽 post_install 來加快 CI 的速度,并確保它不會在不應該更改的情況下更改。

編寫測試?

參考 : 有關此主題的文檔可以在 Python unittest測試Odoo 中找到。

在編寫測試之前,請考慮以下幾點

  • 測試應該獨立于當前數據庫中的數據(包括演示數據)

  • 測試不應該通過留下/更改殘留數據來影響數據庫。這通常是測試框架通過回滾來完成的。因此,在測試中(也不在業務代碼的任何其他地方)絕不能調用 cr.commit 。

  • 對于一個 bug 修復,應該在應用修復前失敗,在應用修復后成功。

  • 不要測試已經在其他地方測試過的東西;你可以相信ORM。大多數業務模塊中的測試應該只測試業務流程。

  • 你不需要將數據刷新到數據庫中。

注解

請記住, onchange 僅適用于表單視圖,而不適用于在Python中更改屬性。這也適用于測試。如果您想模擬表單視圖,可以使用 odoo.tests.common.Form 。

測試應該在模塊的根目錄下的 tests 文件夾中。每個測試文件名應該以 test_ 開頭,并在測試文件夾的 __init__.py 中導入。不應該在模塊的 __init__.py 中導入測試文件夾/模塊。

estate
├── models
│   ├── *.py
│   └── __init__.py
├── tests
│   ├── test_*.py
│   └── __init__.py
├── __init__.py
└── __manifest__.py

所有的測試都應該擴展 odoo.tests.common.TransactionCase 。通常你需要定義一個 setUpClass 和測試用例。在編寫 setUpClass 后,你可以在類中使用 env 并開始與 ORM 交互。

這些測試類是基于 unittest Python模塊構建的。

from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
from odoo.tests import tagged

# The CI will run these tests after all the modules are installed,
# not right after installing the one defining it.
@tagged('post_install', '-at_install')
class EstateTestCase(TransactionCase):

    @classmethod
    def setUpClass(cls):
        # add env on cls and many other things
        super(EstateTestCase, cls).setUpClass()

        # create the data for each tests. By doing it in the setUpClass instead
        # of in a setUp or in each test case, we reduce the testing time and
        # the duplication of code.
        cls.properties = cls.env['estate.property'].create([...])

    def test_creation_area(self):
        """Test that the total_area is computed like it should."""
        self.properties.living_area = 20
        self.assertRecordValues(self.properties, [
           {'name': ..., 'total_area': ...},
           {'name': ..., 'total_area': ...},
        ])


    def test_action_sell(self):
        """Test that everything behaves like it should when selling a property."""
        self.properties.action_sold()
        self.assertRecordValues(self.properties, [
           {'name': ..., 'state': ...},
           {'name': ..., 'state': ...},
        ])

        with self.assertRaises(UserError):
            self.properties.forbidden_action_on_sold_property()

注解

為了更好的可讀性,根據測試的范圍將測試拆分成多個文件。您還可以擁有一個公共類,大多數測試都應該從中繼承;這個公共類可以定義整個模塊的設置。例如,在 account。

Exercise

更新代碼,以便沒有人可以:

  • 為已售出的房產創建一個報價

  • 出售一處沒有被接受的報價的房產

并為這兩種情況創建測試。此外,檢查銷售可出售的房產后是否正確標記為已售出。

Exercise

有人在取消勾選花園復選框時,一直破壞花園區域和方向的重置。請確保不再發生此類情況。

小技巧

提示:記得上面關于 Form 的注釋。