测试 Odoo¶
测试应用程序的方法有很多种。 在 Odoo 中,我们有三种测试
Python 单元测试(请参阅 Testing Python code):对于测试模型业务逻辑很有用
JS 单元测试(参见 Testing JS code):对于单独测试 javascript 代码很有用
游览(参见 Integration Testing):游览模拟真实情况。它们确保 python 和 javascript 部分能够正确地相互通信。
测试Python代码¶
Odoo 支持使用 Python’s unittest library 测试模块。
要编写测试,只需定义一个``tests`` sub-package in your module, it will be automatically inspected for test modules. Test modules should have a name starting with test_ and should be imported from 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 内容(主要是模块)相关的实用程序和帮助程序:
默认情况下,测试在安装相应模块后立即运行一次。测试用例还可以配置为在安装所有模块后运行,而不是在模块安装后立即运行:
# 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_”开头
运行测试¶
如果在启动 Odoo 服务器时启用了 --test-enable,则在安装或更新模块时会自动运行测试。
测试选择¶
在 Odoo 中,可以对 Python 测试进行标记,以方便运行测试时的测试选择。
默认情况下,odoo.tests.BaseCase 的子类(通常通过 TransactionCase 或 HttpCase)自动标记为 standard and at_install。
祈求¶
--test-tags 可用于选择/过滤要在命令行上运行的测试。它隐含 --test-enable,因此在使用 --test-tags 时无需指定 --test-enable。
此选项默认为“+standard` meaning tests tagged ``standard`”(显式或隐式),当使用 --test-enable 启动 Odoo 时,将默认运行。
编写测试时,可以在 测试类 上使用 tagged() 装饰器来添加或删除标签。
装饰器的参数是标签名称,作为字符串。
危险
tagged() 是一个类装饰器,它对函数或方法没有影响
标签可以使用减号(-) 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``标签作为前缀:
from odoo.tests import TransactionCase, tagged
@tagged('-standard', 'nice')
class NiceTest(TransactionCase):
...
默认情况下不会选择此测试,要运行它,必须显式选择相关标签:
$ odoo-bin --test-tags nice
请注意,只有标记为“nice` are going to be executed. To run both nice and ``standard`”测试的测试才向 --test-tags 提供多个值:在命令行上,值是“相加的”(您正在选择具有“任意”指定标签的所有测试)
$ odoo-bin --test-tags nice,standard
配置开关参数还接受``+`` and - prefixes. The + prefix is implied and therefore, totally optional. The - (minus) prefix is made to deselect tests tagged with the prefixed tags, even if they are selected by other specified tags e.g. if there are standard tests which are also tagged as ``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
如果测试的名称明确。可以像常规标签一样通过 , 分隔一次指定多个模块、类和函数。
示例¶
重要
测试将仅在已安装的模块中执行。如果您从干净的数据库开始,则需要至少使用 -i 开关安装模块一次。之后就不再需要它了,除非您需要升级模块,在这种情况下可以使用 -u 。为简单起见,下面的示例中未指定这些开关。
仅运行销售模块中的测试:
$ odoo-bin --test-tags /sale
从销售模块运行测试,但不运行标记为慢的测试:
$ odoo-bin --test-tags '/sale,-slow'
仅运行库存测试或标记为慢的测试:
$ odoo-bin --test-tags '-standard, slow, /stock'
注解
-standard 是隐式的(不是必需的),为了清晰起见而存在
测试 JS 代码¶
测试复杂的系统是防止回归并保证某些基本功能仍然有效的重要保障。由于 Odoo 在 Javascript 中有一个不平凡的代码库,因此有必要对其进行测试。
请参阅 Unit testing 了解前端测试框架的各个方面,或直接跳转到其中一个子部分:
集成测试¶
单独测试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 ]);
添加您想要的任何步骤。
每个步骤至少包含一个触发器。您可以使用 predefined steps 或编写您自己的个性化步骤。
以下是一些步骤示例:
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();
}
},
以下是您的个性化步骤的一些可能的论点:
触发:必需,选择器/元素“
run`an action on. The tour will wait until the element exists and is visible before ``run`”-执行*其上的操作。运行:可选,对 trigger 元素执行的操作。如果没有``run``,则不执行任何操作。
行动可以是:
一个异步函数,使用触发器的
Tipas context (this) 和操作助手作为参数执行。将在触发器元素上运行的操作助手之一的名称:
check确保检查 trigger 元素。此帮助程序仅适用于
<input[type=checkbox]>元素。clear清除 trigger 元素的值。此帮助程序仅适用于
<input>或<textarea>元素。click单击 trigger 元素,执行所有相关的中间事件。
dblclick,与“
click”相同,但重复两次。drag_and_drop target模拟将 trigger 元素拖动到“
target”。edit contentclearthe element and thenfillthecontent。editor content聚焦**触发**元素(所见即所得),然后聚焦“
press`the ``content`”。fill content仅聚焦 trigger 元素,然后聚焦
pressthecontent. This helper is intended for<input>or<textarea>元素。hover在 trigger 元素上执行悬停序列。
press content执行键盘事件序列。
range content聚焦 trigger 元素并仅设置
contentas value. This helper is intended for<input[type=range]>元素。select value在 trigger 元素上执行选择事件序列。仅通过其
value. This helper is intended for<select>元素选择选项。selectByIndex index与``select`` but select the option by its ``index``相同。请注意,第一个选项的索引为 0。
selectByLabel label与``select`` but select the option by its ``label``相同。
uncheck确保未选中 trigger 元素。此帮助程序仅适用于
<input[type=checkbox]>元素。
isActive:可选,仅当 isActive 数组的所有条件都满足时才激活该步骤。 - 浏览器处于 桌面 或 移动 模式。 - 本次游览涉及 社区 或 企业 版本。 - 游览以**自动**(runbot)或**手动**(onboarding)模式运行。
工具提示位置:可选,
"top","right","bottom", or"left"。运行交互式游览时,工具提示相对于**目标**的位置。内容:可选但推荐,交互式游览中工具提示的内容也记录到控制台,因此对于跟踪和调试自动游览非常有用。
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 以在资产中添加 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。如果是“url`is ``False`”,则从注册表中获取 URL。默认为“/odoo”。rainbow_man_message:可选;游览结束时将在彩虹人效果中显示该消息。如果``rainbow_man_message`` isFalse, there is no rainbow effect. Defaults to<b>Good job!</b> You went through all steps of this tour.
开展入职培训¶
通过切换用户菜单中的 Onboarding 选项,可以按顺序启动它们。您可以通过转至 并单击 Onboarding 或 Testing 来运行特定的入门教程。
入门:将以交互模式执行游览。这意味着游览将显示要做什么并等待用户的交互。
测试:将自动执行游览。这意味着游览将在用户面前执行所有步骤。
巡演记录仪¶
您还可以使用游览记录器轻松创建游览。为此,请单击入门教程视图上的 Record。启动后,该工具将记录您在 Odoo 中的所有交互。
创建的游览在入门游览视图中标记为**自定义**。这些游览还可以导出到 JavaScript 文件,准备放入您的模块中。
调试技巧¶
在浏览器中观察测试之旅¶
三种方法具有不同的权衡:
watch=True¶
通过测试套件在本地运行游览时,watch=True parameter can be added to the browser_js or start_tour 调用:
self.start_tour("/web", "your_tour_name", watch=True)
这将自动打开一个 Chrome 窗口,其中正在运行游览。
- 优点
如果游览具有 Python 设置/周围代码或多个步骤,则始终有效
完全自动运行(只需选择启动游览的测试)
事务性(*应该*始终可运行多次)
- 缺点
只在本地工作
仅当测试/游览可以在本地正确运行时才有效
debug=True¶
通过测试套件在本地运行游览时,debug=True parameter can be added to the browser_js or start_tour 调用:
self.start_tour("/web", "your_tour_name", debug=True)
这将自动打开一个全屏 Chrome 窗口,其中打开了开发工具并在游览开始时设置了调试器断点。该游览是使用 debug=assets 查询参数运行的。当抛出错误时,调试器会因异常而停止。
- 优点
与模式
watch=True具有相同的优点更容易调试步骤
- 缺点
只在本地工作
仅当测试/游览可以在本地正确运行时才有效
通过浏览器运行¶
也可以通过浏览器 UI 调用来启动测试之旅
odoo.startTour("tour_name");
在 javascript 控制台中,或通过在 URL 中设置“?debug=tests”来启用 tests mode。
- 优点
更容易运行
可以在生产或测试站点上使用,而不仅仅是本地实例
允许在“Onboarding”模式下运行(手动步骤)
- 缺点
难以用于涉及 Python 设置的测试之旅
根据旅游副作用,可能无法多次使用
小技巧
可以使用此方法来观察需要 Python 设置的游览或与之交互:
在相关游览开始之前添加 python 断点(
start_tourorbrowser_js调用)当断点被击中时,在浏览器中打开实例
进行游览
此时,浏览器将可以看到 Python 设置,并且游览将能够运行。
如果您还希望测试之后继续,您可能需要注释“start_tour` or ``browser_js`”调用,具体取决于游览的副作用。
browser_js 测试期间的屏幕截图和截屏视频¶
从命令行运行使用“HttpCase.browser_js”的测试时,Chrome 浏览器以无头模式使用。默认情况下,如果测试失败,则会在失败时截取 PNG 屏幕截图并写入
'/tmp/odoo_tests/{db_name}/screenshots/'
自 Odoo 13.0 起添加了两个新的命令行参数来控制此行为:--screenshots 和 --screencasts
内省/调试步骤¶
当尝试修复/调试游览时,屏幕截图(失败时)不一定足够。在这种情况下,查看某些步骤或每个步骤中发生的情况可能会很有用。
虽然这在“入门”中非常容易(因为它们主要由用户明确驱动),但在运行“测试”游览或通过测试套件运行游览时会更加复杂。在这种情况下,有两个主要技巧:
调试模式下的步骤属性“
break: true,”(debug=True)。这会在该步骤的开始处添加一个调试器断点。然后您可以在任何需要的地方添加您自己的。
- 优点
很简单
一旦您恢复执行,游览就会继续
- 缺点
由于所有 JavaScript 都被阻止,页面交互受到限制
调试模式下的步骤属性“
pause: true,”(debug=True)。游览将在该步骤结束时停止。这允许检查页面并与页面交互,直到开发人员准备好通过在浏览器控制台中键入 play(); 来恢复。
- 优点
允许与页面交互
没有无用的(对于这种情况)调试器 UI
具有“
run() { debugger; }”操作的步骤。这可以添加到现有步骤,也可以是新的专用步骤。一旦步骤的**触发器**匹配,执行将停止所有javascript执行。
- 优点
简单的
一旦您恢复执行,游览就会继续
- 缺点
由于所有 JavaScript 都被阻止,页面交互受到限制
尝试查找步骤中定义的目标元素后会触发调试器。
性能测试¶
查询计数¶
测试性能的方法之一是测量数据库查询。可以使用 --log-sql CLI 参数手动进行测试。如果要确定操作的最大查询数,可以使用集成在 Odoo 测试类中的 assertQueryCount() 方法。
with self.assertQueryCount(11):
do_something()