测试 Odoo

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

  • Python 单元测试(参见 测试 Python 代码):用于测试模型业务逻辑

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

  • Tours (see Integration Testing): tours simulate a real situation. They ensures that the python and the javascript parts properly talk to each other.

测试 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文档 _所述,但Odoo提供了一些与测试Odoo内容(主要是模块)相关的实用工具和辅助函数:

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

Test class in which all test methods are run in a single transaction, but each test method is run in a sub-transaction managed by a savepoint. The transaction’s cursor is always closed without committing.

The data setup common to all methods should be done in the class method setUpClass, so that it is done once for all test methods. This is useful for test cases containing fast tests but with significant database setup common to all cases (complex in-db test data).

After being run, each test method cleans up the record cache and the registry cache. However, there is no cleanup of the registry models and fields. If a test modifies the registry (custom models and/or fields), it should prepare the necessary cleanup (self.registry.reset_changes()).

browse_ref(xid)[源代码]

Returns a record object for the provided external identifier

参数

xid – fully-qualified external identifier, in the form module.identifier

Raise

ValueError if not found

返回

BaseModel

ref(xid)[源代码]

Returns database ID for the provided external identifier, shortcut for _xmlid_lookup

参数

xid – fully-qualified external identifier, in the form module.identifier

Raise

ValueError if not found

返回

registered id

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

TestCase in which all test methods are run in the same transaction, the transaction is started with the first test method and rolled back at the end of the last.

browse_ref(xid)[源代码]

Returns a record object for the provided external identifier

参数

xid – fully-qualified external identifier, in the form module.identifier

Raise

ValueError if not found

返回

BaseModel

ref(xid)[源代码]

Returns database ID for the provided external identifier, shortcut for _xmlid_lookup

参数

xid – fully-qualified external identifier, in the form module.identifier

Raise

ValueError if not found

返回

registered id

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

Transactional HTTP TestCase with url_open and Chrome headless helpers.

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

Test js code running in the browser - optionnally log as ‘login’ - load page given by url_path - wait for ready object to be available - eval(code) inside the page

To signal success test do: console.log() with the expected success signal (“test successful” by default) To signal test failure raise an exception or call console.error with a message. Test will stop when a failure occurs if error_checker is not defined or returns True for this message

browse_ref(xid)[源代码]

Returns a record object for the provided external identifier

参数

xid – fully-qualified external identifier, in the form module.identifier

Raise

ValueError if not found

返回

BaseModel

ref(xid)[源代码]

Returns database ID for the provided external identifier, shortcut for _xmlid_lookup

参数

xid – fully-qualified external identifier, in the form module.identifier

Raise

ValueError if not found

返回

registered id

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

A decorator to tag BaseCase objects.

Tags are stored in a set that can be accessed from a ‘test_tags’ attribute.

A tag prefixed by ‘-’ will remove the tag e.g. to remove the ‘standard’ tag.

By default, all Test classes from odoo.tests.common have a test_tags attribute that defaults to ‘standard’ and ‘at_install’.

When using class inheritance, the tags ARE inherited.

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

# 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']

The most common situation is to use TransactionCase and test a property of a model in each method:

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: Union[None, int, str, odoo.models.BaseModel] = None)[源代码]

Server-side form view implementation (partial)

Implements much of the “form view” manipulation flow, such that server-side tests can more properly reflect the behaviour which would be observed when manipulating the interface:

  • call the relevant onchanges on “creation”;

  • call the relevant onchanges on setting fields;

  • properly handle defaults & onchanges around x2many fields.

Saving the form returns the current record (which means the created record if in creation mode). It can also be accessed as form.record, but only when the form has no pending changes.

Regular fields can just be assigned directly to the form. In the case of Many2one fields, one can assign a recordset:

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

One can also use the form as a context manager to create or edit a record. The changes are automatically saved at the end of the scope:

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

For Many2many fields, the field itself is a M2MProxy and can be altered by adding or removing records:

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)

Finally One2many are reified as O2MProxy.

Because the One2many only exists through its parent, it is manipulated more directly by creating “sub-forms” with the new() and edit() methods. These would normally be used as context managers since they get saved in the parent record:

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 – empty or singleton recordset. An empty recordset will put the view in “creation” mode from default values, while a singleton will put it in “edit” mode and only load the view’s data.

  • view – the id, xmlid or actual view object to use for onchanges and view constraints. If none is provided, simply loads the default view for the model.

