测试 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

前提是测试名称没有歧义。可以像使用普通标签那样,用 , 分隔,一次指定多个模块、类和函数。

特殊标签

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

    这意味着启用测试时,未打标签的测试会默认被执行。

  • post_install:表示该测试将在所有模块安装完成后执行。这是一个默认的隐式标签。

  • at_install:表示该测试将在模块安装后立即执行,并且先于其他模块的安装。

    应尽量少用,因为它会导致测试无法在 runbot 上并行运行。一个有益的补充是在测试上方添加注释,说明为何需要在其他模块安装之前运行它。

    请注意,它与 post_install 是 互斥 的,因此给测试类打标签时,at_install 通常与 -post_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 代码库,因此有必要对其进行测试。

请参阅 单元测试 了解前端测试框架的各个方面,或直接跳转到下面的某个小节:

集成测试

分别测试 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

  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. 添加你需要的任意步骤。

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

以下是一些步骤示例:

示例

// 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()