这是Odoo系列文章的第八篇,完整目录请见最好用的免费ERP系统Odoo 11开发指南
以下开发均假设读者已完成第七篇的代码,并且所有代码更新后均需自行更新方会在客户端看到变化。如未阅读该篇,请参考代码:Chapter 7
本文主要内容
- 生成服务器日志来帮助调试方法
- 使用 Odoo Shell 来交互式地调用方法
- 使用 Python 调试器来追踪方法的执行
- 使用 Python 单位测试来测试模块
- 运行服务器测试
- 使用 OCA维护的质量工具
生成服务器日志来帮助调试方法
服务器崩溃时使用日志可以帮助查看运行时的情况,下面介绍如何为现有方法添加日志。例如下面的代码中加入了日志语句,存储产品的库存等级到一个文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
from os.path import join # 1. 代码开头从 Python 标准库引用 logging 模块 import logging from odoo import models, api, exceptions EXPORTS_DIR = '/srv/exports' # 2.在定义模型类之前,为模块获取 logger _logger = logging.getLogger(__name__) class ProductProduct(models.Model): _inherit = 'product.product' @api.model def export_stock_level(self, stock_location): # 3. _logger.info('export stock level for %s', stock_location.name) products = self.with_context( location = stock_location.id ).search([]) products = products.filtered('qty_available') # 3. _logger.debug('%d products in the location', len(products)) fname = join(EXPORTS_DIR, 'stock_level.txt') try: with open(fname, 'w') as fobj: for prod in products: fobj.write('%s\t%f\n' % (prod.name, prod.qty_available)) except IOError: # 3. _logger.exception( 'Error while writing to %s in %s', 'stock_level.txt', EXPORTS_DIR) raise exceptions.UserError('unable to save file') |
小知识
__name__变量在模块引用时由 Python 解释器自动设置,名称为模块的全称。由 Odoo 自身的引用机制,插件模块 Python 会视为 odoo.addons 的包。所以假若代码在my_module/models/book.py文件中,__name__则为odoo.addons.my_module.models.book。这样有两个好处:
- Odoo logger(记录器)的全局日志配置会应用到我们的记录器上,这源自 logging 模块的等级结构
- 日志会带有完事模型路径作为前缀,这样有且于查找产生某一日志行的来源
上述的3中使用记录器来产生日志信息,按日志等级升序有 debug, info, warning, error 和 critical 这几种方法。所有这些方法都接受%替代符以及插入消息中的其它变量。%不必自己替换,日志模块足够智能,只在日志生成时替换。如果以 INFO 日志级别运行,DEBUG 日志就会避免进行这种长期看会消耗 CPU 的替换。
这里另一个有用的方法_logger.exception(),可用于异常处理器,消息以 ERROR 级别记录,栈的轨迹也会在应用日志中打印。
扩展知识
可以在命令行或配置文件中控制日志级别,主要有两种方式
- 全局控制日志级别,命令行中使用–log-level
- 为指定记录器设置日志级别,可使用–log-handler=prefix:level。这里的 prefix 为记录器名的一段路径,日志级别为DEBUG,INFO,WARNING,ERROR或CRITICAL。如果省略prefix,则为所有日志记录器设置默认级别。例:
1 |
python odoo.py --log-handler=odoo.addons.my_module:DEBUG |
- 在命令行中可多次指定–log-handler
- 也可以在 Odoo 实例的配置文件中设置日志处理器,这时使用逗号隔的 prefix:level 对。例:
1 |
log_handler = :ERROR,werkzeug:CRITICAL,odoo.service.server:INFO |
使用 Odoo Shell 来交互式地调用方法
Odoo的 Web 端主要面向终端用户,虽然可以通过调试模式进行开发调试,但这通常并不方便,于是有了命令行界面 Odoo Shell。接下来我们就通过命令行来调用上面的export_stock_level方法,
首先执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# 1.进入 Odoo Shell ./odoo-bin shell -c odoo.conf --log-level=error # 以上操作会初始化一些全局变量 # a.env为通过命令行或配置文件指定的数据库连接环境; # b.odoo是一个导入的 odoo 包,可以获取获取该包中的所有 python 模块 # c.openerp 是为向了向下兼容所保留的 odoo 的别名 # d.self 是一个 res.users 记录集,它包含一条与 env 连接的 Odoo 超级用户记录 # 2.获取product.product记录集 product = env['product.product'] # 3.获取主仓库位置记录 location_stock = env.ref('stock.stock_location_stock') # 以上两步使用 env 获取一条空记录集并通过 XML ID 查找记录 # 4.调用export_stock_level方法 product.export_stock_level(location_stock) # 5.退出前结束事务 env.cr.commit() # 以上步骤在这里并非必须,但如果要对数据进行修改并保存则需要执行此步。在进行 RPC 调用时 Odoo 会处理事务,但是在 shell 模式中需要自行调用 env.cr.commit()执行或 env.cr.rollback()回滚 |
使用 Python 调试器追踪方法执行
有时应用日志不足以帮助我们定位问题,于是我们还可以使用 Python 调试器(debugger)。我们依然使用以上的export_stock_level方法进行调试,仅需进行如下修改
1 2 3 4 5 |
@api.model def export_stock_level(self, stock_location): # 此处使用 set_trace()方法来打断点 import pdb; pdb.set_trace() ... |
首先使用如前所述同样的操作:
1 2 3 4 |
./odoo-bin shell -c odoo.conf --log-level=error product = env['product.product'] location_stock = env.ref('stock.stock_location_stock') product.export_stock_level(location_stock) |
此时进入如下Pdb 的命令窗口界面
1、输入 a 查看当前传入方法中的参数值
2、输入 list 查看代码执行到哪一行(箭头所指处)
3、输入 next 或 n 执行到下一行,类似于 PyCharm 中的单步调试
4、输入 p 变量名查看指定变量的当前值,如(p products 或 p fname)
5、输入!fname = ‘/tmp/stock_level.txt’来修改fname对应的变量值
6、输入return 或 r来执行到当前方法结束处
7、输入cont 或 c 恢复程序执行
使用 Python 调试器手动单步调试,有以下小贴士:
- 把日志级别降低避免输出泛滥,通常建议使用 ERROR 级别。对有些日志记录如需内容更为全面,可以在命令行中使用–log-handler选项
- 添加–workers=0来避免多进程所引发的断点多次调用
- 添加–max-cron-threads=0来取消 ir.cron 定时任务的执行,以避免单步调试时触发任务产生不需要的日志和其它副面作用。
pdb 命令很多都可以通过其首字母来执行,总结如下:
- h(elp):显示 pdb 命令的帮助
- a(rgs):显示当前函数/方法的参数值
- l(ist):以11行的源代码块显示,并且当前执行命令行居中显示,连续调用会在源代码中不断前进。并且可以起始和结束两个数字来指令显示区域
- p:打印变量
- pp:以美化的方式打印变量(对于列表和字母非常有用)
- u(p):移到调用栈中的上一级
- d(own):移到调用栈中的下一级
- n(ext):执行当前行代码
- s(tep):进入调用方法内部执行
- r(eturn):恢复当前方法的执行直到返回行
- c(ont(inue)):恢复程序执行到下一断点
- b(reak) <args>:创建新的断点并显示标识符,args 可以是如下的一种:
- 不跟参数:显示所有断点
- 行号:在当前文件指定行打断点
- 文件名:行号:在指定文件(在 sys.path 路径中搜索)指定行打断点
- 函数名:在指定函数/方法的第一行打断点
- tbreak <args>:与 break 相似,但是在执行完断点后将取消断点,所以在循环调用中不会重复触发
- disable bp_ip:根据指定的 ID 取消断点
- enable bl_id:根据指定的 ID 激活已取消断点
- j(ump) lineno:执行指定行号的代码,通常用于重新执行代码或跳过一些行
- (!) statement:执行 Python 语句,在不与 pdb 保留字冲突时可省略!,比如因为 a 是一条pdb命令,在定义一个名为 a 的变量时需在前面加上!
扩展知识
本例中我们使用 pdb.set_trace()方法来打断点,但有时我们无法在代码中进行修改,此时可以通过在 Odoo shell 中使用 pdb.runcall()来实现。比如实现同样的调试也可以这样:
这里我们使用了 Python 标准库 pdb,同时还有 ipdb 和 pudb 都可以作为 pdb 的替代,它们命令大同小异。当前我们也完全可以借助 IDE 来进行断点调试。
使用 Python 单位测试来测试模块
Odoo 支持多种测试插件模块的书写方式,如 YAML 测试和 Python 测试。不建议给新模块写 YAML 测试,因为在未来的更新中很有可能就被删除掉了,所以这里我们只讨论 Python 单元测试。
我们依然采用本系统文章中的 my_module 来进行演示,
1、创建一个 tests 模块
1 |
mkdir my_module/tests |
2、创建___init__.py 文件
1 |
from . import test_library |
3、创建 test_library.py
注:user_demo 用户在安装时勾选Load demostration data 时会自动添加,否则需要在debug 模式下点击 Settings > Technical > Sequences & Identifiers > External Identifiers进行添加,也可使用其它用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
from odoo.tests.common import TransactionCase class LibraryTestCase(TransactionCase): def setUp(self): # 使用 setUp 方法创建一本书 super(LibraryTestCase, self).setUp() demo_user = self.env.ref('base.user_demo') demo_user.groups_id |= self.env.ref('my_module.group_librarian') # sudo()用于改变 self.book_model 的环境用户,这可以保证不会使用跳过各种规则的管理用户 book_model = self.env['library.book'].sudo(demo_user) self.book = book_model.create( {'name': 'Test book', 'state': 'draft'} ) def test_change_draft_available(self): # 将状态由 draft 修改为 available self.book.change_state('available') self.assertEqual(self.book.state, 'available') def test_change_available_draft_no_effect(self): # 测试一个非法状态修改:由 available 改为 draft self.book.change_state('available') self.book.change_state('draft') self.assertEqual( self.book.state, 'draft', 'the state cannot change from %s to draft' % self.book.state ) |
注:test_change_available_draft_no_effect方法原书应为错误代码,修改如上
按照惯例测试模块名以 test_开头,以上 TrasactionCase 继承自 Python 标准库 unittest.TestCase,通过重载 setUp和 tearDown()方法来使用:
- setUp()方法初始化用于常规操作的 self.env 属性
- teardown()方法用于数据库事务回滚,以方便测试隔离
小贴士:如在测试代码中重新定义的以上两个方法后一定要记得调用 super()实现
测试通过 test 前缀的方法定义,方法运行前后会分别调用 setUp()和 tearDown(),在方法中则可以使用 unittest.TestCase 中的任意 assertion 方法:
Method | Checks that |
---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) | bool(x) is True |
assertFalse(x) | bool(x) is False |
assertIn(a, b) | a in b |
assertNotIn(a, b) | a not in b |
assertRaises(exc, fun, *args, **kwargs) | fun(*args, **kwargs) raises exc |
以上除 assertRaises 以外的方法都支持一个可选参数 msg,用于在 assertion为假时作为错误消息展示。assertRaises最好是用作上下文管理器,比如我们要测试修改记录抛出 UserError 异常,可以这么写
1 2 3 4 5 |
class TestCase(TransactionCase): # setUp method defines self.record def testWriteRaisesUserError(self): with self.assertRaises(UserError): self.record.write({'some_field': some_value}) |
如果传递给 assertRaises 的异常由代码块生成测试成功,否则失败。更多单元测试内容,参见官方标准库文档。
扩展知识
odoo.tests.common 模块中定义一些测试基类
- TransactionCase:每个测试方法单独运行,并在其后对数据库事务回滚。这表示一个测试方法所做的修改在另一个测试方法中不可见。可以使用传统的 setUp()和 tearDown()方法来进行初始化和清理
- SingleTransactionCase:所以的测试方法在同一个事务中运行,在最一个方法执行后回滚。如需对测试事例进行初始化和清理,需要继承 setUpClass()和 tearDownClass()方法。同时应记得添加@classmethod 类装饰器,否则在调用 super()时会出现一些奇怪的报错
- SavePointCase:它继承自 SingleTransactionCase 并在测试方法运行前创建一个数据库 SAVEPOINT,然后在运行完成后还原。好处在于可以独立运行测试而无需在测试间使用消耗系统的 setUp()方法重建所有数据,初始化通过 setUpClass()方法,然后每次测试后事务会还原到保存的状态
该模块还定义了两个装饰器 at_install(bool)和 post_install(bool),默认测试在插件模块初始化后和下一个插件模块运行之前运行,对应装饰器@at_install(True)和@post_install(False)。有时需要做出调整。比和 module_a 和 module_b 继承自相同模型,但彼此没有依赖,它们都需要添加必填字段(如field_a 和 field_b)的默认值到模型。在测试时创建新记录,如果在测试时两个模型都运行,则会出错,分别运行又正常运行。原因在于比如先加载module_a,在创建新记录时 field_b 的默认值未被计算,数据库对 field_b的 NOT NULL 约束会不让记录创建。解决方案是为两个模块测试方法中添加@at_install(False)和@post_install(True),这会强制测试在两个模块初始化完成后运行。
运行服务器测试
1 2 3 |
# 为模块添加 demo 数据 ./odoo-bin -c odoo.conf --without-demo=False --stop-after-init -i my_module ./odoo-bin -c odoo.conf --test-enable --log-level=error --stop-after-init -u my_module |
–test-enable 选项通知 Odoo 运行测试,–stop-after-init 标记在测试运行后停止实例,-u 更新指定模块。如果测试没有执行,则应检查模块测试数据有没有被激活,不确定的话可以执行:
1 |
UPDATE ir_module_module SET demo=true WHERE name='my_module'; |
也可以使用–log-level=error –log-handler=odoo.modules.loading:INFO运行测试,这样操作日志不会被输出,仅仅显示错误消息。
小贴士:Odoo 返回的状态为测试错误的次数,所以只有不是0,就表明测试失败,服务器日志中可以获取更多信息。
扩展知识
这种测试的方式最大的缺点是即使我们知道大部分代码都正常,但还是要测试整个模块,而且还要去更新整个模块。有一种简洁些的方式是可指定测试的内容(如 test_library.py)
1 |
./odoo-bin -c odoo.conf --log-level=error --stop-after-init --test-file myaddons/my_module/tests/test_library.py |
这样公会运行指定文件中所定义的测试,它不会更新模块,所以如果修改了字段或数据文件而没更新的话就不会生效。
小贴士:如果测试没有报错一定要保持怀疑的态度,这有可能是你自身的错误导致的,比如忘记在 tests/__init__.py 中导入测试文件,通常我们会在第一次运行时通过在第一行添加 assert False 强制错误以确定是否正常运行。
使用 OCA 维护的质量工具
OCA(Odoo Community Association)在 Github 上维护有大量 Odoo 项目,该机构通过 Travis CI来进行持续集成,本节展示如何使用 OCA维护者的 QA 工具。
1、访问https://travis-ci.org/并点击 Sign in with Github 进行登录(需进行授权)
2、点击右上角姓名处进入个人资料页,然后点击 Sync Account按钮将公开代码同步到 Travis 上
3、对需要使用 Travis on 的仓库点击滑块进行激活
4、在本地仓库添加.travis.yml 文件,填入如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
language: python sudo: false cache: apt: true directories: - $HOME/.cache/pip python: - "3.5" addons: apt: packages: - expect-dev # provides unbuffer utility - python-lxml # because pip installation is slow - python-simplejson - python-serial - python-yaml virtualenv: system_site_packages: true env: global: - VERSION="11.0" TESTS="0" LINT_CHECK="0" matrix: - LINT_CHECK="1" - TESTS="1" ODOO_REPO="odoo/odoo" - TESTS="1" ODOO_REPO="OCA/OCB" install: - git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} - travis_install_nightly script: - travis_run_tests after_success: - travis_after_tests_success |
5、push 到 GitHub 上,此时再到 travis-ci.org 页面上点击项目名,此时就会开始首次 build,如果代码符合 OCA 代码标准,就会显示为绿色,Alan 使用课程代码测试时日志中显示virtualenv 执行报错,因而这里先列出一个反例
当我们在仓库上使用 Travis CI时,Trvis 会在 GitHub 上注册一个钩子(hook),默认钩子会在每次向仓库分支 push 或 pull 请求时触发 Travis CI重建。这里使用的 Travis CI配置文件比较高阶,与https://github.com/OCA/maintainer-quality-tools中 sample_files 子目录中文件极其相近(此处移除了用于模块翻译的 transifex 配置),配置释意如下:
- addons:与 Odoo 的插件模块无关,它用于请求 Travis 在测试环境中安装一些 Ubuntu 包文件,这样我就无需从源码中安装 Python 相关包
- env:此处定义了环境变量和编译矩阵。维护者质量工具通过环境变量了解到需测试项,并且会对 env 逐行测试运行
- VERSION:需测试的 Odoo 版本
- LINT_CHECK:0表示不使用 flake8或 Pylint 测试,1则反之。在矩阵中,首次编译会进行 lint 检查,因为这样一旦不符合编码标准或 linter 发现错误都可以快速反馈
- TESTS:0作用于无需测试模块,1则反之
- ODOO_REPO:在 TESTS为1时测试 GitHub 仓库中的 Odoo,示例中使用了官方仓库以及社区补丁(OCB)进行编译, 如果不设置,仅使用官方仓库
- install:用于下载编译环境中的 maintainer-quality-tools 并调用 travis_install_nightly 工具,以在 Travis 上配置 Odoo
- script:从maintainer-quality-tools中调用 travis_run_test,这个脚本用于检测编译矩阵的环境变量并执行相应动作
- after_success:在script 部分成功运行后,travis_after_test_success 脚本会运行。在示例上下文中,脚本会用 https://coveralls.io检测模块的测试覆盖并在编译时生成报告
可能你对于这整个复杂的配置不感兴趣,而希望使用更为轻量级的工具,那么其中的 Pylint 和 Flake8都可以独立作为静态代码检测器来使用。
使用 Pylint 检测代码
Pylint 是一个 Python的静态代码检测器,还有一个针对Odoo 的项目叫作 pylint-odoo,安装方法为:
1 2 3 |
pip install --upgrade --pre pylint-odoo # 安装完成后可以通过指定模块路径来进行检测 pylint --load-plugins=pylint_odoo -e epylint myaddons/my_module/ |
此时会给出一长串建议如
1 |
Missing ./README.rst file. Template here: https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst (missing-readme) |
README文件地址为https://github.com/OCA/pylint-odoo/blob/master/README.rst,比如我们不需要 missing author的提示,在该文件中找对应的代码为 C8101,就可以执行时使用-d 或–disable 来不予以检查 (也可以在配置文件中进行该设置)
1 |
pylint --load-plugins=pylint_odoo -e epylint -d C8101 myaddons/my_module/ |
使用 Flake8检测代码
Flake8也是一个用于检测代码格式的常用工具,支持 Python 的文本编辑器或 IDE 通过都会提供运行 flake8的的方式,在不符合编辑规范时高亮提示,使用 pip 安装
1 |
pip install flake8 |
在项目根目录下创建.flake8文件,这里使用 OCA 的配置文件版本
1 2 3 4 5 6 |
# E123,E133,E226,E241,E242 are ignored by default by pep8 and flake8 # F811 is legal in odoo 8 when we implement 2 interfaces for a method # F999 pylint support this case with expected tests ignore = E123,E133,E226,E241,E242,F811,F601 max-line-length = 79 exclude = __unported__,__init__.py |
此时使用 flake8加文件或目录路径执行即可,更建议的做法是在编辑器中进行设置
1 |
flake8 myaddons/my_module/ |
本文参考代码:Chapter 8