App testing example
Testing a login page
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.
Project summary
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.
Automating your tests
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
Still have questions?
Our forums are full of helpful information and Streamlit experts.