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

重要

本教程是 开始 教程的扩展。确保您已经完成了该教程,并使用您构建的 estate 模块作为本教程中的练习基础。如果您想从一个干净的基础开始,请从 technical-training-solutions 存储库中获取 16.0-core 分支。

参考: Odoo’s Test Framework: Learn Best Practices (Odoo Experience 2020) on 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 FAQ 中找到。

大部分的测试都会在每次提交到GitHub时在 Runbot <https://runbot.odoo.com> __上运行。

您可以通过在runbot仪表板上进行筛选来查看提交/分支的状态。

每个分支都创建了一个 bundle 。一个bundle由配置和批次组成。

A batch is a set of builds, depending on the parameters of the bundle. A batch is green (i.e. passes the tests) if all the builds are green.

A build is when we launch a server. It can be divided in sub-builds. Usually there are builds for the community version, the enterprise version (only if there is an enterprise branch but you can force the build), and the migration of the branch. A build is green if every sub-build is green.

A sub-build only does some parts of what a full build does. It is used to speed up the CI process. Generally it is used to split the post install tests in 4 parallel instances. A sub-build is green if all the tests are passing and there are no errors/warnings logged.

注解

  • 无论做了哪些修改,所有测试都会运行。纠正错误消息中的拼写错误或重构整个模块都会触发相同的测试。所有模块也将被安装。这意味着即使Runbot是绿色的,也可能会出现问题,即您的更改依赖于模块,而这些模块不依赖于更改所在的模块。

  • Runbot 上没有安装本地化模块(即特定于某个国家/地区的模块),除了通用模块。一些具有外部依赖关系的模块也可能被排除在外。

  • 每晚都会运行额外的测试:模块操作、本地化、单模块安装、多构建用于非确定性错误等。这些测试不会保留在标准 CI 中,以缩短执行时间。

你也可以登录到Runbot构建的版本。有3个可用的用户: admindemoportal 。密码与登录名相同。这对于在不必在本地构建的情况下快速测试不同版本的功能非常有用。完整的日志也可用;这些用于监控。

机器人杜

在你获得召唤robodoo的权利之前,你很可能需要获得更多的经验,但这里还是有一些评论。

Robodoo 是那个在你的 PR 上以标签形式轰炸 CI 状态的家伙,但他也是那个将你的提交友好地集成到主要仓库中的人。

当最后一批测试通过时,评审人员可以要求 robodoo 合并您的 PR(这更像是 rebase 而不是 merge)。然后它将进入合并机器人。

合并机器人

Mergebot is the last testing phase before merging a 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 unittestTesting 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 的注释。