Spaces:
Sleeping
Sleeping
File size: 5,503 Bytes
746d2f1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
---
title: App testing example
slug: /develop/concepts/app-testing/examples
---
# 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](https://en.wikipedia.org/wiki/Timing_attack), 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
```none
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.
```python
"""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.
```python
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](https://github.com/marketplace/actions/streamlit-app-action). This is as simple as adding a workflow file at `myproject/.github/workflows/`:
```yaml
# .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/[email protected]
with:
app-path: app.py
```
|