Selenium Automation in Practice: Test a Login Flow and a Dashboard Check
Selenium is useful when you need to test how a real user interacts with a browser: typing into forms, clicking buttons, waiting for pages to update, and checking visible UI state. Unit tests can verify business logic, and API tests can verify endpoints, but Selenium helps you catch problems in the full browser experience.
In this article, you will build a small Selenium test suite in Python that checks a login flow and verifies that a dashboard loads correctly. The examples are written for junior and mid-level developers who want practical, maintainable browser automation.
What We Are Going to Test
Assume your web app has the following pages:
/login— a login form with email and password fields./dashboard— a protected page shown after login.- A logout button that returns the user to the login screen.
We will test three things:
- A valid user can log in.
- The dashboard displays the expected heading.
- The user can log out successfully.
Install Selenium and Pytest
Start with a clean Python virtual environment:
python -m venv .venv source .venv/bin/activate pip install selenium pytest
Modern Selenium can manage browser drivers automatically through Selenium Manager, so you usually do not need to download chromedriver manually. You only need Chrome, Firefox, or another supported browser installed on your machine.
Create a Basic Selenium Test
Create a file called test_login.py:
from selenium import webdriver from selenium.webdriver.common.by import By def test_login_page_title(): driver = webdriver.Chrome() driver.get("http://localhost:8000/login") assert "Login" in driver.title driver.quit()
Run it with:
pytest
This test opens Chrome, visits the login page, checks the page title, and closes the browser. It works, but it has a problem: if the assertion fails, driver.quit() may not run. That can leave browser windows open. A better pattern is to use a Pytest fixture.
Use a Browser Fixture
Fixtures help you reuse setup and cleanup logic across tests. Create a file called conftest.py:
import pytest from selenium import webdriver from selenium.webdriver.chrome.options import Options @pytest.fixture def driver(): options = Options() options.add_argument("--window-size=1280,900") browser = webdriver.Chrome(options=options) yield browser browser.quit()
Now update test_login.py:
from selenium.webdriver.common.by import By def test_login_page_title(driver): driver.get("http://localhost:8000/login") assert "Login" in driver.title
This is cleaner. Pytest starts the browser before each test and closes it afterward, even if the test fails.
Prefer Stable Selectors
One common mistake in Selenium automation is selecting elements by CSS classes that are mainly used for styling. For example, this selector is fragile:
driver.find_element(By.CSS_SELECTOR, ".btn.btn-primary.mt-4")
If a developer changes the button styling, the test may break even though the app still works. A better option is to add dedicated test attributes to your HTML:
<input data-testid="email-input" type="email" name="email"> <input data-testid="password-input" type="password" name="password"> <button data-testid="login-button">Log in</button>
Then select elements like this:
driver.find_element(By.CSS_SELECTOR, '[data-testid="email-input"]')
This makes your tests less sensitive to visual refactors.
Write the Login Test
Now let’s test a real login flow. In a real project, use a test database user, not a personal account.
from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC BASE_URL = "http://localhost:8000" def test_user_can_login(driver): driver.get(f"{BASE_URL}/login") email = driver.find_element(By.CSS_SELECTOR, '[data-testid="email-input"]') password = driver.find_element(By.CSS_SELECTOR, '[data-testid="password-input"]') login_button = driver.find_element(By.CSS_SELECTOR, '[data-testid="login-button"]') email.send_keys("[email protected]") password.send_keys("password123") login_button.click() heading = WebDriverWait(driver, 10).until( EC.visibility_of_element_located( (By.CSS_SELECTOR, '[data-testid="dashboard-heading"]') ) ) assert heading.text == "Dashboard" assert "/dashboard" in driver.current_url
The important part is WebDriverWait. Browser tests should not assume that the next page appears instantly. Network calls, frontend rendering, and redirects can take time. Avoid using time.sleep() unless you are debugging. Explicit waits are more reliable because they wait only until a condition is true.
Create Helper Functions for Repeated Actions
As your test suite grows, repeated login code becomes noisy. You can move it into a helper function:
from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC BASE_URL = "http://localhost:8000" def login(driver, email="[email protected]", password="password123"): driver.get(f"{BASE_URL}/login") driver.find_element(By.CSS_SELECTOR, '[data-testid="email-input"]').send_keys(email) driver.find_element(By.CSS_SELECTOR, '[data-testid="password-input"]').send_keys(password) driver.find_element(By.CSS_SELECTOR, '[data-testid="login-button"]').click() WebDriverWait(driver, 10).until( EC.url_contains("/dashboard") )
Then your dashboard test becomes simpler:
from selenium.webdriver.common.by import By def test_dashboard_shows_user_summary(driver): login(driver) summary = driver.find_element(By.CSS_SELECTOR, '[data-testid="user-summary"]') assert "[email protected]" in summary.text
This style makes each test easier to read. The test describes the behavior, while the helper handles the browser details.
Test Logout Behavior
A good end-to-end test should cover the user’s exit path too. Here is a logout test:
from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def test_user_can_logout(driver): login(driver) driver.find_element(By.CSS_SELECTOR, '[data-testid="logout-button"]').click() WebDriverWait(driver, 10).until( EC.url_contains("/login") ) login_heading = driver.find_element( By.CSS_SELECTOR, '[data-testid="login-heading"]' ) assert login_heading.text == "Log in"
This confirms that the logout button works, the user is redirected, and the login page is visible again.
Run Tests in Headless Mode
For local debugging, seeing the browser is helpful. In CI/CD, you usually want headless mode. Update your fixture to support both modes with an environment variable:
import os import pytest from selenium import webdriver from selenium.webdriver.chrome.options import Options @pytest.fixture def driver(): options = Options() options.add_argument("--window-size=1280,900") if os.getenv("HEADLESS") == "true": options.add_argument("--headless=new") browser = webdriver.Chrome(options=options) yield browser browser.quit()
Run normally:
pytest
Run headless:
HEADLESS=true pytest
Capture Screenshots on Failure
When a Selenium test fails in CI, a screenshot can save a lot of debugging time. You can add a small Pytest hook to conftest.py:
import os import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: driver = item.funcargs.get("driver") if driver: os.makedirs("screenshots", exist_ok=True) screenshot_path = f"screenshots/{item.name}.png" driver.save_screenshot(screenshot_path)
Now failed tests will create screenshots in a screenshots directory. This is especially useful when the test passes locally but fails in a pipeline because of viewport, timing, or environment differences.
Practical Selenium Guidelines
- Use
data-testidattributes for stable selectors. - Prefer
WebDriverWaitovertime.sleep(). - Keep tests focused on user behavior, not implementation details.
- Use helper functions for common flows like login.
- Run visible browser tests locally and headless tests in CI.
- Capture screenshots when tests fail.
Final Thoughts
Selenium automation is most valuable when it tests important user journeys: login, checkout, onboarding, dashboard access, account settings, and other flows that must not break. You do not need hundreds of browser tests. Start with a few high-value paths, use stable selectors, and make your tests readable.
The goal is not to test every button with Selenium. The goal is to confirm that your application works from the user’s point of view. With Pytest fixtures, explicit waits, reusable helpers, and failure screenshots, you can build a Selenium test suite that is practical, maintainable, and friendly to both local development and CI environments.
Leave a Reply