测试 Odoo

测试应用程序的方法有很多。在 Odoo 中,我们有三种测试类型:

  • Python 单元测试(参见 Testing Python code ):用于测试模型的业务逻辑

  • JS 单元测试(参见 Testing JS code ):用于单独测试 JavaScript 代码

  • 巡游测试(参见 Integration Testing ):模拟真实场景。它们确保 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 内容(主要是模块)相关的实用工具和助手:

class odoo.tests.TransactionCase(methodName='runTest')[源代码]

测试类,其中所有测试方法都在单个事务中运行,但每个测试方法都在由保存点管理的子事务中运行。事务的游标始终在未提交的情况下关闭。

所有方法共用的数据设置应在类方法 setUpClass 中完成,以便对所有测试方法只执行一次。这对于包含快速测试但需要显著数据库设置的测试用例非常有用(复杂的数据库测试数据)。

每个测试方法运行后,都会清理记录缓存和注册表缓存。但是,注册表中的模型和字段不会被清理。如果测试修改了注册表(自定义模型和/或字段),则应准备必要的清理操作(self.registry.reset_changes())。

browse_ref(xid)[源代码]

为提供的 external identifier 返回一个记录对象

参数

xid – 完全限定的 external identifier,格式为 模块.标识符

抛出

如果未找到,则抛出 ValueError

返回

BaseModel

ref(xid)[源代码]

为提供的 external identifier 返回数据库 ID,是 _xmlid_lookup 的快捷方式

参数

xid – 完全限定的 external identifier,格式为 模块.标识符

抛出

如果未找到,则抛出 ValueError

返回

已注册的 ID

class odoo.tests.SingleTransactionCase(methodName='runTest')[源代码]

所有测试方法都在同一事务中运行的 TestCase,事务在第一个测试方法开始时启动,并在最后一个测试方法结束时回滚。

browse_ref(xid)[源代码]

为提供的 external identifier 返回一个记录对象

参数

xid – 完全限定的 external identifier,格式为 模块.标识符

抛出

如果未找到,则抛出 ValueError

返回

BaseModel

ref(xid)[源代码]

为提供的 external identifier 返回数据库 ID,是 _xmlid_lookup 的快捷方式

参数

xid – 完全限定的 external identifier,格式为 模块.标识符

抛出

如果未找到,则抛出 ValueError

返回

已注册的 ID

class odoo.tests.HttpCase(methodName='runTest')[源代码]

带有 url_open 和 Chrome 无头助手的事务性 HTTP 测试用例。

browser_js(url_path, code, ready='', login=None, timeout=60, cookies=None, error_checker=None, watch=False, success_signal='test successful', debug=False, cpu_throttling=None, **kw)[源代码]

测试在浏览器中运行的 JavaScript 代码。

要表示测试成功,请使用预期的 success_signal 调用 console.log()。默认值为 “test successful”。要表示测试失败,请抛出异常或使用消息调用 console.error。如果未定义 error_checker 或对该消息返回 True,则测试将在发生失败时停止。

参数
  • url_path (string) – 加载浏览器页面的 URL 路径

  • code (string) – 要执行的 JavaScript 代码

  • ready (string) – 在继续测试之前等待的 JavaScript 对象

  • login (string) – 将执行测试的登录用户,例如 ‘admin’、’demo’

  • timeout (int) – 等待测试完成的最大时间(以秒为单位)。默认值为 60 秒

  • cookies (dict) – 在加载页面之前要设置的 cookie 字典

  • error_checker – 用于过滤失败的函数。如果提供,则会用错误日志消息调用该函数,如果返回 False,则忽略日志并继续测试。如果不提供,则每个错误日志都会触发失败。

  • watch (bool) – 打开一个新浏览器窗口以观察测试执行

  • success_signal (string) – 等待的成功信号字符串,用于判断测试是否成功

  • debug (bool) – 自动打开一个全屏 Chrome 窗口,附带已打开的开发者工具,并在游览开始时设置调试断点。游览运行时会带有 debug=assets 查询参数。当抛出错误时,调试器会在异常处停止。

  • cpu_throttling (int) – CPU 限制速率作为减速因子(1 表示无限制,2 表示 2 倍减速,依此类推)