12.0 新版功能.

save()[源代码]

Save the form (if necessary) and return the current record:

  • does not save readonly fields;

  • does not save unmodified fields (during edition) — any assignment or onchange return marks the field as modified, even if set to its current value.

When nothing must be saved, it simply returns the current record.

引发

AssertionError – if the form has any unfilled required field

property record

Return the record being edited by the form. This attribute is readonly and can only be accessed when the form has no pending changes.

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

Proxy object for editing the value of a many2many field.

Behaves as a Sequence of recordsets, can be indexed or sliced to get actual underlying recordsets.

add(record)[源代码]

Adds record to the field, the record must already exist.

The addition will only be finalized when the parent record is saved.

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

Removes a record at a certain index or with a provided id from the field.

clear()[源代码]

Removes all existing records in the m2m

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

Proxy object for editing the value of a one2many field.

new()[源代码]

Returns a Form for a new One2many record, properly initialised.

The form is created from the list view if editable, or the field’s form view otherwise.

引发

AssertionError – if the field is not editable

edit(index)[源代码]

Returns a Form to edit the pre-existing One2many record.

The form is created from the list view if editable, or the field’s form view otherwise.

引发

AssertionError – if the field is not editable

remove(index)[源代码]

Removes the record at index from the parent form.

引发

AssertionError – if the field is not editable

运行测试

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

测试选择

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

Subclasses of odoo.tests.BaseCase (usually through TransactionCase or HttpCase) are automatically tagged with standard and at_install by default.

调用

--test-tags 可以用于在命令行上选择/过滤要运行的测试。它隐含了 --test-enable,因此在使用 --test-tags 时不需要指定 --test-enable

此选项默认为 +standard,意味着标记为 standard 的测试(无论是显式还是隐式标记)将在使用 --test-enable 启动 Odoo 时默认运行。

When writing tests, the tagged() decorator can be used on test classes to add or remove tags.

装饰器的参数是标签名称,以字符串形式表示。

危险

tagged() is a class decorator, it has no effect on functions or methods

Tags can be prefixed with the minus (-) sign, to remove them instead of add or select them e.g. if you don’t want your test to be executed by default you can remove the standard tag:

from odoo.tests import TransactionCase, tagged

@tagged('-standard', 'nice')
class NiceTest(TransactionCase):
    ...

此测试不会默认选择,需要显式选择相关标签才能运行:

$ odoo-bin --test-tags nice

Note that only the tests tagged nice are going to be executed. To run both nice and standard tests, provide multiple values to --test-tags: on the command-line, values are additive (you’re selecting all tests with any of the specified tags)

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

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

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

When you write a test that does not inherit from the BaseCase, this test will not have the default tags, you have to add them explicitly to have the test included in the default test suite. This is a common issue when using a simple unittest.TestCase as they’re not going to get run:

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: All Odoo tests that inherit from BaseCase are implicitly tagged standard. --test-tags also defaults to standard.

    这意味着当测试被启用时,默认情况下将执行未标记的测试。

  • at_install: Means that the test will be executed right after the module installation and before other modules are installed. This is a default implicit tag.

  • post_install: Means that the test will be executed after all the modules are installed. This is what you want for HttpCase tests most of the time.

    请注意,这与 at_install 不是互斥的 ,然而,由于通常不会同时使用两者,当标记一个测试类时, post_install 通常与 -at_install 配对使用。

示例

重要

只有已安装的模块才会执行测试。如果您从一个干净的数据库开始,您需要使用 -i 开关至少安装一次模块。之后就不再需要了,除非您需要升级模块,在这种情况下可以使用 -u。为简单起见,这些开关在下面的示例中没有指定。

仅运行销售模块的测试:

$ odoo-bin --test-tags /sale

运行销售模块的测试,但不运行标记为slow的测试:

$ odoo-bin --test-tags '/sale,-slow'

仅运行库存中的测试或标记为slow的测试:

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

注解

-standard is implicit (not required), and present for clarity

测试 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 中有一个 controller,映射到路由 /web/tests。这个控制器只是简单地渲染 web.qunit_suite 模板。

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

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

  • 我们希望 runbot 也运行这些测试,所以有一个测试(在 test_js.py 中)简单地生成一个浏览器并将其指向 web/tests 的 URL。请注意,browser_js 方法生成一个 Chrome 无头实例。

