本記事は「 TTDC Advent Calendar 2024 」 17日目の記事です。
概要
StreamlitはPythonで簡単にWebアプリを作成できるパッケージです。
そうして作成したWebアプリをデプロイする場合、当然ながらテストの実施が必要になります。
本記事ではStreamlitに用意されているテスト用のクラスAppTest
を実装し、
できること・できないことを私自身の備忘録も兼ねてまとめました。
最後に本記事の実装例をまとめたサンプルコードを用意しているので、ぜひ参考にしてください。
- 環境、ライブラリ
- python 3.10
- streamlit==1.40.2
- pandas==2.2.3
- pytest==8.3.3
- pytest-mock==3.14.0
TL;DR
- Streamlit内部の変数値テストを行うようなケースであればAppTestは簡単に実装可能
- ブラウザ上の出力やUI等をテストする場合はSelenium等のUIテストパッケージの利用が必要
AppTestの実施
呼び出し
下記メソッドによりPythonファイルを読み込み、テスト環境下でアプリケーションを実行することが可能です。
from streamlit.testing.v1 import AppTest
at = AppTest.from_file("<path/to/streamlit_app.py>")
その他、文字列型としてコードを受け取ってテストを行うAppTest.from_string
や、
関数単位でのテストを行うAppTest.from_function
も用意されています。
ウィジェットへの入出力
ウィジェットへの入力はset_value()
を呼び出して行うことができます。
keyを指定していないウィジェットの場合はindex番号で取得することになりますが、
後に同種のウィジェットを追加する場合はテストが破綻する可能性があることに注意が必要です。
# keyを指定したウィジェットのvalueを変更する
at.number_input(key="number").set_value(10)
at.sidebar.slider(key="number_range").set_value((3,5))
# keyが未指定のウィジェットはindexで呼び出しを行う
# 3番目のst.checkboxのvalueを変更する
at.checkbox[2].set_value(True)
# AppTest.getを利用した実装
# 1番目のst.text_inputのvalueを変更する
at.get("text_input")[0].set_value("Hello, World")
各ウィジェットのvalue
を呼び出すことで出力を取得し、テストを行うことができます。
assert at.number_input(key="number").value == 10
session_state変数への入出力
Streamlitはアプリ実行間の変数を保持する機能としてsession_state
が用意されています。
session_state変数の代入および読み込みを行うことで処理中の変数をテストすることができます。
# session_state入力 -> ウィジェットへの反映を確認
at.session_state.number = 20
assert at.number_input(key="number").value == 20
# ウィジェット入力 -> session_stateへの反映を確認
at.sidebar.slider(key="number_range").set_value((4, 6))
assert at.session_state.number_range == (4, 6)
アプリの再実行
at.run()
によってアプリが再実行されます。
通常、Streamlitではウィジェットへの入力が行われる度に自動的にアプリが再実行されますが、
AppTestにおいては再実行するタイミングを明示的に記述します。
# アプリの再実行
at.run()
# ウィジェットのメソッドとしても利用可能
at.number_input(key="number").set_value(10).run()
AppTestで未実装のウィジェット
基本的な入力ウィジェット(st.button
やst.slider
等)に対しては先ほどのように値の入出力を行うことが出来ます。
一方で発展的なウィジェット(st.file_uploader
やst.dataframe
等)に関してはAppTestに実装されていません。
そのため、それらを利用するアプリのテスト時にはsession_stateを経由して直接ウィジェットのvalueに値を代入する必要があります。
または、pytestのpluginであるmocker
を利用し、ウィジェットの戻り値を指定する方法もあります。
file = open(path, "rb")
mocker.patch("streamlit.file_uploader", return_value=file)
at.run()
その他、ウィジェットでブラウザ上に出力された結果や、UIの情報を取得するといったこともできません。
また、st.download_button
のようなウィジェットでダウンロードしたファイルのテストを行うことも不可能です。
そのため、ブラウザ上の出力やUIに対するテストを行いたい場合はAppTestではなく、Selenium等の一般的なUIテストツールを利用すべきでしょう。
注意点
エラー出力
AppTest実行中に発生したエラーはコマンドライン上には表示されず、アプリ内のフロント側に表示されます。
そのため、エラー確認や異常系テストは以下のメソッドから取得してください。
参考:Streamlit: AppTest.exception
at.exception
特殊なウィジェット名
一部のウィジェットではat.<widget_name>()
のように直接呼び出すことができません。
例えば以下のようなst.formコンテナ内のst.form_submit_buttonをテストしたい場合、
with st.form(key="my_form"):
#####
# do something
#####
st.form_submit_button("Run")
単にat.form_submit_button()
として呼び出すことはできず、次のようなコードになります。
at.button(key="FormSubmitter:my_form-Run").click()
実装中にウィジェットの呼び出しが上手くいかない場合、at.session_state
からウィジェットのステータスを確認し、ウィジェットの正式なキーを参照しましょう。
まとめ
AppTestでできること
- 基本的なウィジェットに対する入出力が可能
- session_state変数への入出力を行うことができる
AppTestでできないこと
-
st.file_uploader
やst.dataframe
等の発展的なウィジェットへの入出力ができない - そのため、ブラウザ上に出力される結果やUI等に対するテストは行えない場合が多い
結論
- Streamlit内部の変数値テストを行うようなケースであればAppTestは簡単に実装可能
- ブラウザ上の出力やUI等をテストする場合はSelenium等のUIテストパッケージの利用が必要
付録
本記事で説明した内容を含んだサンプルコードです。
-
ディレクトリ構成
. ├── app │ └── app.py └── test └── test_app.py
-
実行方法
$ cd <path/to/test> $ pytest test_app.py
import streamlit as st
st.number_input("input number", key="number")
st.sidebar.slider("slider", value=(0, 10), key="number_range")
st.checkbox("1")
st.checkbox("2")
st.checkbox("3")
st.checkbox("4")
st.text_input("input text")
with st.form("my_form"):
st.session_state.form_number = st.number_input(
"form input", value=0, key="form_num"
)
st.form_submit_button("Run")
st.session_state.file = st.file_uploader("upload file")
from streamlit.testing.v1 import AppTest
import pandas as pd
at = AppTest.from_file("../app/app.py")
at.run()
# テスト用ファイル(DataFrame)の用意
TEST_DF = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
TEST_DF_PATH = "test_df.csv"
TEST_DF.to_csv(TEST_DF_PATH, index=False)
def test_stremalit(mocker):
# ウィジェットのテスト
at.number_input(key="number").set_value(10).run()
assert at.number_input(key="number").value == 10
# サイドバーウィジェットのテスト
at.sidebar.slider(key="number_range").set_value((3, 5)).run()
assert at.sidebar.slider(key="number_range").value == (3, 5)
# key指定の無いウィジェットのテスト
at.checkbox[2].set_value(True).run()
assert at.checkbox[2].value
# AppTest.getを利用したテスト
at.get("text_input")[0].set_value("Hello, World").run()
assert at.get("text_input")[0].value == "Hello, World"
# Session_state変数の代入・テスト
at.session_state.number = 20
assert at.number_input(key="number").value == 20
at.sidebar.slider(key="number_range").set_value((4, 6)).run()
assert at.session_state.number_range == (4, 6)
# formコンテナ内でのウィジェットのテスト
at.number_input(key="form_num").set_value(10)
at.button(key="FormSubmitter:my_form-Run").click().run()
assert at.session_state.form_number == 10
# mockerを用いたファイルアップロードおよび読み込みテスト
file = open(TEST_DF_PATH, "rb")
mocker.patch("streamlit.file_uploader", return_value=file)
at.run()
loaded_df = pd.read_csv(at.session_state.file)
pd.testing.assert_frame_equal(TEST_DF, loaded_df)
おわりに
今回の記事は以上になります。
最後まで読んでいただき、ありがとうございました!
本記事の内容に誤りなどあれば、コメントにてご教授お願いいたします。