使用单元测试保护您的代码

重要

本教程是 服务器框架入门 教程的扩展。确保您已完成该教程,并使用您构建的 estate 模块作为本教程练习的基础。

参考Odoo 的测试框架:学习最佳实践 (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/odooodoo/enterpriseodoo/upgrade 或 odoo.sh 中,都必须通过 CI。如果您正在处理其他项目,则应考虑添加自己的 CI。

Runbot

参考:与此主题相关的文档可以在 Runbot 常见问题解答 中找到。

大多数测试每次在 GitHub 上推送提交时都会在 Runbot 上运行。

您可以通过过滤 Runbot 仪表板查看提交/分支的状态。

每个分支都会创建一个 捆绑包(bundle) 。捆绑包由配置和批次组成。

一个 批次(batch) 是一组构建,具体取决于捆绑包的参数。如果所有构建均为绿色(即通过测试),则批次为绿色。

一个 构建(build) 是指启动服务器。它可以分为子构建。通常会有社区版、企业版(仅当存在企业分支时,但您可以强制构建)以及分支迁移的构建。如果所有子构建均为绿色,则构建为绿色。

一个 子构建(sub-build) 仅完成完整构建的部分内容。它用于加速 CI 过程。通常用于将安装后测试拆分为 4 个并行实例。如果所有测试均通过且未记录错误/警告,则子构建为绿色。

注解

  • 无论进行了哪些修改,所有测试都会运行。更正错误消息中的拼写错误或重构整个模块都会触发相同的测试。所有模块也会被安装。这意味着即使 Runbot 是绿色的,某些内容可能仍然无法正常工作,例如,您的更改依赖于某个模块,而包含更改的模块并未依赖该模块。

  • 本地化模块(即特定国家的模块)不会在 Runbot 上安装(通用模块除外)。某些具有外部依赖项的模块也可能被排除在外。

  • 有一个夜间构建会运行额外的测试:模块操作、本地化、单模块安装、针对非确定性错误的多构建等。这些测试未保留在标准 CI 中,以缩短执行时间。

您还可以登录由 Runbot 构建的构建。有 3 个可用用户:admindemoportal。密码与登录名相同。这有助于快速测试不同版本的内容,而无需在本地构建。完整的日志也可用;这些日志用于监控。

Robodoo

您很可能需要积累更多经验才能获得召唤 Robodoo 的权限,但这里有一些备注。

Robodoo 是在您的 PR 上标记 CI 状态的人,但他也是友善地将您的提交集成到主存储库中的人。

当最后一个批次变为绿色时,评审员可以要求 Robodoo 合并您的 PR(更像是 rebase 而不是 merge)。然后它将进入合并机器人。

合并机器人

Mergebot 是合并 PR 之前的最后一个测试阶段。

它会提取目标分支中尚未存在的提交,进行暂存并重新运行测试,即使您仅在社区版中进行了更改,也会包括企业版的测试。

此步骤可能会因 Staging failed 错误消息而失败。原因可能是:

  • 目标分支上已存在的非确定性错误。如果您是 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 (也不应在业务代码的任何其他地方调用)。

  • 对于错误修复,测试应在应用修复之前失败,并在之后通过。

  • 不要测试已在其他地方测试过的内容;您可以信任 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 进行交互。

这些测试类基于 Python 的 unittest 模块构建。

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 的内容。