App testing example

让我们考虑一个登录页面。在这个例子中,secrets.toml不存在。我们将在测试中手动声明虚拟秘密。为了避免时间攻击,登录脚本使用hmac来比较用户的密码和秘密值,这是一种安全最佳实践。

登录页面行为

在深入研究应用程序的代码之前,让我们思考一下这个页面应该做什么。无论你是使用测试驱动开发还是在编写代码后编写单元测试,思考需要测试的功能都是一个好主意。登录页面的行为应如下:

  • 在用户与应用交互之前:
    • 他们的状态是“未验证”。
    • 显示密码提示。
  • 如果用户输入了错误的密码:
    • 他们的状态为“错误”。
    • 显示错误信息。
    • 输入的密码尝试被清除。
  • 如果用户输入了正确的密码:
    • 他们的状态为“已验证”。
    • 显示确认消息。
    • 显示注销按钮(无需登录提示)。
  • 如果已登录用户点击Log out按钮:
    • 他们的状态为“未验证”。
    • 会显示密码提示。

登录页面项目结构

myproject/ ├── app.py └── tests/ └── test_app.py

登录页面 Python 文件

页面规范中提到的用户状态被编码在st.session_state.status中。该值在脚本开始时初始化为“unverified”,并在密码提示接收到新条目时通过回调更新。

"""app.py""" import streamlit as st import hmac st.session_state.status = st.session_state.get("status", "unverified") st.title("My login page") def check_password(): if hmac.compare_digest(st.session_state.password, st.secrets.password): st.session_state.status = "verified" else: st.session_state.status = "incorrect" st.session_state.password = "" def login_prompt(): st.text_input("Enter password:", key="password", on_change=check_password) if st.session_state.status == "incorrect": st.warning("Incorrect password. Please try again.") def logout(): st.session_state.status = "unverified" def welcome(): st.success("Login successful.") st.button("Log out", on_click=logout) if st.session_state.status != "verified": login_prompt() st.stop() welcome()

登录页面测试文件

这些测试严格遵循上述应用程序的规范。在每个测试中,在运行应用程序并进行进一步的模拟和检查之前,会设置一个虚拟的秘密。

from streamlit.testing.v1 import AppTest def test_no_interaction(): at = AppTest.from_file("app.py") at.secrets["password"] = "streamlit" at.run() assert at.session_state["status"] == "unverified" assert len(at.text_input) == 1 assert len(at.warning) == 0 assert len(at.success) == 0 assert len(at.button) == 0 assert at.text_input[0].value == "" def test_incorrect_password(): at = AppTest.from_file("app.py") at.secrets["password"] = "streamlit" at.run() at.text_input[0].input("balloon").run() assert at.session_state["status"] == "incorrect" assert len(at.text_input) == 1 assert len(at.warning) == 1 assert len(at.success) == 0 assert len(at.button) == 0 assert at.text_input[0].value == "" assert "Incorrect password" in at.warning[0].value def test_correct_password(): at = AppTest.from_file("app.py") at.secrets["password"] = "streamlit" at.run() at.text_input[0].input("streamlit").run() assert at.session_state["status"] == "verified" assert len(at.text_input) == 0 assert len(at.warning) == 0 assert len(at.success) == 1 assert len(at.button) == 1 assert "Login successful" in at.success[0].value assert at.button[0].label == "Log out" def test_log_out(): at = AppTest.from_file("app.py") at.secrets["password"] = "streamlit" at.session_state["status"] = "verified" at.run() at.button[0].click().run() assert at.session_state["status"] == "unverified" assert len(at.text_input) == 1 assert len(at.warning) == 0 assert len(at.success) == 0 assert len(at.button) == 0 assert at.text_input[0].value == ""

看到在上一个测试中会话状态是如何被修改的吗?测试没有完全模拟用户登录,而是通过设置at.session_state["status"] = "verified"直接跳转到已登录状态。运行应用程序后,测试继续模拟用户注销。

如果 myproject/ 被推送到 GitHub 作为一个仓库,你可以使用 Streamlit App Action 添加 GitHub Actions 测试自动化。这就像在 myproject/.github/workflows/ 添加一个工作流文件一样简单:

# .github/workflows/streamlit-app.yml name: Streamlit app on: push: branches: ["main"] pull_request: branches: ["main"] permissions: contents: read jobs: streamlit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" - uses: streamlit/streamlit-app-action@v0.0.3 with: app-path: app.py
forum

还有问题吗?

我们的 论坛 充满了有用的信息和Streamlit专家。