模块化和测试

Odoo 的设计方式允许任何插件修改系统的其他部分的行为。例如, voip 插件可以修改 FieldPhone 小部件以使用额外的功能。从测试系统的角度来看,这并不是很好,因为这意味着在安装 voip 插件时,插件 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,需要进行模拟。这就是为什么我们开发了一些专门的辅助函数,位于 test_utils.js 中。

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

  • DOM 帮助程序:用于模拟对特定目标的事件/操作。例如,testUtils.dom.click 在目标上执行单击操作。请注意,它比手动执行更安全,因为它还检查目标是否存在且可见。

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

  • qunit assertions: QUnit can be extended with specialized assertions. For Odoo, we frequently test some DOM properties. This is why we made some assertions to help with that. For example, the containsOnce assertion takes a widget/jQuery/HtmlElement and a selector, then checks if the target contains exactly one match for the css selector.

例如,使用这些辅助工具,一个简单的表单测试可能如下所示:

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 特定装置中,并且将记录更多信息。例如,所有模拟的网络通信将在控制台中可用。

  • 在处理失败的测试时,通常会添加调试标志,然后注释测试的结尾(特别是销毁调用)。这样,就可以直接查看小部件的状态,甚至更好的是,通过点击/交互来操作小部件。

集成测试

分别测试Python代码和JS代码非常有用,但这并不能证明Web客户端和服务器能够协同工作。为了做到这一点,我们可以编写另一种测试:tours。一个tour是一种有趣的业务流程的迷你场景。它解释了应该遵循的一系列步骤。测试运行程序将创建一个PhantomJs浏览器,将其指向正确的URL,并根据场景模拟点击和输入。

编写测试向导

结构

要为 your_module 编写测试导览,请从创建所需的文件开始:

your_module
├── ...
├── static
|   └── tests
|       └── tours
|           └── your_tour.js
├── tests
|   ├── __init__.py
|   └── test_calling_the_tour.py
└── __manifest__.py

然后您可以:

  • 更新 __manifest__.py 文件,在 assets 中添加 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
        test: true,
    }, [
        // Your sequence of steps
    ]);
    
  2. 添加任何您想要的步骤。

Every step contains at least a trigger. You can either use the predefined steps or write your own personalized step.

以下是一些步骤示例:

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: Required, Selector/element to run an action on. The tour will wait until the element exists and is visible before run-ing the action on it.

  • run: Optional, Action to perform on the trigger element. If no run, no action.

    The action can be:

    • A function, asynchronous, executed with the trigger’s Tip as context (this) and the action helpers as parameter.

    • 触发器元素上将运行的动作助手之一的名称:

      check

      Ensures that the trigger element is checked. This helper is intended for <input[type=checkbox]> elements only.

      clear

      Clears the value of the trigger element. This helper is intended for <input> or <textarea> elements only.

      click

      Clicks the trigger element, performing all the relevant intermediate events.

      dblclick,

      Same as click with two repetitions.

      拖放 target

      模拟将 trigger 元素拖动到 target 上。

      edit content

      clear the element and then fill the content.

      editor content

      Focus the trigger element (wysiwyg) and then press the content.

      fill content

      Focus the trigger element and then press the content. This helper is intended for <input> or <textarea> elements only.

      hover

      Performs a hover sequence on the trigger element.

      press content

      Performs a keyboard event sequence.

      range content

      Focus the trigger element and set content as value. This helper is intended for <input[type=range]> elements only.

      select value

      Performs a selection event sequence on trigger element. Select the option by its value. This helper is intended for <select> elements only.

      selectByIndex index

      Same as select but select the option by its index. Note that first option has index 0.

      selectByLabel label

      Same as select but select the option by its label.

      uncheck

      Ensures that the trigger element is unchecked. This helper is intended for <input[type=checkbox]> elements only.

  • isActive: Optional, Activates the step only if all conditions of isActive array are met. - Browser is in either desktop or mobile mode. - The tour concerns either community or enterprise edition. - The tour is run in either auto (runbot) or manual (onboarding) mode.

  • tooltipPosition: Optional, "top", "right", "bottom", or "left". Where to position the tooltip relative to the target when running interactive tours.

  • content: 可选但建议的,交互式导览中工具提示的内容,也会记录到控制台,非常有用于跟踪和调试自动化导览。

  • timeout: 等待步骤可以 run 的时间,以毫秒为单位,10000 (10 秒)。

