Search code examples
selenium-webdriverpyteststreamlitgui-testing

How to test a Streamlit application with Selenium and pytest testing framework?


I have built a simple Streamlit application and I would like to test the UI elements with the selenium and pytest framework. It is necessary to install a browser (e.g Google Chrome) and download its driver (in my case the ChromeDriver) before the UI test. After the download, I extracted the driver into a folder (e.g. <path_to_driver>/selenium) and added it to my path.

I would like to test the following Streamlit script names as app/Home.py:

import streamlit as st

st.set_page_config(page_title="Home")

st.title("Welcome to My Streamlit App")
st.write("This is the home page of the application.")

st.header("Instructions")
st.write("Navigate to the sidebar to explore different pages:")
st.write("- Home: This page.")
st.write("- About: Learn more about the app.")

st.subheader("Features")
st.write("- Interactive data visualization.")
st.write("- Easy navigation between pages.")

st.info("Enjoy exploring the app!")

It sets the page title to "Home" and displays a title, introductory text, headers, and bullet-pointed features.

The below pytest script tests the title of the Home page.

import subprocess
import time

import pytest
from selenium import webdriver


@pytest.fixture(scope="session")
def driver():
    # Start the Streamlit application as a separate process
    app_process = subprocess.Popen(
        ["streamlit", "run", "app/Home.py"]
        )

    # Wait for the Streamlit app to start
    time.sleep(2)

    driver = webdriver.Chrome()
    yield driver
    driver.quit()

    # Stop the Streamlit application process after the test
    app_process.terminate()
    app_process.wait()

def test_home_title(driver):
    driver.get("http://localhost:8501")

    # How to find the streamlit element and then wait for them to being ready?
    time.sleep(1)

    assert driver.title == "Home"

This code snippet sets up a pytest fixture named driver to test a Streamlit application using Selenium. The fixture runs once for all test cases (session scope) and performs the following steps:

  • It starts the Streamlit application as a separate process using subprocess.Popen.
  • It waits for a short period (2 seconds) to allow the Streamlit app to start.
  • It initializes a Chrome WebDriver instance for Selenium testing.
  • Inside the test function, the WebDriver navigates to the Streamlit app's URL (http://localhost:8501) and waits for a brief moment (1 second).
  • An assertion verifies that the title of the Streamlit app matches "Home".

Both time.sleep can be avoided by using Selenium's built-in implicit or explicit waits functionality. However, I have no idea how to find the elements and wait for them to be ready.

How to find the elements (st.header, st.write, and st.info) in the Streamlit application?

How to wait for them by using the Selenium built-in waiting functionalities?


Solution

  • There is a testing framework that eases the creation of the test cases, the seleniumbase. For Pythonista, it is an all-in-one testing framework. It uses Selenium/WebDriver APIs without downloading it explicitly and it incorporates pytest, and behave test-runners.

    Once the testing framework is installed (pip install seleniumbase), the test_home_title test case will be reduced to:

    import subprocess
    
    from seleniumbase import BaseCase
    
    
    class PageContentTest(BaseCase):
        @classmethod
        def setUpClass(cls) -> None:
            cls.app_process = subprocess.Popen(["streamlit", "run", "app/Home.py"])
    
        def test_home_page(self) -> None:
            self.open("http://localhost:8501")
    
            self.assert_title("Home")
    
            # Assert the headers
            self.assert_text("Welcome to My Streamlit App")
            self.assert_text("Instructions")
            self.assert_text("Features")
    
            # Assert the info element
            self.assert_element("div.stAlert")
    
        @classmethod
        def tearDownClass(cls) -> None:
            cls.app_process.terminate()
            cls.app_process.wait()
    

    You do not see any webdriver construction and deconstruction. No need to define implicit or explicit waits because SeleniumBase waits under the hood. Inherit from the seleniumbase.BaseCase and create methods for the test cases, the rest is automatically handled.

    Execute the test with one of the supported test runners:

    pytest --browser=chrome --headless
    

    In this example, we test using the Chrome browser in a headless (without a graphical user interface) mode.

    Note: Change the default browser by using the --browser=firefox option flag.