browse_ref(xid)[源代码]

为提供的 external identifier 返回一个记录对象

参数

xid – 完全限定的 external identifier,格式为 模块.标识符

抛出

如果未找到,则抛出 ValueError

返回

BaseModel

ref(xid)[源代码]

为提供的 external identifier 返回数据库 ID,是 _xmlid_lookup 的快捷方式

参数

xid – 完全限定的 external identifier,格式为 模块.标识符

抛出

如果未找到,则抛出 ValueError

返回

已注册的 ID

odoo.tests.tagged(*tags)[源代码]

一个用于标记 BaseCase 对象的装饰器。

标签存储在一个集合中,可以通过 ‘test_tags’ 属性访问。

以 ‘-’ 为前缀的标签将移除该标签,例如移除 ‘standard’ 标签。

默认情况下,odoo.tests.common 中的所有测试类都有一个 test_tags 属性,默认值为 ‘standard’ 和 ‘at_install’。

使用类继承时,标签会被继承。

默认情况下,测试将在相应模块安装完成后立即运行一次。测试用例也可以配置为在所有模块安装完成后运行,而不是在模块安装后立即运行:

# coding: utf-8
from odoo.tests import HttpCase, tagged

# This test should only be executed after all modules have been installed.
@tagged('-at_install', 'post_install')
class WebsiteVisitorTests(HttpCase):
  def test_create_visitor_on_tracked_page(self):
      Page = self.env['website.page']

最常见的做法是使用 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_ 开头

class odoo.tests.Form(record: odoo.models.BaseModel, view: None | int | str | odoo.models.BaseModel = None)[源代码]

服务器端表单视图实现(部分)

实现大部分“表单视图”操作流程,使服务器端测试能够更准确地反映界面操作时的行为:

  • 在“创建”时调用相关的 onchange;

  • 在设置字段时调用相关的 onchange;

  • 正确处理 x2many 字段周围的默认值和 onchange。

保存表单将返回当前记录(如果是创建模式,则表示创建的记录)。它也可以通过 form.record 访问,但前提是表单没有未提交的更改。

常规字段可以直接分配给表单。对于 Many2one 字段,可以分配一个记录集:

# empty recordset => creation mode
f = Form(self.env['sale.order'])
f.partner_id = a_partner
so = f.save()

还可以将表单用作上下文管理器来创建或编辑记录。更改将在作用域结束时自动保存:

with Form(self.env['sale.order']) as f1:
    f1.partner_id = a_partner
    # f1 is saved here

# retrieve the created record
so = f1.record

# call Form on record => edition mode
with Form(so) as f2:
    f2.payment_term_id = env.ref('account.account_payment_term_15days')
    # f2 is saved here

对于 Many2many 字段,字段本身是一个 M2MProxy ,可以通过添加或删除记录来修改:

with Form(user) as u:
    u.groups_id.add(env.ref('account.group_account_manager'))
    u.groups_id.remove(id=env.ref('base.group_portal').id)

最后,One2many 被具体化为 O2MProxy

由于 One2many 仅通过其父级存在,因此可以通过 new()edit() 方法创建“子表单”来直接操作。这些通常用作上下文管理器,因为它们会保存在父记录中:

with Form(so) as f3:
    f.partner_id = a_partner
    # add support
    with f3.order_line.new() as line:
        line.product_id = env.ref('product.product_product_2')
    # add a computer
    with f3.order_line.new() as line:
        line.product_id = env.ref('product.product_product_3')
    # we actually want 5 computers
    with f3.order_line.edit(1) as line:
        line.product_uom_qty = 5
    # remove support
    f3.order_line.remove(index=0)
    # SO is saved here
