App testing example

Let's consider a login page. In this example, secrets.toml is not present. We'll manually declare dummy secrets directly in the tests. To avoid timing attacks, the login script uses hmac to compare a user's password to the secret value as a security best practice.

Login page behavior

Before diving into the app's code, let's think about what this page is supposed to do. Whether you use test-driven development or you write unit tests after your code, it's a good idea to think about the functionality that needs to be tested. The login page should behave as follows:

  • Before a user interacts with the app:
    • Their status is "unverified."
    • A password prompt is displayed.
  • If a user types an incorrect password:
    • Their status is "incorrect."
    • An error message is displayed.
    • The password attempt is cleared from the input.
  • If a user types a correct password:
    • Their status is "verified."
    • A confirmation message is displayed.
    • A logout button is displayed (without a login prompt).
  • If a logged-in user clicks the Log out button:
    • Their status is "unverified."
    • A password prompt is displayed.

Login page project structure

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

Login page Python file

The user's status mentioned in the page's specifications are encoded in st.session_state.status. This value is initialized at the beginning of the script as "unverified" and is updated through a callback when the password prompt receives a new entry.

"""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()

Login page test file

These tests closely follow the app's specifications above. In each test, a dummy secret is set before running the app and proceeding with further simulations and checks.

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 == ""

See how Session State was modified in the last test? Instead of fully simulating a user logging in, the test jumps straight to a logged-in state by setting at.session_state["status"] = "verified". After running the app, the test proceeds to simulate the user logging out.

If myproject/ was pushed to GitHub as a repository, you could add GitHub Actions test automation with Streamlit App Action. This is as simple as adding a workflow file at 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

Still have questions?

Our forums are full of helpful information and Streamlit experts.