重要

一个导览的最后一步应该总是将客户端返回到“稳定”状态(例如,没有进行中的编辑),并确保所有副作用(网络请求)已经完成运行,以避免拆卸期间的竞争条件或错误。

Python

To start a tour from a python test, make the class inherit from HTTPCase, and call start_tour:

def test_your_test(self):
    # Optional Setup
    self.start_tour("/web", 'your_module.your_tour_name', login="admin")
    # Optional verifications

调试技巧

在浏览器中观察旅游

There are three ways with different tradeoffs:

watch=True

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

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

这将自动打开一个 Chrome 窗口,其中包含正在运行的导览。

优势
  • 如果导览有 Python 设置/周围代码或多个步骤,则始终有效

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

  • transactional (should always be runnable multiple times)

缺点
  • 仅在本地工作

  • 仅在测试/导览可以在本地正确运行时才有效

debug=True

When running a tour locally via the test suite, the debug=True parameter can be added to the browser_js or start_tour call:

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

This will automatically open a fullscreen Chrome window with opened devtools and a debugger breakpoint set at the start of the tour. The tour is ran with the debug=assets query parameter. When an error is thrown, the debugger stops on the exception.

优势
  • Same advantages as mode watch=True

  • Easier to debug steps

缺点
  • 仅在本地工作

  • 仅在测试/导览可以在本地正确运行时才有效

通过浏览器运行

游览也可以通过浏览器 UI 启动,可以通过调用

odoo.startTour(tour_name);

在javascript控制台中,或者通过启用 测试模式 ,在URL中设置 ?debug=tests ,然后在调试菜单中选择 开始导览 并选择一个导览:

../../../_images/tours.png
优势
  • 更容易运行

  • 可以在生产或测试站点上使用,而不仅仅是本地实例

  • 允许在“入职”模式下运行(手动步骤)

缺点
  • 在涉及 Python 设置的测试环节中使用起来更加困难

  • 根据您的副作用,可能无法多次使用

小技巧

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

  • 在相关的导览开始之前添加一个 python 断点 (start_tourbrowser_js 调用)

  • 当断点被触发时,在浏览器中打开实例

  • 运行导览

此时,Python设置将对浏览器可见,并且可以运行该教程。

如果您希望测试继续进行,取决于导览的副作用,您可能需要注释掉 start_tourbrowser_js 调用。

在浏览器JS测试期间进行截图和屏幕录像

在运行使用 HttpCase.browser_js 的测试时,从命令行中使用Chrome浏览器的无头模式。默认情况下,如果测试失败,将在失败时刻拍摄PNG截图并写入

'/tmp/odoo_tests/{db_name}/screenshots/'

自 Odoo 13.0 版本以来,添加了两个新的命令行参数来控制此行为: --screenshots--screencasts

内省/调试步骤

当尝试修复/调试导览时,截图(在失败时)可能不足够。在这种情况下,查看每个步骤发生的情况可能会很有用。

当在”引导”模式下时,这非常容易(因为它们大多数是由用户显式驱动的),但在运行”测试”游览或通过测试套件运行游览时,情况就更加复杂了。在这种情况下,有两个主要技巧:

  • A step property break: true, in debug mode (debug=True).

    This adds a debugger breakpoint at the start of the step. You can then add your own wherever you need.

    优势
    • 非常简单

    • the tour continues as soon as you resume execution

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

  • A step property pause: true, in debug mode (debug=True).

    The tour will stop at the end of the step. This allows inspecting and interacting with the page until the developer is ready to resume by typing play(); in the browser console.

    优势
    • 允许与页面交互

    • 没有无用的(对于此情况)调试器用户界面

  • A step with a run() { debugger; } action.

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

    优势
    • simple

    • the tour continues as soon as you resume execution

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

    • the debugger is triggered after trying to find targeted element defined in the step.

性能测试

查询计数

One of the ways to test performance is to measure database queries. Manually, this can be tested with the --log-sql CLI parameter. If you want to establish the maximum number of queries for an operation, you can use the assertQueryCount() method, integrated in Odoo test classes.

with self.assertQueryCount(11):
    do_something()