ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

marimo 笔记本测试实战:pytest 单元测试、doctest 文档测试与测试执行机制解析

marimo 笔记本测试实战:pytest 单元测试、doctest 文档测试与测试执行机制解析 marimo 笔记本测试实战:pytest 单元测试、doctest 文档测试与测试执行机制解析【免费下载链接】marimoA reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor.项目地址: https://gitcode.com/GitHub_Trending/ma/marimo本文基于 marimo 仓库中的examples/testing测试示例目录,系统讲解两种在 marimo 笔记本中编写与运行测试的完整方案:用pytest对笔记本做单元测试,以及用 Python 标准库doctest对 docstring 代码片段做文档测试。读完本文,你将掌握测试笔记本的两种写法、测试单元(cell)的识别规则、pytest fixture 在 marimo 中的使用边界,以及 marimo 编辑器内响应式自动跑测试这一行为的源码级实现机制。为什么 marimo 笔记本可以被常规工具测试marimo 笔记本与普通 Python 脚本的本质区别只在于它使用了marimo.App()与app.cell/app.function装饰器来组织代码——它本身仍然是一个纯 Python 文件。这带来一个直接结论:所有面向 Python 的测试工具(pytest、doctest、coverage、lint 工具)都可以原样作用于笔记本文件,不需要额外的测试框架转换层。examples/testing目录下的 README(examples/testing/README.md)正是围绕这一点给出两个基础示例:方案示例文件运行方式用 pytest 测试笔记本test_with_pytest.pypytest test_with_pytest.py用 doctest 测试 docstring 代码片段running_doctests.py作为笔记本直接运行官方文档 docs/guides/testing/index.md 对这两类测试的总结是:Because marimo notebooks are stored as Python, test them like any other Python program.(因为 marimo 笔记本以 Python 形式存储,像测试任何 Python 程序一样测试它们即可)。方案一:用 pytest 测试笔记本最小可运行示例完整阅读 examples/testing/test_with_pytest.py,其全部内容如下:import marimo __generated_with 0.23.9 app marimo.App() app.function def inc(x): return x 1 app.cell def test_answer(): assert inc(3) 5, This test fails return app.cell def test_sanity(): assert inc(3) 4, This test passes return if __name__ __main__: app.run()这个文件演示了三个关键写法:被测对象放在app.function中。inc是一个由 marimo 管理的纯函数,测试 cell 可以直接引用它,无需关心 import 与依赖;测试 cell 以test_前缀命名 cell 函数。cell 名test_answer与test_sanity使这两个 cell 被识别为测试单元;一个 cell 里写一个断言。失败与通过的测试各占一个 cell,便于定位。在命令行执行:pytest test_with_pytest.py预期结果:test_sanity通过,test_answer失败(断言inc(3) 5不成立),pytest 会给出标准的失败报告。这正是 docs/guides/testing/pytest.md 中Testing at the command-line一节的用法——对任何笔记本文件直接pytest my_notebook.py,pytest 会自动发现其中所有名字以test_开头的 cell,或仅包含test_函数与Test类的 cell。测试 cell 的识别规则marimo 对哪些 cell 是测试 cell有明确而保守的判定,来自官方 pytest 指南:名字以test_开头的cell 函数被视为测试;cell整体只包含测试代码(名字以test_开头的函数、名字以Test开头的类、或带pytest.fixture装饰的函数)时,该 cell 会被测试运行器执行;如果 cell 里混入了其他内容(辅助函数、常量、变量、import 等),该 cell 会被跳过——官方建议把辅助函数移到单独的 cell。test_with_pytest.py中的两个测试 cell 恰好都只含assert语句,因此会被完整执行;而app.function def inc(x)所在 cell 不是测试,它只负责提供被测函数。编辑器内的响应式测试(reactive tests)除了命令行,marimo 在编辑模式下还会自动发现并执行笔记本内的测试。这一行为的开关是配置项runtime.reactive_tests,其默认值为True——见 marimo/_config/config.py 中 runtime 默认配置块:reactive_tests: True,该配置项的作用说明(同文件 docstring):reactive_tests: ifTrue, marimo will automatically run pytest on cells containing only test functions and test classes.如果不需要编辑器自动跑测试,可在配置文件中关闭runtime.reactive_tests。从源码结构看,这条响应式测试链路的挂载点在 marimo/_runtime/kernel_lifecycle.py 的_build_hooks中:if is_edit_mode and user_config[runtime].get(reactive_tests, False): hooks.add_post_execution(attempt_pytest, Priority.LATE)也就是说,只有编辑模式(is_edit_mode)下才会注册attempt_pytest这个cell 执行后钩子(定义于 marimo/_runtime/runner/hooks_post_execution.py);marimo run(非编辑模式)不会触发自动测试。当某个测试 cell 重新执行后,marimo 会调用 pytest 对该 cell 内容做一轮测试,并把结果呈现回编辑器界面。底层实现:marimo 如何把 cell 骗过 pytestpytest 通过静态扫描模块来收集测试,它看到的笔记本文件里只有app.cell装饰的函数,而不是真正的可收集测试对象。marimo 在 marimo/_ast/pytest.py 中用 AST 改写的方式解决了这个问题,核心机制包括:1. 顶层test_cell 的签名重写(wrap_fn_for_pytest)当 cell 本身以test_开头时,marimo 解析 cell 函数的源码 AST,生成一个桩函数(scaffold):# 生成结果的示意形态 def name_of_fn_passed(vars_ending_in_fixture, ...) - Any: return cell(vars_ending_in_fixturevars_ending_in_fixture)这个桩函数把 cell 的参数中以_fixture结尾的名字视为 pytest fixture 注入点(见 marimo/_ast/pytest.py 的wrap_fn_for_pytest:fixtures [arg for arg in args if arg.endswith(_fixture)])。这样 pytest 在收集时看到的是一个签名为(fixture_name,)的普通测试函数,调用它时才会真正执行对应 cell 的__call__。2. 多测试 cell 的类封装(build_test_class)如果一个测试 cell 内部定义了多个测试函数或测试类,marimo 会在模块层动态构造一个名为MarimoTestBlock_n的类(常量MARIMO_TEST_STUB_NAME MarimoTestBlock,见 marimo/_ast/pytest.py),把 cell 内的每个test_函数包装为staticmethod、每个Test*类封装为嵌套测试类,并注入到笔记本模块的命名空间中供 pytest 收集。官方文档示例的输出中就出现了这种形态:test_notebook.py::MarimoTestBlock_0::test_parameterized[3-4] PASSED test_notebook.py::MarimoTestBlock_0::test_parameterized[4-5] PASSEDbuild_test_class的 docstring 特别说明了一个鲁棒性设计:每个测试都是符号化解析的,某个测试定义即使写错了,也不会破坏整个测试套件的定义或阻止套件运行。3. fixture 的解析方式marimo 支持pytest.fixture与pytest.mark.parametrize等装饰器(is_pytest_decorator、has_fixture_decorator等函数通过 AST 识别pytest.*装饰器)。但存在一条重要限制(来自官方 pytest 指南的 Fixture Limitations):在一个 cell 中定义的 fixture 不能被另一个 cell 中的测试使用。原因是 pytest 收集测试是静态的:它只解析笔记本文件、不执行。因此 pytest 只能看到模块级 fixture(来自conftest.py或 import 的模块)以及与被测测试同作用域定义的 fixture。marimo 的 cell 执行顺序由运行时依赖图决定,静态分析无法得知哪些 fixture 会可用;而且仅为发现 fixture 就完整运行一遍笔记本代价过高。官方推荐的两种写法:在笔记本的setup cell(with app.setup:)中 import fixture,使其进入模块级作用域;或者把 fixture 与被测测试放在同一个 cell中(如 class-scope fixture 与test_方法定义在同一 cell 内);最通用的做法仍是使用标准的conftest.py。如果 fixture 装饰器在求值时失败,marimo 会让对应测试抛出ValueError,并提示 Consider exposing relevant variables in app.setup(见 marimo/_ast/pytest.py 的_make_fails)。命令行 pytest 与响应式测试的分工总结维度命令行pytest my_notebook.py编辑器响应式测试触发方式手动执行 pytestcell 执行后自动触发(编辑模式 reactive_testsTrue)覆盖范围全部test_cell 与纯测试 cell最近变更/执行的测试 cell适用场景CI、完整回归开发时即时反馈方案二:用 doctest 测试 docstring 代码片段对于带文档字符串的函数,Python 标准库 doctest 指向的完整示例是 examples/testing/running_doctests.py:import marimo __generated_with 0.19.7 app marimo.App(widthmedium) app.cell def _(): import marimo as mo return (mo,) app.function def euclid_mcd(a: int, b: int) - int: Return the MCD between positive a, b. euclid_mcd(42, 24) 6 euclid_mcd(24, 42) 6 euclid_mcd(42, 42) 42 assert a 0 assert b 0 if a b: a, b b, a if (a ! b): r a - b return euclid_mcd(b, r) return a app.cell def _(mo): # Include a reference to each function to test euclid_mcd import doctest failures, success doctest.testmod(verboseTrue) mo.md(fSuccess: {success}, Failures: {failures}) return if __name__ __main__: app.run()这个示例有三个值得注意的技术点:被测函数放在app.function中,docstring 内写交互示例。euclid_mcd(欧几里得求最大公约数)的 docstring 包含 3 组输入/输出对,覆盖非对称参数(42, 24)与(24, 42)以及相等情况(42, 42);运行 doctest 的 cell 必须显式引用被测函数。注意# Include a reference to each function to test注释下单独一行写着euclid_mcd——marimo 的 cell 依赖分析基于源码中出现的名字,若 cell 不引用euclid_mcd,该 cell 与函数之间就没有依赖边,doctest 运行时可能拿不到这个函数。把对函数的显式引用作为依赖锚点,是 marimo 笔记本中使用跨 cell 定义对象的通用技巧;测试结果回显为 Markdown。doctest.testmod(verboseTrue)返回(failures, success)计数,示例通过mo.md(...)把Success: N, Failures: M渲染到笔记本界面,失败的片段会以**test**形式逐条打印在标准错误流中。由于笔记本就是 Python 程序,你也可以在外部直接对该文件跑python -m doctest风格的检查,或者把doctest.testmod放进一个test_cell 里交给 pytest 收集——两种方式都成立。小结与延伸阅读marimo 的测试策略可以概括为一句话:不造新框架,复用 Python 生态既有工具。单元/集成测试:给 cell 命名test_*,用 pytest 从命令行或编辑器响应式钩子执行;底层由 marimo/_ast/pytest.py 的 AST 桩函数与MarimoTestBlock类封装完成对 pytest 收集器的适配;文档测试:用doctest.testmod直接验证app.function的 docstring 示例;开关与默认值:runtime.reactive_tests默认True,可在配置中关闭编辑器的自动测试行为;注意事项:纯测试 cell 与混合 cell 的识别差异、跨 cell fixture 不可见的静态收集限制。相关仓库路径索引:路径内容examples/testing/README.md本文主参考:两个测试示例的说明examples/testing/test_with_pytest.pypytest 测试笔记本示例examples/testing/running_doctests.pydoctest 文档测试示例docs/guides/testing/pytest.mdpytest 指南:识别规则、fixture 限制、完整输出示例docs/guides/testing/doctest.mddoctest 指南marimo/_ast/pytest.py测试 cell → pytest 收集对象的 AST 转换实现marimo/_runtime/kernel_lifecycle.py响应式测试钩子attempt_pytest的注册点marimo/_config/config.pyreactive_tests默认值【免费下载链接】marimoA reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor.项目地址: https://gitcode.com/GitHub_Trending/ma/marimo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表