Get started with app testing
本指南将涵盖一个简单的示例,展示如何在项目中构建测试以及如何使用pytest
执行它们。在了解了大局之后,继续阅读以学习应用程序测试基础:
- 初始化和运行模拟应用程序
- 检索元素
- 操作小部件
- 检查结果
Streamlit的应用程序测试框架并不绑定任何特定的测试工具,但我们将在示例中使用pytest
,因为它是最常见的Python测试框架之一。要尝试本指南中的示例,请确保在开始之前将pytest
安装到您的Streamlit开发环境中:
pip install pytest
pytest
A simple testing example with
本节解释了如何使用pytest
构建和执行一个简单的测试。有关pytest
的全面介绍,请查看Real Python的指南Effective Python testing with pytest。
pytest
is structured How
pytest
使用一种命名约定来方便地执行测试文件和函数。将您的测试脚本命名为 test_
或
的形式。例如,您可以使用 test_myapp.py
或 myapp_test.py
。在您的测试脚本中,每个测试都写成一个函数。每个函数的名称都以 test
开头或结尾。在本指南的示例中,我们将所有测试脚本和测试函数的前缀都设置为 test_
。
你可以在一个测试脚本中编写任意数量的测试(函数)。当在目录中调用pytest
时,该目录中的所有test_
文件都将用于测试。这包括子目录中的文件。这些文件中的每个test_
函数都将作为测试执行。你可以将测试文件放在项目目录中的任何位置,但通常会将测试收集到指定的tests/
目录中。有关其他结构和执行测试的方法,请查看pytest
文档中的如何调用pytest。
Example project with app testing
考虑以下项目:
myproject/
├── app.py
└── tests/
└── test_app.py
主应用程序文件:
"""app.py"""
import streamlit as st
# Initialize st.session_state.beans
st.session_state.beans = st.session_state.get("beans", 0)
st.title("Bean counter :paw_prints:")
addend = st.number_input("Beans to add", 0, 10)
if st.button("Add"):
st.session_state.beans += addend
st.markdown(f"Beans counted: {st.session_state.beans}")
测试文件:
"""test_app.py"""
from streamlit.testing.v1 import AppTest
def test_increment_and_add():
"""A user increments the number input, then clicks Add"""
at = AppTest.from_file("app.py").run()
at.number_input[0].increment().run()
at.button[0].click().run()
assert at.markdown[0].value == "Beans counted: 1"
让我们快速浏览一下这个应用程序中的内容并在运行之前进行测试。主应用程序文件(app.py
)在渲染时包含四个元素:st.title
、st.number_input
、st.button
和st.markdown
。测试脚本(test_app.py
)包括一个测试(名为test_increment_and_add
的函数)。我们将在本指南的后半部分详细介绍测试语法,但这里简要解释一下这个测试的作用:
- 初始化模拟应用程序并执行第一个脚本运行。
at = AppTest.from_file("app.py").run()
- 模拟用户点击加号图标(添加)以增加数字输入(并重新运行生成的脚本)。
at.number_input[0].increment().run()
- 模拟用户点击“添加”按钮(以及由此产生的脚本重新运行)。
at.button[0].click().run()
- 检查最后是否显示了正确的消息。
assert at.markdown[0].value == "Beans counted: 1"
断言是测试的核心。当断言为真时,测试通过。当断言为假时,测试失败。一个测试可以有多个断言,但保持测试的紧密聚焦是良好的实践。当测试专注于单一行为时,更容易理解和应对失败。
pytest
Try out a simple test with
- 将上述文件复制到一个新的“myproject”目录中。
- 打开终端并切换到您的项目目录。
cd myproject
- 执行
pytest
:pytest
测试应该成功执行。您的终端应该显示如下内容:

