用单元测试守护代码¶
重要
本教程是 服务器框架入门 教程的延伸。请确保你已完成该教程,并以你构建的 estate 模块作为本教程练习的基础。
参考:YouTube 上的 Odoo 的测试框架:学习最佳实践 (Odoo Experience 2020)。
出于多种原因,编写测试是必要的。以下是一份非详尽的清单:
确保代码将来不会被破坏
界定代码的范围
给出用例示例
它是从技术角度记录代码的一种方式
在朝着目标努力之前先定义目标,有助于你的编码
运行测试¶
在了解如何编写测试之前,我们需要先知道如何运行它们。
$ 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 'post_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¶
参考:与本主题相关的文档可在 Runbot FAQ 中找到。
每次在 GitHub 上推送提交时,大多数测试都会在 Runbot 上运行。
你可以通过在 runbot 仪表板上筛选来查看提交/分支的状态。
每个分支都会创建一个 bundle。一个 bundle 由配置和批次组成。
批次 是一组构建,具体取决于 bundle 的参数。如果所有构建都是绿色(即通过测试),则该批次为绿色。
构建 指我们启动一次服务器。一次构建可以划分为多个子构建。通常会有社区版构建、企业版构建(仅当存在企业版分支时才会构建,但你可以强制构建)以及分支迁移的构建。如果每个子构建都是绿色,则该构建为绿色。
子构建 只执行完整构建的部分工作。它用于加速 CI 流程。通常用于将安装后测试拆分为 4 个并行实例。如果所有测试都通过且没有记录错误/警告,则子构建为绿色。
注解
无论做了哪些修改,所有测试都会运行。修正错误消息中的拼写错误或重构整个模块会触发相同的测试。所有模块也会被安装。这意味着即使 Runbot 显示为绿色,某些功能仍可能无法正常工作,也就是你的改动依赖于某个模块,而你所改动模块并不依赖该模块。
本地化模块(即特定国家/地区的模块)不会安装在 Runbot 上(通用模块除外)。某些带有外部依赖的模块也可能被排除。
有一个夜间构建会运行额外的测试:模块操作、本地化、单模块安装、针对非确定性缺陷的多次构建等。为了缩短执行时间,这些测试并未纳入标准 CI。
你还可以登录到由 Runbot 构建的构建。可用的用户有 3 个:admin、demo 和 portal。密码与登录名相同。这对于在不同版本上快速测试而不必在本地构建非常有用。完整的日志也可获取;这些日志用于监控。
Robodoo¶
你很可能需要再积累一点经验,才有权限召唤 robodoo,不过这里还是给出一些说明。
Robodoo 会在你的 PR 上不断以标签形式推送 CI 状态,但他也会好心地把你的提交集成到主仓库中。
当最后一个批次为绿色时,审核者可以要求 robodoo 合并你的 PR(这更像是一次 rebase 而不是 merge)。之后它会交给 mergebot 处理。
Mergebot¶
Mergebot 是合并 PR 之前的最后一个测试阶段。
它会取出你分支中目标分支尚不存在的提交,暂存这些提交并再次运行测试,即使你只在社区版中做了改动,也会包含企业版。
此步骤可能会失败,并显示 Staging failed 错误消息。可能的原因有
目标分支上已存在的非确定性缺陷。如果你是 Odoo 员工,可以在此查看:https://runbot.odoo.com/runbot/errors
由你引入但此前未被 CI 检测到的非确定性缺陷
你正要合并的内容与刚刚合并的另一个提交之间存在不兼容
如果你只在社区版仓库中做了改动,则与企业版仓库存在不兼容
在要求合并机器人重试之前,务必确认问题并非由你引起:将你的分支变基到目标分支上,并在本地重新运行测试。
模块¶
由于 Odoo 是模块化的,测试也必须模块化。这意味着测试应定义在添加你所新增功能的那个模块中,并且测试不能依赖于你的模块未依赖的模块所提供的功能。
Reference:与本主题相关的文档可在 特殊标签 中找到。
from odoo.tests import tagged, TransactionCase
# The CI will run these tests after all the modules are installed,
# not right after installing the one defining it.
@tagged('post_install') # this is the default
class PostInstallTestCase(TransactionCase):
def test_01(self):
...
@tagged('at_install', '-post_install') # add `at_install` and remove `post_install`
class AtInstallTestCase(TransactionCase):
def test_01(self):
...
如果你要测试的行为可能因安装另一个模块而改变,则需要确保设置了 at_install 标签;否则,你可以使用 post_install 标签来加速 CI,并确保在不该改变时它不会改变。
编写测试¶
Reference:与本主题相关的文档可在 Python unittest 和 测试 Odoo 中找到。
编写测试之前,有几件事需要考虑
测试应独立于数据库中当前的数据(包括演示数据)
测试不应通过留下或修改残留数据来影响数据库。这通常由测试框架通过回滚来完成。因此,绝不能在测试中调用
cr.commit(在业务代码的其他任何地方也不行)。对于缺陷修复,测试应当在应用修复之前失败,在应用修复之后通过。
不要测试已在其他地方测试过的内容;你可以信任 ORM。业务模块中的大多数测试应只测试业务流程。
你不应该需要将数据刷新到数据库中。
注解
请记住,onchange 仅适用于表单视图,而不是在 Python 中修改属性。这在测试中同样适用。如果你想模拟表单视图,可以使用 odoo.tests.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 import TransactionCase
from odoo.exceptions import UserError
# The CI will run these tests after all the modules are installed,
# not right after installing the one defining it.
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()
注解
为提高可读性,请根据测试范围将测试拆分到多个文件中。你还可以定义一个 Common 类,让大多数测试继承它;这个公共类可以为模块定义完整的初始化设置。例如,参见 account。
练习
修改代码,使任何人都无法:
为已售出的房产创建报价
出售没有已接受报价的房产
并为这两种情况创建测试。此外还要检查:出售一个可以出售的房产后,该房产是否被正确标记为已售出。
练习
有人总是在取消勾选“花园”复选框时破坏“花园面积”和“朝向”的重置。请确保这种情况不再发生。
小技巧
提示:请回想一下上文关于 Form 的注释。