Frappe Testing 测试框架深度指南:TestConfig、TestRunner 与测试发现机制解析
【免费下载链接】frappeLow code web framework for real world applications, in Python and Javascript项目地址: https://gitcode.com/GitHub_Trending/fr/frappe
导读
Frappe 作为面向真实业务场景的 Low-code Web 框架,其测试体系直接关系到每个应用的交付质量。本文以 frappe/testing/README.md 为核心骨架,系统讲解frappe.testing模块中TestConfig、TestRunner、TestResult三大核心类,以及discover_all_tests、discover_doctype_tests、discover_module_tests三个测试发现函数的设计与用法。读完本文,你将掌握如何以编程方式驱动 Frappe 测试、测试分类与执行优先级的工作机制、测试环境准备/清理的完整流程,并能通过bench run-tests命令的实际参数理解整个框架的运行脉络。
模块总览:一个完整的测试框架
frappe.testing是一个为 Frappe 应用提供完整测试能力的模块,覆盖测试发现(discovery)、执行(execution)、结果上报(reporting)与环境准备(environment setup)全流程。其核心能力可以从模块入口 frappe/testing/init.py 的文档字符串中确认,主要包括:
- TestConfig:用于定制测试执行的配置类;
- TestRunner:继承
unittest.TextTestRunner的主执行类,附带 Frappe 特有的功能; - TestResult:自定义测试结果类,提供改进的输出格式与日志记录;
- discover_all_tests / discover_doctype_tests / discover_module_tests:分别用于发现指定 Frappe 应用中全部测试、特定 DocType 的测试、特定模块的测试。
该模块通常由 Frappe 的 CLI 命令(bench run-tests)间接调用,但同时也完全支持在代码中以编程方式驱动自定义测试执行场景。
编程式用法:三行代码跑通测试
原文档给出的最小可用示例完整如下,这也是理解整个框架的最佳切入点:
from frappe.testing import TestConfig, TestRunner, discover_all_tests config = TestConfig(failfast=True, verbose=2) runner = TestRunner(cfg=config) discover_all_tests(['my_app'], runner) runner.run()这段代码的执行链路是:
- 构造配置:
TestConfig(failfast=True, verbose=2)创建配置对象,开启失败即停; - 创建运行器:
TestRunner(cfg=config)初始化执行器,注意TestRunner的构造签名中cfg: TestConfig是关键字参数,runner.py 中将其传入unittest.TextTestRunner.__init__,并把failfast等配置自动透传给底层 unittest 执行器; - 发现测试:
discover_all_tests(['my_app'], runner)扫描my_app应用的所有测试模块并装载进 runner 内部的per_app_categories数据结构; - 执行测试:
runner.run()实际运行已发现的测试套件。
需要说明:原示例中的verbose=2在当前的TestConfig数据类定义(config.py)中并不存在,verbosity 实际由TestRunner(verbosity=...)或 CLI 层控制(见下文main()中按日志级别动态设定 verbosity 的逻辑)。在实际编程调用时,建议显式传入verbosity参数。
TestConfig 与 TestParameters:两层配置模型
TestConfig:运行器级配置
config.py 中定义了TestConfig数据类,其字段与含义如下:
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
profile | bool | False | 是否启用 cProfile 性能剖析 |
failfast | bool | False | 遇到首个错误/失败即停止 |
tests | tuple | () | 需要执行的特定测试方法名集合,用于过滤 |
case | str | None | None | 指定测试模块中的特定 TestCase 类 |
pdb_on_exceptions | tuple | None | None | 指定需要在异常时进入 pdb 调试的异常类型元组 |
selected_categories | list[str] | [] | 仅运行选定的测试分类(如 unit/integration) |
skip_before_tests | bool | False | 是否跳过 before_tests hooks 的执行 |
test_service | TestService | None | None | 只运行声明了所需外部服务的测试(如 web-server) |
其中pdb_on_exceptions会驱动 runner.py 的_apply_debug_decorators方法:当配置了该字段时,运行器会遍历套件中的每个测试方法,用debug_on上下文管理器装饰器(定义于 frappe/tests/classes/context_managers.py)包裹指定异常类型,从而在异常处自动进入调试器。
TestParameters:轻量模式的参数模型
同一文件中还定义了TestParameters数据类,服务于--lightmode轻量测试路径(见 commands/testing.py),字段包括site、app、module、doctype、module_def、verbose、tests、force、profile、junit_xml_output、doctype_list_path、failfast、case、test_service。它记录了用户从命令行传入的原始参数,由FrappeTestLoader(loader.py)消费,按tests > doctype > module > app的优先级生成测试套件。
TestRunner:分类执行与剖析
测试分类与执行优先级
TestRunner继承自unittest.TextTestRunner,在 runner.py 中定义了分类优先级字典:
CATEGORY_PRIORITIES = { "unit": 1, "integration": 2, "functional": 3, }测试发现阶段会将每个测试按类别归入runner.per_app_categories[app][category](一个双层defaultdict,值为unittest.TestSuite)。执行阶段iterRun()会:
- 对每个应用,按
CATEGORY_PRIORITIES对分类排序(未登记的分类按float("inf")排在最后); - 跳过没有实际测试用例的分类(
_has_tests通过迭代套件判断); - 调用
_prepare_category为分类准备环境; - 在
_profile()上下文中逐分类 yield 套件交给上层执行。
分类环境准备
_prepare_category(runner.py)使用分发器模式:integration分类交给IntegrationTestPreparation实例,old-frappe-test-class-category交给get_compat_frappe_test_case_preparation(cfg)(兼容旧的FrappeTestCase写法,相关 deprecation 警告在 discovery 阶段给出),未知分类仅记录 debug 日志不做额外准备。
性能剖析
_profile()上下文管理器在cfg.profile=True时启用cProfile.Profile(),结束后用pstats按cumulative排序输出统计,帮助定位耗时测试。
测试发现:三大发现函数
discovery.py 提供三个发现函数,均以runner作为装载目标并返回 runner,便于链式调用。三者都用@debug_timer装饰器(utils.py)记录耗时日志。
discover_all_tests:全应用扫描
def discover_all_tests(apps: list[str], runner) -> "TestRunner":实现要点:
- 接受字符串或列表,内部统一转为列表;
- 通过
os.walk(frappe.get_app_path(app))遍历应用目录,跳过隐藏目录及node_modules、locals、public、__pycache__; - 跳过 doctype boilerplate 目录;
- 收集所有以
test_开头、以.py结尾且文件名不是test_runner.py的文件; - 将文件路径转换为模块名后调用
_add_module_tests装载。
discover_doctype_tests:按 DocType 精确发现
def discover_doctype_tests(doctypes: list[str], runner, app: str, force: bool = False) -> "TestRunner":实现要点:
- 通过
frappe.db.get_value("DocType", doctype, "module")查询 DocType 所属模块,查不到则抛出TestRunnerError("Invalid doctype ..."); - 通过
Module Def校验 DocType 归属应用:若未指定app则自动采用 DocType 所属应用;若指定了不匹配的应用则抛出TestRunnerError(防止跨应用误跑); - 使用
frappe.modules.utils.get_module_name(doctype, module, "test_")拼出测试模块名(例如test_<doctype>); force为真时删除 DocType 记录(用于重新生成测试数据)。
discover_module_tests:按模块精确发现
def discover_module_tests(modules: list[str], runner, app: str) -> "TestRunner":实现要点:从模块名(如my_app.tests.test_something)中取首段作为模块所属应用,并做与 doctype 路径一致的应用归属校验,然后直接importlib.import_module装载该模块。
_add_module_tests:分类的最终裁决者
这是三大发现函数的公共落点(discovery.py),它完成:
- 导入模块并加载测试:若配置了
case,用loadTestsFromTestCase(getattr(module, case))只加载指定测试类;否则loadTestsFromModule加载全模块; - 方法级过滤:
cfg.tests中指定的测试方法名才保留; - 服务过滤:
requires_selected_test_service与apply_test_service_skips(来自 frappe/tests/utils/test_capabilities.py)处理测试的外部服务依赖; - 分类判定:
IntegrationTestCase→integration,UnitTestCase→unit,其余归入unspecified-category;若类直接继承自FrappeTestCase,则归入old-frappe-test-class-category并触发 v17 弃用警告; - 分类过滤:
selected_categories非空时仅保留匹配分类的测试,最终写入runner.per_app_categories[app][category]。
注意IntegrationTestCase与UnitTestCase从 frappe/tests/init.py 导出,该文件同时定义了global_test_dependencies = ["User"],这是环境准备阶段创建全局测试记录的依据。
TestResult:面向可读性的结果输出
result.py 中自定义的TestResult继承unittest.TextTestResult,核心特性:
- 着色输出:通过
click.style为不同结果上色——成功✔(绿色)、错误/失败✖(红色)、跳过=(白色); - 慢测试标记:
SLOW_TEST_THRESHOLD = 2秒,超过阈值的方法在输出后追加红色耗时(如(2.314s)),计时基于time.monotonic(); - 按测试类分组展示:
startTest中检测到测试类切换时打印类名,并输出该类 setUpClass/tearDownClass 期间被缓冲的 stdout/stderr(▹/▸前缀),还打印_newly_created_test_records记录的测试数据创建统计; - 缓冲输出管理:
startTestRun/stopTestRun/startTest/stopTest接管 stdout/stderr 重定向,保证模块级与类级输出排在首个测试之前、单个测试输出紧随该测试; - traceback 局部变量:
tb_locals = True让失败 traceback 附带局部变量,便于定位; - 错误列表:
printErrors以ERROR/FAIL标签(红底)分组打印错误详情。
此外还保留了FrappeTestResult(较精简的变体),供--lightmode路径使用。
环境准备与清理:测试前的三件套
environment.py 负责测试环境的生命周期管理:
_initialize_test_environment(site, config):frappe.init(site)初始化站点;未连接数据库时frappe.connect();随后_disable_scheduler_if_needed()关闭调度器(并记录用户原本的开关状态);frappe.clear_cache()清缓存;toggle_test_mode(True)进入测试模式,并依据日志级别设置frappe.flags.print_messages与frappe.flags.tests_verbose;_cleanup_after_tests():若调度器原本由框架关闭(非用户显式关闭),则恢复enable_scheduler();frappe.db.commit()结束事务并再次清缓存;IntegrationTestPreparation:集成测试分类执行前的准备器——除非skip_before_tests=True,否则执行该应用的before_testshooks(frappe 框架自身的before_tests = "frappe.utils.install.before_tests"定义于 frappe/hooks.py);随后加载{app}.tests模块中的global_test_dependencies列表,逐个调用make_test_records(doctype, commit=True)创建全局测试记录(frappe 应用默认依赖User)。
CLI 落地:bench run-tests 如何串联整个框架
frappe.testing的实际主战场是 CLI。bench run-tests命令定义在 frappe/commands/testing.py,其完整选项如下:
| 选项 | 作用 |
|---|---|
--app | 指定要测试的 App |
--doctype | 指定 DocType |
--module-def | 运行某个 Module Def 下所有 DocType 的测试 |
--case | 选择特定 TestCase |
--doctype-list-path | 指向含 DocType 列表的.txt文件(如erpnext/tests/server/agriculture.txt) |
--test | 指定具体测试(可多次使用) |
--module | 在指定模块内运行测试 |
--debug | 关闭缓冲,在 breakpoint 或异常时进入 pdb |
--profile | 启用性能剖析 |
--coverage | 启用覆盖率收集 |
--skip-before-tests | 不运行 before_tests hooks |
--junit-xml-output | 输出 JUnit XML 报告到指定路径 |
--failfast | 首个错误/失败即停止 |
--test-category | unit/integration/all,默认all |
--test-service | 只运行声明了指定外部服务的测试 |
--lightmode | 跳过所有 before 测试准备,走轻量执行路径 |
其核心执行流程(main(),testing.py):
- 前置检查:
allow_tests站点配置或 CI 环境变量为真才允许运行,否则提示bench --site {site} set-config allow_tests true开启; - 互斥校验:
--doctype、--doctype-list-path、--module-def、--module四者互斥,同时指定会报click.UsageError; - 构造
TestConfig(profile/failfast/tests/case/pdb_on_exceptions/selected_categories/skip_before_tests/test_service一一映射); _initialize_test_environment准备环境;- 按优先级分支调用发现函数:
doctype或doctype_list_path→discover_doctype_tests;module_def→ 先查该 Module Def 下所有可导入测试的非子表 DocType 再批量发现(_run_module_def_tests);module→discover_module_tests;否则 →discover_all_tests(frappe.get_installed_apps(), runner); - 遍历
runner.iterRun()逐应用逐分类执行,可配合xmlrunner输出 JUnit XML(--junit-xml-output); finally中_cleanup_after_tests()兜底清理。
若启用--lightmode,则走run_tests_in_light_mode分支:不执行 before_tests hooks 与全局测试记录创建,直接用FrappeTestLoader+FrappeTestResult快速执行,适合对测试基础设施依赖极小的场景。
此外 testing.py 还提供bench run-parallel-tests(按 build 分片并行执行,支持 Orchestrator 模式)与bench run-ui-tests(封装 Cypress 运行 UI 测试,自动安装 cypress、cypress-drag-drop、cypress-real-events 等依赖)两个配套命令。
小结
frappe.testing是一套"unittest 之上、面向 Frappe 业务"的完整测试框架:TestConfig用数据类集中管理执行策略,TestRunner通过分类优先级、环境准备分发器、pdb 注入与 cProfile 剖析扩展标准 unittest,三大发现函数覆盖"全应用 / 按 DocType / 按模块"三种粒度并内置应用归属校验,TestResult让测试输出具备颜色、分组与耗时提示,而environment.py则保证了调度器、缓存、hooks 与全局测试记录在测试前后的确定性。理解这套机制后,无论是直接调用 API 编写自定义测试工具,还是通过bench run-tests各种选项组合精确控制测试范围,都能得心应手。
【免费下载链接】frappeLow code web framework for real world applications, in Python and Javascript项目地址: https://gitcode.com/GitHub_Trending/fr/frappe
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考