参数
  • record – 空记录集或单例记录集。空记录集会使视图从默认值进入“创建”模式,而单例记录集会使视图进入“编辑”模式并仅加载视图数据。

  • view – 用于触发 onchange 和视图约束的 ID、XMLID 或实际视图对象。如果未提供,则简单加载模型的默认视图。

12.0 新版功能.

save()[源代码]

保存表单(如有必要)并返回当前记录:

  • 不会保存 readonly 字段;

  • 不会保存未修改的字段(在编辑期间)—— 任何赋值或 onchange 返回都会将字段标记为已修改,即使设置为其当前值也是如此。

当无需保存时,仅返回当前记录。

引发

AssertionError – 如果表单中存在未填写的必填字段

property record

返回正在被表单编辑的记录。此属性是只读的,并且只能在表单没有待处理更改时访问。

class odoo.tests.M2MProxy(form, field_name)[源代码]

用于编辑多对多字段值的代理对象。

行为类似于 Sequence 的记录集,可以对其进行索引或切片以获取实际的基础记录集。

add(record)[源代码]

record 添加到字段中,该记录必须已经存在。

添加操作只有在父记录保存后才会最终完成。

remove(id=None, index=None)[源代码]

从字段中移除指定索引处或具有指定 ID 的记录。

clear()[源代码]

移除多对多字段中的所有现有记录

class odoo.tests.O2MProxy(form, field_name)[源代码]

用于编辑一对多字段值的代理对象。

new()[源代码]

返回一个新 One2many 记录的 Form ,并正确初始化。

如果列表视图可编辑,则从列表视图创建表单,否则从字段的表单视图创建。

引发

AssertionError – 如果字段不可编辑

edit(index)[源代码]

返回一个用于编辑现有 One2many 记录的 Form

如果列表视图可编辑,则从列表视图创建表单,否则从字段的表单视图创建。

引发

AssertionError – 如果字段不可编辑

remove(index)[源代码]

从父表单中移除索引为 index 的记录。

引发

AssertionError – 如果字段不可编辑

运行测试

如果在启动 Odoo 服务器时启用了 --test-enable ,则在安装或更新模块时会自动运行测试。

测试选择

在 Odoo 中,可以为 Python 测试添加标签,以便在运行测试时方便选择。

odoo.tests.BaseCase 的子类(通常通过 TransactionCaseHttpCase )默认会自动标记为 standardat_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 的测试会被执行。要同时运行 nicestandard 测试,请向 --test-tags 提供多个值:在命令行中,这些值是 累加的 (即选择具有 任意 指定标签的所有测试)。

$ odoo-bin --test-tags nice,standard

配置开关参数还接受 +- 前缀。 + 前缀是隐含的,因此完全可选。 - (减号)前缀用于取消选择带有该前缀标签的测试,即使它们被其他指定标签选中。例如,如果有标记为 standard 的测试同时也标记为 slow ,您可以运行所有标准测试,但排除那些慢速测试:

$ odoo-bin --test-tags 'standard,-slow'

当您编写一个未继承自 BaseCase 的测试时,该测试将不会拥有默认标签,您必须显式添加这些标签以将其包含在默认测试套件中。这是一个常见的问题,尤其是在使用简单的 unittest.TestCase 时,因为它们不会被执行:

import unittest
from odoo.tests import tagged

