通过单元测试保护您的代码¶
重要
本教程是 服务器框架101 教程的扩展。确保您已完成它并使用您构建的 estate 模块作为本教程中练习的基础。
参考:YouTube 上的 `Odoo’s Test Framework: Learn Best Practices <https://www.youtube.com/watch?v=JEIscps0OOQ>`__(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 '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 上运行。
您可以通过在 runbot 仪表板上进行过滤来查看提交/分支的状态。
为每个分支创建一个 bundle。捆绑包由配置和批次组成。
**批次**是一组构建,具体取决于捆绑包的参数。如果所有构建都是绿色的,则批次是绿色的(即通过测试)。
**构建**是我们启动服务器的时候。它可以分为子构建。通常有社区版本的构建、企业版本(仅当有企业分支但可以强制构建)以及分支的迁移。如果每个子构建都是绿色的,则构建是绿色的。
**子构建**仅执行完整构建的某些部分。它用于加速 CI 过程。通常,它用于将安装后测试分为 4 个并行实例。如果所有测试都通过并且没有记录错误/警告,则子构建为绿色。
注解
无论所做的修改如何,所有测试都会运行。纠正错误消息中的拼写错误或重构整个模块会触发相同的测试。所有模块也将被安装。这意味着即使 Runbot 是绿色的,某些东西也可能无法工作,即您的更改依赖于更改所在模块不依赖的模块。
Runbot 上未安装本地化模块(即特定于国家/地区的模块)(通用模块除外)。一些具有外部依赖关系的模块也可以被排除。
每晚构建都会运行额外的测试:模块操作、本地化、单模块安装、针对不确定性错误的多重构建等。这些不会保留在标准 CI 中以缩短执行时间。
您还可以登录 Runbot 构建的版本。有 3 个可用用户:admin、demo 和 portal。密码与登录时相同。这对于在不同版本上快速测试事物很有用,而无需在本地构建。完整的日志也可用;这些用于监控。
罗布都¶
在获得召唤机器人的权利之前,你很可能需要获得更多的经验,但无论如何,这里有一些评论。
Robodoo 是那个在你的 PR 上发送 CI 状态作为标签的人,但他也是那个善意地将你的提交集成到主存储库中的人。
当最后一批是绿色时,审阅者可以要求 robodoo 合并你的 PR(它更像是 rebase 而不是 merge)。然后它将转到合并机器人。
合并机器人¶
Mergebot 是合并 PR 之前的最后一个测试阶段。
它将获取分支中尚未出现在目标上的提交,将其暂存并再次重新运行测试,包括企业版本,即使您只是在社区中更改某些内容。
此步骤可能会失败并显示 Staging failed 错误消息。这可能是由于
目标上已经存在的不确定性错误。如果您是 Odoo 员工,可以在此处查看:https://runbot.odoo.com/runbot/errors
您引入但之前在 CI 中未检测到的不确定性错误
与之前合并的另一个提交以及您尝试合并的内容不兼容
如果您仅在社区存储库中进行更改,则与企业存储库不兼容
在要求合并机器人重试之前,请务必检查问题是否来自您:在目标上重新建立分支并在本地重新运行测试。
模块¶
由于 Odoo 是模块化的,因此测试也需要模块化。这意味着测试是在添加您要添加的功能的模块中定义的,并且测试不能依赖于来自您的模块不依赖的模块的功能。
参考:与此主题相关的文档可以在 Special Tags 中找到。
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 和 Testing Odoo 中找到。
在编写测试之前需要考虑以下几点
测试应该独立于数据库中当前的数据(包括演示数据)
测试不应通过保留/更改残留数据来影响数据库。这通常由测试框架通过回滚来完成。因此,您绝不能在测试中(或业务代码中的其他任何地方)调用“
cr.commit”。对于错误修复,测试应该在应用修复之前失败并在应用修复之后通过。
不要测试已经在其他地方测试过的东西;你可以信任 ORM。业务模块中的大多数测试应该只测试业务流程。
您不需要将数据刷新到数据库中。
注解
请记住“onchange` only applies in the Form views, not by changing the attributes in python. This also applies in the tests. If you want to emulate a Form view, you can use ``odoo.tests.Form`”。
测试应位于模块的“tests` folder at the root of your module. Each test file name should start with test_ and be imported in the __init__.py of the test folder. You shouldn’t import the test folder/module in the ``__init__.py`”中。
estate
├── models
│ ├── *.py
│ └── __init__.py
├── tests
│ ├── test_*.py
│ └── __init__.py
├── __init__.py
└── __manifest__.py
所有测试都应该扩展类中可用的``odoo.tests.common.TransactionCase``. You usually define a setUpClass and the tests. After writing the setUpClass, you have an 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()
注解
为了获得更好的可读性,请根据测试的范围将测试拆分为多个文件。您还可以拥有一个大多数测试都应继承的 Common 类;这个公共类可以定义模块的整个设置。例如,在 account 中。
Exercise
更新代码,这样就没有人可以:
为已售房产创建出价
出售未接受报价的房产
并为这两种情况创建测试。另外,检查出售可出售的财产是否在出售后正确标记为已出售。
Exercise
当您取消选中“花园”复选框时,有人不断破坏“花园面积”和“方向”的重置。确保不再发生这种情况。
小技巧
提示:记住上面关于 Form 的注释。