通过在项目目录的根目录执行pytest
,所有以测试前缀(test_
)开头的Python文件将被扫描以查找测试函数。在每个测试文件中,每个以测试前缀开头的函数将作为测试执行。pytest
然后统计成功并列出失败。你也可以指示pytest
仅扫描你的测试目录。例如,从项目目录的根目录执行:
pytest tests/
pytest
Handling file paths and imports with
测试脚本中的导入和路径应该相对于调用pytest
的目录。这就是为什么测试函数使用路径app.py
而不是../app.py
,即使应用程序文件位于测试脚本的上一个目录中。通常你会从包含主应用程序文件的目录调用pytest
。这通常是项目目录的根目录。
此外,如果在调用pytest
的目录中存在.streamlit/
,则其中的任何config.toml
和secrets.toml
都可以被你的模拟应用访问。例如,你的模拟应用将能够访问以下常见设置中的config.toml
和secrets.toml
文件:
项目结构:
myproject/
├── .streamlit/
│ ├── config.toml
│ └── secrets.toml
├── app.py
└── tests/
└── test_app.py
在test_app.py
中的初始化:
# Path to app file is relative to myproject/
at = AppTest.from_file("app.py").run()
执行测试的命令:
cd myproject
pytest tests/
Fundamentals of app testing
既然你已经了解了pytest
的基础知识,让我们深入探讨如何使用Streamlit的应用测试框架。每个测试都从初始化和运行你的模拟应用开始。额外的命令用于检索、操作和检查元素。
在下一页,我们将超越基础并涵盖更高级的场景,如处理机密、会话状态或多页面应用。
How to initialize and run a simulated app
要测试一个Streamlit应用程序,您首先需要使用应用程序中一个页面的代码初始化AppTest
的实例。有三种方法可以初始化一个模拟应用程序。这些方法作为类方法提供给AppTest
。我们将重点介绍AppTest.from_file()
,它允许您提供应用程序页面的路径。这是在应用程序开发过程中构建自动化测试的最常见场景。AppTest.from_string()
和AppTest.from_function()
可能对一些简单或实验性的场景有帮助。
让我们继续上面的example from above。
回想一下测试文件:
"""test_app.py"""
from streamlit.testing.v1 import AppTest
def test_increment_and_add():
"""A user increments the number input, then clicks Add"""
at = AppTest.from_file("app.py").run()
at.number_input[0].increment().run()
at.button[0].click().run()
assert at.markdown[0].value == "Beans counted: 1"
查看测试函数中的第一行:
at = AppTest.from_file("app.py").run()
这做了两件事,相当于:
# Initialize the app.
at = AppTest.from_file("app.py")
# Run the app.
at.run()
AppTest.from_file()
返回一个 AppTest
的实例,该实例使用 app.py
的内容进行初始化。.run()
方法用于首次运行应用程序。查看测试时,请注意 .run()
方法手动执行每个脚本运行。每次测试都必须显式运行应用程序。这适用于应用程序的首次运行以及由模拟用户输入导致的任何重新运行。
How to retrieve elements
AppTest
类的属性返回元素的序列。元素按照在渲染的应用程序中的显示顺序排序。可以通过索引检索特定元素。此外,具有键的小部件可以通过键检索。
通过索引检索元素
AppTest
的每个属性都返回相关元素类型的序列。可以通过索引检索特定元素。在上面的示例中,at.number_input
返回应用中所有 st.number_input
元素的序列。因此,at.number_input[0]
是应用中的第一个此类元素。同样,at.markdown
返回所有 st.markdown
元素的集合,其中 at.markdown[0]
是第一个此类元素。
查看AppTest
类的“Attributes”部分或App测试速查表中当前支持的元素列表。你也可以使用.get()
方法并传递属性的名称。at.get("number_input")
和at.get("markdown")
分别等同于at.number_input
和at.markdown
。
返回的元素序列按页面上的显示顺序排列。如果使用容器以不同的顺序插入元素,这些序列可能与代码中的顺序不匹配。考虑以下示例,其中使用容器来切换页面上两个按钮的顺序:
import streamlit as st
first = st.container()
second = st.container()
second.button("A")
first.button("B")
如果上述应用程序被测试,第一个按钮(at.button[0]
)将标记为“B”,第二个按钮(at.button[1]
)将标记为“A”。作为真实的断言,这些将是:
assert at.button[0].label == "B"
assert at.button[1].label == "A"
通过键检索小部件
您可以通过小部件的键而不是它们在页面上的顺序来检索键控小部件。小部件的键作为arg或kwarg传递。例如,查看此应用程序和以下(真实)断言:
import streamlit as st
st.button("Next", key="submit")
st.button("Back", key="cancel")
assert at.button(key="submit").label == "Next"
assert at.button("cancel").label == "Back"
检索容器
您还可以通过检索特定的容器来缩小元素序列的范围。每个检索到的容器都具有与AppTest
相同的属性。例如,at.sidebar.checkbox
返回侧边栏中所有复选框的序列。at.main.selectbox
返回应用程序主体中所有选择框的序列(不在侧边栏中)。
对于AppTest.columns
和AppTest.tabs
,返回的是一个容器序列。因此at.columns[0].button
将是应用程序中第一列出现的所有按钮的序列。
How to manipulate widgets
所有小部件都有一个通用的.set_value()
方法。此外,许多小部件都有特定的方法来操作它们的值。测试元素类的名称与AppTest
属性的名称非常匹配。例如,查看AppTest.button
的返回类型以查看Button
的相应类。除了使用.set_value()
设置按钮的值外,您还可以使用.click()
。查看每个测试元素类以了解其特定方法。
How to inspect elements
所有元素,包括小部件,都有一个通用的.value
属性。这将返回元素的内容。对于小部件,这与Session State中的返回值或值相同。对于非输入元素,这将是主要内容参数的值。例如,.value
返回st.markdown
或st.error
的body
值。它返回st.dataframe
或st.table
的data
值。
此外,您可以检查小部件的许多其他详细信息,如标签或禁用状态。许多参数可供检查,但并非全部。使用linting软件查看当前支持的内容。以下是一个示例:
import streamlit as st
st.selectbox("A", [1,2,3], None, help="Pick a number", placeholder="Pick me")
assert at.selectbox[0].value == None
assert at.selectbox[0].label == "A"
assert at.selectbox[0].options == ["1","2","3"]
assert at.selectbox[0].index == None
assert at.selectbox[0].help == "Pick a number"
assert at.selectbox[0].placeholder == "Pick me"
assert at.selectbox[0].disabled == False
提示
请注意,st.selectbox
的options
被声明为整数,但被断言为字符串。正如st.selectbox
文档中所指出的,选项在内部被转换为字符串。如果你发现结果不符合预期,请仔细检查文档,看看是否有关于内部类型转换的说明。
还有问题吗?
我们的 论坛 充满了有用的信息和Streamlit专家。