@tagged('standard', 'at_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

如果测试名称无歧义。可以一次指定多个模块、类和函数,用 , 分隔,类似于常规标签。

特殊标签

  • standard :所有继承自 BaseCase 的 Odoo 测试都会被隐式标记为 standard。 --test-tags 默认值也是 standard

    这意味着未标记的测试在启用测试时将默认执行。

  • at_install :意味着测试将在模块安装完成后立即执行,并且在其他模块安装之前执行。这是默认的隐式标签。

  • post_install :意味着测试将在所有模块安装完成后执行。这通常是 HttpCase 测试所需要的。

    请注意,这与 at_install 并不互斥,但由于通常不需要同时使用两者,因此在标记测试类时, post_install 通常与 -at_install 配对使用。

示例

重要

测试仅在已安装的模块中执行。如果从干净的数据库开始,您需要至少使用 -i 开关安装模块。之后,除非需要升级模块(在这种情况下可以使用 -u ),否则不再需要此开关。为简化起见,以下示例中未指定这些开关。

仅运行来自 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 中有一个非平凡的代码库,因此有必要对其进行测试。在本节中,我们将讨论隔离测试 JS 代码的做法:这些测试停留在浏览器中,不应触及服务器。

QUnit 测试套件

Odoo 框架使用 QUnit 库测试框架作为测试运行器。QUnit 定义了 测试模块 (一组相关测试)的概念,并为我们提供了一个基于 Web 的界面来执行测试。

例如,以下是一个 pyUtils 测试可能的样子:

QUnit.module('py_utils');

QUnit.test('simple arithmetic', function (assert) {
    assert.expect(2);

    var result = pyUtils.py_eval("1 + 2");
    assert.strictEqual(result, 3, "should properly evaluate sum");
    result = pyUtils.py_eval("42 % 5");
    assert.strictEqual(result, 2, "should properly evaluate modulo operator");
});

运行测试套件的主要方法是启动一个正在运行的 Odoo 服务器,然后在浏览器中导航到 /web/tests 。测试套件随后将由浏览器的 JavaScript 引擎执行。

../../../_images/tests.png

Web UI 具有许多有用的功能:它可以仅运行某些子模块,或过滤匹配字符串的测试。它可以显示每个断言(无论是失败还是通过),重新运行特定测试,等等。

警告

在测试套件运行时,请确保:

  • 您的浏览器窗口处于焦点状态,

  • 它没有缩放(放大或缩小)。必须保持 100% 的缩放比例。

如果不满足这些条件,某些测试可能会失败,并且不会提供明确的解释。

测试基础设施

以下是测试基础设施最重要部分的高级概述:

  • 有一个名为 web.qunit_suite 的资源包。该包包含主要代码(通用资源 + 后端资源)、一些库、QUnit 测试运行器以及下面列出的测试包。

  • 一个名为 web.tests_assets 的资源包包含了测试套件所需的大多数资源和工具:自定义 QUnit 断言、测试助手、延迟加载资源等。

  • 另一个资源包 web.qunit_suite_tests 包含了所有的测试脚本。通常,测试文件会添加到这里。

  • 在 Web 中有一个映射到路由 /web/testscontroller 。该控制器简单地渲染 web.qunit_suite 模板。

  • 要执行测试,只需将浏览器指向路由 /web/tests。在这种情况下,浏览器会下载所有资源,然后 QUnit 接管执行。

  • qunit_config.js 中有一些代码,当测试通过或失败时,会在控制台记录一些信息。

  • 我们希望 runbot 也能运行这些测试,因此有一个测试(在 test_js.py 中)会启动一个浏览器并将其指向 web/tests URL。请注意,browser_js 方法会启动一个无头模式的 Chrome 实例。

模块化与测试

由于 Odoo 的设计方式,任何插件都可以修改系统其他部分的行为。例如,voip 插件可以修改 FieldPhone 小部件以使用额外功能。从测试系统的角度来看,这并不是很好,因为这意味着当安装了 voip 插件时,addon web 中的测试将会失败(请注意,runbot 会在安装所有插件的情况下运行测试)。

同时,我们的测试系统也是有用的,因为它可以检测到其他模块何时破坏了一些核心功能。这个问题没有完全的解决方案。目前,我们根据具体情况进行处理。

通常,修改其他行为并不是一个好主意。对于我们的 voip 示例,添加一个新的 FieldVOIPPhone 小部件并修改需要它的几个视图无疑是更干净的做法。这样,FieldPhone 小部件不会受到影响,并且两者都可以进行测试。

添加新的测试用例

假设我们正在维护一个插件 my_addon,并且我们想为某些 JavaScript 代码(例如,位于 my_addon.utils 中的某个实用函数 myFunction)添加测试。添加新测试用例的过程如下:

  1. 创建一个新文件 my_addon/static/tests/utils_tests.js。该文件包含添加 QUnit 模块 my_addon > utils 的基本代码。

    odoo.define('my_addon.utils_tests', function (require) {
    "use strict";
    
    var utils = require('my_addon.utils');
    
    QUnit.module('my_addon', {}, function () {
    
        QUnit.module('utils');
    
    });
    });
    
  2. my_addon/assets.xml 中,将文件添加到主测试资源中:

    <?xml version="1.0" encoding="utf-8"?>
    <odoo>
        <template id="qunit_suite_tests" name="my addon tests" inherit_id="web.qunit_suite_tests">
            <xpath expr="//script[last()]" position="after">
                <script type="text/javascript" src="/my_addon/static/tests/utils_tests.js"/>
            </xpath>
        </template>
    </odoo>
    
  3. 重启服务器并更新 my_addon,或者通过界面操作(以确保加载了新的测试文件)。

  4. utils 子测试套件定义之后添加一个测试用例:

    QUnit.test("some test case that we want to test", function (assert) {
        assert.expect(1);
    
        var result = utils.myFunction(someArgument);
        assert.strictEqual(result, expectedResult);
    });
    
  5. 访问 /web/tests/ 以确保测试被执行。

辅助函数和专用断言

没有辅助工具的情况下,测试 Odoo 的某些部分相当困难。特别是视图,因为它们与服务器通信并可能执行许多 RPC 调用,这需要模拟(mock)。这就是我们开发了一些专用辅助函数的原因,这些函数位于 test_utils.js 中。

  • 模拟测试函数:这些函数帮助设置测试环境。最重要的用例是模拟 Odoo 服务器返回的答案。这些函数使用一个 mock server 。这是一个 JavaScript 类,用于模拟最常见的模型方法的答案,例如 read、search_read、nameget 等。

  • DOM 辅助工具:用于模拟某些特定目标上的事件/操作。例如,testUtils.dom.click 会在目标上执行点击操作。请注意,这样做比手动操作更安全,因为它还会检查目标是否存在且可见。

  • 创建辅助工具:它们可能是 test_utils.js 导出的最重要的函数。这些辅助工具可用于创建一个带有模拟环境的小部件,并包含许多小细节以尽可能模拟真实条件。其中最重要的是 createView

  • qunit assertions :QUnit 可以通过专用断言进行扩展。在 Odoo 中,我们经常测试一些 DOM 属性。这就是为什么我们制作了一些断言来帮助完成这项工作。例如,containsOnce 断言接受一个小部件/jQuery/HtmlElement 和一个选择器,然后检查目标是否恰好包含一个匹配该 CSS 选择器的元素。

例如,借助这些辅助工具,以下是一个简单的表单测试示例:

QUnit.test('simple group rendering', function (assert) {
    assert.expect(1);

    var form = testUtils.createView({
        View: FormView,
        model: 'partner',
        data: this.data,
        arch: '<form string="Partners">' +
                '<group>' +
                    '<field name="foo"/>' +
                '</group>' +
            '</form>',
        res_id: 1,
    });

    assert.containsOnce(form, 'table.o_inner_group');

    form.destroy();
});

注意 testUtils.createView 辅助工具和 containsOnce 断言的使用。此外,在测试结束时,表单控制器被正确销毁。

最佳实践

无特定顺序:

  • 所有测试文件都应添加到 some_addon/static/tests/

  • 对于错误修复,确保在没有修复的情况下测试失败,而在修复后测试通过。这可以确保修复确实有效。

  • 尽量保持测试运行所需的最少代码量。

  • 通常,两个小测试比一个大测试更好。小测试更容易理解和修复。

  • 始终在测试后清理。例如,如果您的测试实例化了一个小部件,则应在结束时销毁它。

  • 不需要完全覆盖所有代码,但添加一些测试会有很大帮助:它可以确保您的代码不会完全崩溃,并且每当修复错误时,向现有测试套件中添加测试会变得容易得多。

  • 如果您想检查某些否定断言(例如,某个 HtmlElement 不具有特定的 CSS 类),则尝试在同一测试中添加肯定断言(例如,通过执行改变状态的操作)。这将有助于避免测试在未来失效(例如,如果 CSS 类被更改)。

提示

  • 仅运行一个测试:您可以(临时!)将 QUnit.test(…) 定义更改为 QUnit.only(…)。这有助于确保 QUnit 仅运行此特定测试。

  • 调试标志:大多数创建实用函数都有调试模式(通过 debug: true 参数激活)。在这种情况下,目标小部件将被放入 DOM 中,而不是隐藏在 qunit 特定的 fixture 中,并且会记录更多信息。例如,所有模拟的网络通信都会显示在控制台中。

  • 在处理失败的测试时,通常会添加调试标志,然后注释掉测试的结尾部分(尤其是 destroy 调用)。这样可以直接查看小部件的状态,甚至可以通过点击或交互来操作小部件。

集成测试

分别测试 Python 代码和 JavaScript 代码非常有用,但这并不能证明 Web 客户端和服务器能够协同工作。为此,我们可以编写另一种测试:巡游(tours)。巡游是一个有趣的业务流程的小型场景。它描述了一系列应遵循的步骤。测试运行器将创建一个 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

  1. 通过注册来设置您的巡游。

    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
    ]);
    
  2. 添加您想要的任何步骤。

