
本文介绍在 Streamlit 应用单元测试中有效模拟函数的方法,核心是避免直接执行模块顶层代码,改用 AppTest.from_function() 配合依赖注入,确保 @patch 能正确拦截目标函数调用。
本文介绍在 streamlit 应用单元测试中有效模拟函数的方法,核心是避免直接执行模块顶层代码,改用 `apptest.from_function()` 配合依赖注入,确保 `@patch` 能正确拦截目标函数调用。
在 Streamlit 测试中,直接使用 AppTest.from_file("foo.py") 会立即执行模块全局作用域中的代码(如 dialog_func() 调用),此时 patch 尚未生效——因为补丁是在 with patch(...) 块内才被激活,而模块导入时已调用 dummy_func() 并返回 'foo',导致模拟完全失效。
✅ 正确做法是重构应用入口:将主逻辑封装为可调用函数(如 main()),并通过 AppTest.from_function() 显式传入待测函数及依赖项,实现可控的、可注入的执行流程。
以下是推荐的重构与测试方案:
1. 修改 foo.py —— 引入显式入口与依赖参数
import streamlit as st
def dialog_func(dummy_func):
"""将依赖作为参数传入,支持运行时替换"""
result = dummy_func()
st.write(result)
def dummy_func():
return 'foo'
def main(dialog_func=dialog_func, dummy_func=dummy_func):
# 注意:此处不直接调用 dialog_func(),而是通过参数传入并调用
dialog_func(dummy_func)✅ 关键改进:
dialog_func显式接收dummy_func作为参数,解耦硬编码依赖;main()函数作为唯一可测试入口,避免模块级副作用;- 默认参数保留原有行为,不影响正常运行。
2. 编写 test_foo.py —— 使用 from_function + patch 组合
from unittest.mock import patch
import foo
from streamlit.testing.v1 import AppTest
def test_foo():
with patch("foo.dummy_func") as mock_dummy_func:
mock_dummy_func.return_value = "bar"
# 传入被测函数及其依赖(mock 已就绪,patch 在上下文中生效)
at = AppTest.from_function(
foo.main,
args=(foo.dialog_func, mock_dummy_func) # 注入 mock 实例
).run()
assert at.markdown[0].value == "bar"⚠️ 注意事项:
patch("foo.dummy_func")的路径必须与被调用方实际导入路径一致(即foo.py内部调用的是dummy_func(),所以 patchfoo.dummy_func);- 不要再用
from_file(),否则无法控制执行时机;- 若
dialog_func本身也需模拟,同样可通过参数注入或独立 patch;- Streamlit 测试中
st.write()会生成st.Markdown元素,因此断言使用at.markdown[0].value。
总结:Streamlit 单元测试中的函数模拟失败,根源在于模块执行时机与 patch 生效时机错位。采用「函数式入口 + 参数化依赖 + AppTest.from_function」是官方推荐且稳定可靠的模式,既保持代码可读性,又为测试提供充分的控制力。对于复杂应用,还可进一步结合 pytest.fixture 或 streamlit.testing 的 AppTest.session_state 等能力构建更健壮的测试体系。


















