测试 Odoo¶
测试应用的方式有很多种。在 Odoo 中,我们有三类测试
Python 单元测试(参见 测试 Python 代码):用于测试模型的业务逻辑
JS 单元测试(参见 测试 JS 代码):用于单独测试 JavaScript 代码
漫游(参见 集成测试):漫游模拟真实场景,可确保 Python 部分与 JavaScript 部分能够正确通信。
测试 Python 代码¶
Odoo 支持使用 Python 的 unittest 库 来测试模块。
要编写测试,只需在模块中定义一个 tests 子包,系统会自动在其中查找测试模块。测试模块的名称应以 test_ 开头,并应从 tests/__init__.py 导入,例如:
your_module
├── ...
├── tests
| ├── __init__.py
| ├── test_bar.py
| └── test_foo.py
而 __init__.py 的内容如下:
from . import test_foo, test_bar
警告
未从 tests/__init__.py 导入的测试模块不会被执行
测试运行器会直接运行任何测试用例,如官方 unittest documentation 中所述,不过 Odoo 还提供了许多与测试 Odoo 内容(主要是模块)相关的工具和辅助函数:
最常见的情况是使用 TransactionCase,并在每个方法中测试模型的某个属性,例如:
class TestModelA(TransactionCase):
def test_some_action(self):
record = self.env['model.a'].create({'field': 'value'})
record.some_action()
self.assertEqual(
record.field,
expected_field_value)
# other tests...
注解
测试方法必须以 test_ 开头
运行测试¶
如果在启动 Odoo 服务器时启用了 --test-enable,那么在安装或更新模块时会自动运行测试。
测试选择¶
在 Odoo 中,可以为 Python 测试打标签,以便在运行测试时方便地选择测试。
odoo.tests.BaseCase 的子类(通常通过 TransactionCase 或 HttpCase)默认会自动打上 standard 和 at_install 标签。
调用方式¶
可以使用 --test-tags 在命令行上选择/过滤要运行的测试。它会隐含启用 --test-enable,因此在使用 --test-tags 时无需再指定 --test-enable。
该选项默认为 +standard,表示使用 --test-enable 启动 Odoo 时,默认会运行(显式或隐式)标记为 standard 的测试。
编写测试时,可以在 测试类 上使用 tagged() 装饰器来添加或移除标签。
装饰器的参数是标签名称,以字符串形式给出。
危险
tagged() 是类装饰器,对函数或方法没有作用
标签可以加上减号(-)前缀,以便 移除 标签,而不是添加或选择标签。例如,如果不希望测试默认被执行,可以移除 standard 标签:
from odoo.tests import TransactionCase, tagged
@tagged('-standard', 'nice')
class NiceTest(TransactionCase):
...
该测试默认不会被选中,要运行它必须显式选择相应的标签:
$ odoo-bin --test-tags nice
请注意,只会执行标记为 nice 的测试。要 同时 运行 nice 和 standard 测试,请为 --test-tags 提供多个值:在命令行上,这些值是 累加 的(即选中带有 任一 指定标签的所有测试)
$ odoo-bin --test-tags nice,standard
配置开关参数也接受 + 和 - 前缀。+ 前缀是隐含的,因此完全可以省略。- (减号)前缀用于取消选择带有该前缀标签的测试,即使这些测试已被其他指定标签选中。例如,如果有同时标记为 slow 的 standard 测试,你可以运行所有标准测试,但 排除 运行缓慢的测试:
$ odoo-bin --test-tags 'standard,-slow'
当你编写的测试没有继承 BaseCase 时,该测试不会有默认标签,你必须显式添加这些标签,才能让测试包含在默认测试套件中。使用简单的 unittest.TestCase 时这是一个常见问题,因为这类测试不会被执行:
import unittest
from odoo.tests import tagged
@tagged('standard', 'post_install')
class SmallTest(unittest.TestCase):
...
除了标签之外,你还可以指定要测试的特定模块、类或函数。--test-tags 所接受格式的完整语法为:
[-][tag][/module][:class][.method]
因此,如果你想测试 stock_account 模块,可以使用:
$ odoo-bin --test-tags /stock_account
如果你想测试具有唯一名称的特定函数,可以直接指定:
$ odoo-bin --test-tags .test_supplier_invoice_forwarded_by_internal_user_without_supplier
这等价于
$ odoo-bin --test-tags /account:TestAccountIncomingSupplierInvoice.test_supplier_invoice_forwarded_by_internal_user_without_supplier
前提是测试名称没有歧义。可以像使用普通标签那样,用 , 分隔,一次指定多个模块、类和函数。
示例¶
仅运行 sale 模块中的测试:
$ odoo-bin --test-tags /sale
运行 sale 模块中的测试,但不运行标记为 slow 的测试:
$ odoo-bin --test-tags '/sale,-slow'
仅运行 stock 模块中的测试或标记为 slow 的测试:
$ odoo-bin --test-tags '-standard, slow, /stock'
注解
-standard 是隐含的(并非必需),此处写出来是为了清晰
测试 JS 代码¶
测试复杂系统是防止回归、确保某些基本功能仍然可用的一项重要保障。由于 Odoo 拥有相当复杂的 JavaScript 代码库,因此有必要对其进行测试。
请参阅 单元测试 了解前端测试框架的各个方面,或直接跳转到下面的某个小节:
集成测试¶
分别测试 Python 代码和 JS 代码非常有用,但并不能证明 Web 客户端与服务器能够协同工作。为此,我们可以编写另一种测试:漫游。漫游是某个有趣业务流程的微型场景,它说明了应遵循的一系列步骤。随后测试运行器会创建一个 PhantomJs 浏览器,将其指向正确的 url,并根据该场景模拟点击和输入。
编写测试漫游¶
结构¶
要为 your_module 编写测试漫游,首先要创建所需的文件:
your_module
├── ...
├── static
| └── tests
| └── tours
| └── your_tour.js
├── tests
| ├── __init__.py
| └── test_calling_the_tour.py
└── __manifest__.py
然后你可以:
更新
__manifest__.py,将your_tour.js添加到资源中。'assets': { 'web.assets_tests': [ 'your_module/static/tests/tours/your_tour.js', ], },
更新
tests文件夹中的__init__.py,以导入test_calling_the_tour。
Javascript¶
通过注册来设置你的漫游。
import tour from 'web_tour.tour'; tour.register('rental_product_configurator_tour', { url: '/web', // Here, you can specify any other starting url }, [ // Your sequence of steps ]);
添加你需要的任意步骤。
每个步骤至少包含一个触发器。你可以使用 预定义步骤,也可以编写自己的个性化步骤。
以下是一些步骤示例:
示例
// First step
tour.stepUtils.showAppsMenuItem(),
// Second step
{
trigger: '.o_app[data-menu-xmlid="your_module.maybe_your_module_menu_root"]',
isActive: ['community'], // Optional
run: "click",
}, {
// Third step
},
示例
{
trigger: '.js_product:has(strong:contains(Chair floor protection)) .js_add',
run: "click",
},
示例
{
isActive: ["mobile", "enterprise"],
content: "Click on Add a product link",
trigger: 'a:contains("Add a product")',
tooltipPosition: "bottom",
async run(helpers) { //Exactly the same as run: "click"
helpers.click();
}
},
以下是你的个性化步骤可以使用的参数:
trigger:必填,要在其上
run动作的选择器/元素。漫游会等待该元素存在且可见,然后才对 该元素 执行run动作。run:可选,要在 trigger 元素上执行的动作。如果没有
run,则不执行任何动作。动作可以是:
一个异步函数,以触发器的
Tip作为上下文(this),并以动作辅助函数作为参数执行。某个动作辅助函数的名称,该辅助函数将在触发器元素上运行:
check确保 trigger 元素处于勾选状态。该辅助函数仅适用于
<input[type=checkbox]>元素。clear清除 trigger 元素的值。该辅助函数仅适用于
<input>或<textarea>元素。click点击 trigger 元素,并触发所有相关的中间事件。
dblclick,与
click相同,但重复两次。drag_and_drop target模拟将 trigger 元素拖动到
target上。edit content先
clear该元素,然后fill填入content。editor content聚焦 trigger 元素(wysiwyg),然后
press按下content。fill content聚焦 trigger 元素,然后
press按下content。该辅助函数仅适用于<input>或<textarea>元素。hover在 trigger 元素上执行悬停操作序列。
press content执行键盘事件序列。
range content聚焦 trigger 元素,并将
content设为其值。该辅助函数仅适用于<input[type=range]>元素。select value在 trigger 元素上执行选择事件序列。按
value选择选项。该辅助函数仅适用于<select>元素。selectByIndex index与
select相同,但按index选择选项。请注意,第一个选项的索引为 0。selectByLabel label与
select相同,但按label选择选项。uncheck确保 trigger 元素处于未勾选状态。该辅助函数仅适用于
<input[type=checkbox]>元素。
isActive:可选,仅当满足 isActive 数组中的所有条件时才启用该步骤。- 浏览器处于 desktop 或 mobile 模式。- 漫游涉及 community 或 enterprise 版本。- 漫游以 auto (runbot)或 manual (引导)模式运行。
tooltipPosition:可选,
"top"、"right"、"bottom"或"left"。运行交互式漫游时,提示框相对于 target 的定位位置。content:可选但建议填写,表示交互式漫游中提示框的内容;它也会输出到控制台,因此对跟踪和调试自动化漫游非常有用。
timeout:等待该步骤可以
run的时间,单位为毫秒,10000(10 秒)。
重要
漫游的最后一个(些)步骤应始终让客户端回到“稳定”状态(例如没有正在进行的编辑),并确保所有副作用(网络请求)都已执行完毕,以避免竞态条件或拆卸过程中的错误。
更多内容
Python¶
要从 Python 测试中启动漫游,请让该类继承 HTTPCase,并调用 start_tour:
def test_your_test(self):
# Optional Setup
self.start_tour("/web", "your_tour_name", login="admin")
# Optional verifications
编写入门导览¶
结构¶
要为 your_module 编写入门导览,请先创建所需的文件:
your_module
├── ...
├── data
| └── your_tour.xml
├── static/src/js/tours/your_tour.js
└── __manifest__.py
然后可以更新 __manifest__.py,在 assets 中添加 your_tour.js,在 data 中添加 your_tour.xml。
'data': [ 'data/your_tour.xml', ], 'assets': { 'web.assets_backend': [ 'your_module/static/src/js/tours/your_tour.js', ], },
Javascript¶
JavaScript 部分与 :ref: 测试导览 <testing/javascript/test> 相同。
XML¶
当导览已注册到 JavaScript 注册表后,即可在 XML 中创建 web_tour.tour 记录,如下所示:
<?xml version="1.0" encoding="utf-8"?> <odoo> <record id="your_tour" model="web_tour.tour"> <field name="name">your_tour</field> <field name="sequence">10</field> <field name="rainbow_man_message">Congrats, that was a great tour</field> </record> </odoo>
name:必需,名称必须与 JavaScript 注册表中的名称一致。sequence:可选;决定入门导览的执行顺序。默认为 1000。url:可选;启动导览的 URL。如果url为False,则从注册表获取 URL。默认为 “/odoo”。rainbow_man_message:可选;将在导览完成时通过彩虹人效果显示该消息。如果rainbow_man_message为False,则不会显示彩虹效果。默认为<b>Good job!</b> You went through all steps of this tour.
运行入门导览¶
在用户菜单中切换 入门 选项,即可按顺序依次启动所有入门导览。也可以进入 ,然后点击 入门 或 测试 来运行特定的入门导览。
入门:将以交互模式执行导览。也就是说,导览会展示应执行的操作,并等待用户进行交互。
测试:将自动执行导览。也就是说,导览会在用户面前执行所有步骤。
导览录制器¶
也可以使用导览录制器轻松创建导览。为此,请在入门导览视图中点击 录制。启动后,该工具会记录你在 Odoo 中的所有交互。
创建的导览会在入门导览视图中标记为 自定义。这些导览还可以导出为 JavaScript 文件,随时可以放入你的模块中。
调试技巧¶
在浏览器中观察测试导览¶
有三种方式,各有利弊:
watch=True¶
在本地通过测试套件运行导览时,可以在 browser_js 或 start_tour 调用中添加 watch=True 参数:
self.start_tour("/web", "your_tour_name", watch=True)
这会自动打开一个 Chrome 窗口,并在其中运行该导览。
- 优点
只要导览包含 Python 初始化代码 / 周边代码,或包含多个步骤,就总能正常使用
完全自动运行(只需选择启动该导览的测试)
事务性(应该 总能多次运行)
- 缺点
只能在本地运行
仅当测试 / 导览能在本地正常运行时才可用
debug=True¶
在本地通过测试套件运行导览时,可以在 browser_js 或 start_tour 调用中添加 debug=True 参数:
self.start_tour("/web", "your_tour_name", debug=True)
这会自动打开一个全屏 Chrome 窗口,同时打开开发者工具,并在导览开始时设置一个调试器断点。导览会以 debug=assets 查询参数运行。当抛出错误时,调试器会在异常处暂停。
- 优点
与
watch=True模式具有相同的优点更容易调试各个步骤
- 缺点
只能在本地运行
仅当测试 / 导览能在本地正常运行时才可用
通过浏览器运行¶
测试导览也可以通过浏览器 UI 启动,只需调用
odoo.startTour("tour_name");
在 JavaScript 控制台中调用,或者通过在 URL 中设置 ?debug=tests 来启用 测试模式。
- 优点
更易于运行
可用于生产站点或测试站点,而不仅限于本地实例
允许以“入门”模式(手动步骤)运行
- 缺点
用于涉及 Python 初始化的测试导览时较难操作
根据导览的副作用,可能无法多次运行
小技巧
也可以使用这种方法来观察需要 Python 初始化的导览,或与之交互:
在启动相关导览(
start_tour或browser_js调用)之前添加一个 python 断点命中断点后,在浏览器中打开该实例
运行该导览
此时 Python 初始化对浏览器可见,导览即可运行。
如果还希望测试在之后继续执行,可能需要注释掉 start_tour 或 browser_js 调用,具体取决于导览的副作用。
browser_js 测试期间的截图和录屏¶
从命令行运行使用 HttpCase.browser_js 的测试时,Chrome 浏览器以无头模式运行。默认情况下,如果测试失败,会在失败时刻截取一张 PNG 截图,并写入
'/tmp/odoo_tests/{db_name}/screenshots/'
自 Odoo 13.0 起新增了两个命令行参数来控制此行为,即 --screenshots 和 --screencasts
检查 / 调试步骤¶
在尝试修复 / 调试导览时,失败时的截图未必足够。此时,查看某些步骤或每个步骤正在发生什么会很有帮助。
在“入门”模式下这很容易(因为大多由用户明确驱动),但在运行“测试”导览或通过测试套件运行导览时就复杂得多。此时主要有两个技巧:
在调试模式(debug=True)下使用步骤属性
break: true,。这会在步骤开始时添加一个调试器断点。之后可以在需要的任何位置添加自己的断点。
- 优点
非常简单
恢复执行后导览会立即继续
- 缺点
由于所有 JavaScript 都被阻塞,页面交互受到限制
在调试模式(debug=True)下使用步骤属性
pause: true,。导览会在该步骤结束时暂停。这样,在开发者于浏览器控制台中输入 play(); 恢复之前,可以检查和操作页面。
- 优点
允许与页面交互
没有(针对此情况)无用的调试器界面
使用带
run() { debugger; }动作的步骤。这可以添加到现有步骤中,也可以作为一个新的专用步骤。一旦匹配到步骤的 触发条件,执行将停止所有 JavaScript 执行。
- 优点
简单
恢复执行后导览会立即继续
- 缺点
由于所有 JavaScript 都被阻塞,页面交互受到限制
尝试查找步骤中定义的目标元素后才会触发调试器。
性能测试¶
查询次数¶
测试性能的方法之一是测量数据库查询次数。手动测试时,可以使用 --log-sql 命令行参数。如果要确定某项操作的最大查询次数,可以使用 assertQueryCount() 方法,该方法已集成在 Odoo 测试类中。
with self.assertQueryCount(11):
do_something()