每个步骤至少包含一个触发器。您可以使用 预定义步骤 或编写您自己的个性化步骤。

以下是一些步骤示例:

Example

// 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
},

Example

{
    trigger: '.js_product:has(strong:contains(Chair floor protection)) .js_add',
    run: "click",
},

Example

{
    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:可选,在 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 填充内容。

      editor content

      聚焦 触发 元素(wysiwyg),然后 按下 内容

      fill content

      聚焦 触发 元素,然后 按下 内容 。此辅助工具仅适用于 <input><textarea> 元素。

      悬停

      触发 元素上执行悬停操作序列。

      press content

      执行键盘事件序列。

      range content

      聚焦 触发 元素并将 内容 设置为值。此辅助工具仅适用于 <input[type=range]> 元素。

      select value

      触发 元素上执行选择事件序列。通过其 选择选项。此辅助工具仅适用于 <select> 元素。

      selectByIndex index

      select 相同,但通过其 索引 选择选项。注意,第一个选项的索引为 0。

      selectByLabel label

      select 相同,但通过其 标签 选择选项。

      取消选中

      确保 触发 元素未被选中。此辅助工具仅适用于 <input[type=checkbox]> 元素。

  • isActive :可选,仅当满足 isActive 数组的所有条件时激活步骤。- 浏览器处于 桌面移动 模式。- 该巡游涉及 社区版企业版 。- 巡游以 自动 (runbot)或 手动 (引导)模式运行。

  • tooltipPosition:可选, "顶部""右侧""底部""左侧" 。在运行交互式巡游时,提示框相对于 目标 的位置。

  • content:可选但推荐,交互式巡游中提示框的内容,同时也会记录到控制台,因此对于跟踪和调试自动化巡游非常有用。

  • timeout:步骤可以 运行 前需要等待的时间,单位为毫秒,默认为 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 ,将 your_tour.js 添加到资源中,并将 your_tour.xml 添加到数据中。

'data': [
   'data/your_tour.xml',
],
'assets': {
    'web.assets_backend': [
        'your_module/static/src/js/tours/your_tour.js',
    ],
},

JavaScript

JavaScript 部分与 :ref: the test tour <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。如果 urlFalse ,则从注册表中获取 URL。默认为 “/odoo” 。

  • rainbow_man_message :可选;在巡游完成时显示彩虹人效果中的消息。如果 rainbow_man_messageFalse ,则没有彩虹效果。默认为 <b>干得好!</b> 您已经完成了此巡游的所有步骤。

运行引导巡游

可以通过在用户菜单中切换 引导 选项,按照其顺序启动所有巡游。您还可以通过进入 设置 ‣ 技术 ‣ 用户界面 ‣ 巡游 并单击 引导测试 来运行特定的引导巡游。

  • 引导:将以交互模式执行巡游。这意味着巡游将显示需要执行的操作,并等待用户进行交互。

  • 测试:将自动执行巡游。这意味着巡游将在用户面前执行所有步骤。

巡游录制器

您还可以使用巡游录制器轻松创建巡游。为此,请在引导巡游视图中单击 录制 。启动后,该工具将记录您在 Odoo 中的所有交互。

创建的巡游在引导巡游视图中标记为 自定义。这些巡游还可以导出为 JavaScript 文件,以便放入您的模块中。

调试提示

在浏览器中观察测试巡游

有三种方法,各有优劣:

watch=True

当通过测试套件在本地运行巡游时,可以将 watch=True 参数添加到 browser_jsstart_tour 调用中::

self.start_tour("/web", "your_tour_name", watch=True)

这将自动打开一个 Chrome 窗口,巡游将在其中运行。

优点
  • 如果巡游包含 Python 设置/外围代码或多个步骤,则始终有效

  • 完全自动运行(只需选择启动巡游的测试)

  • 事务性(应该 始终能够多次运行)

缺点
  • 仅在本地工作

  • 仅在测试/巡游可以在本地正确运行时有效

debug=True

当通过测试套件在本地运行巡游时,可以将 debug=True 参数添加到 browser_jsstart_tour 调用中::

self.start_tour("/web", "your_tour_name", debug=True)

这将自动打开一个全屏的 Chrome 窗口,同时打开开发者工具,并在巡游开始时设置调试器断点。巡游将以 debug=assets 查询参数运行。当抛出错误时,调试器会在异常处停止。

优点
  • watch=True 模式具有相同的优点

  • 更容易调试步骤

缺点
  • 仅在本地工作

  • 仅在测试/巡游可以在本地正确运行时有效

通过浏览器运行

测试巡游也可以通过调用以下方式从浏览器界面启动:

odoo.startTour("tour_name");

在 JavaScript 控制台中,或者通过在 URL 中设置 ?debug=tests 来启用 测试模式

优点
  • 更容易运行

  • 可用于生产或测试站点,而不仅仅是本地实例

  • 允许以“引导”模式运行(手动步骤)

缺点
  • 对于涉及 Python 设置的测试巡游来说更难使用

  • 可能由于巡游的副作用而无法多次运行

小技巧

可以使用此方法观察或与需要 Python 设置的巡游进行交互:

  • 在相关巡游启动之前( start_tourbrowser_js 调用)添加一个 Python 断点

  • 当命中断点时,在浏览器中打开实例

  • 运行巡游

此时,Python 设置将对浏览器可见,巡游将能够运行。

如果希望测试在之后继续运行(取决于巡游的副作用),您可能需要注释掉 start_tourbrowser_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; } 操作的步骤。

    这可以添加到现有步骤中,也可以是一个新的专用步骤。一旦步骤的 trigger 被匹配,执行将停止所有 JavaScript 执行。

    优点
    • 简单

    • 一旦恢复执行,巡游将继续

    缺点
    • 由于所有 JavaScript 被阻塞,页面交互受到限制

    • 在尝试查找步骤中定义的目标元素后触发调试器。

性能测试

查询计数

测试性能的一种方法是测量数据库查询。手动测试时,可以使用 --log-sql 命令行参数。如果要为某个操作设定最大查询数量,可以使用集成在 Odoo 测试类中的 assertQueryCount() 方法。

with self.assertQueryCount(11):
    do_something()