Writing Stable E2E Tests with pytest and pytest-bdd
End-to-end (E2E) testing is crucial for ensuring that your application works correctly from the user’s perspective. However, E2E tests are notorious for being flaky, slow, and difficult to maintain. In this comprehensive guide, we’ll explore how to write stable and maintainable E2E tests using pytest and pytest-bdd.
Why pytest-bdd?
pytest-bdd combines the power of pytest with the clarity of Behavior-Driven Development (BDD). It allows you to write tests in natural language using Gherkin syntax while leveraging Python’s robust testing ecosystem.
Benefits of BDD with pytest-bdd:
- Readable Tests: Tests are written in plain English using Gherkin syntax
- Collaboration: Non-technical stakeholders can understand and contribute to test scenarios
- Living Documentation: Feature files serve as up-to-date documentation
- Reusable Steps: Step definitions can be shared across multiple scenarios
- pytest Integration: Full access to pytest’s powerful features and plugins
Setting Up Your Testing Environment
Let’s start by setting up a complete testing environment:
1
2
3
4
5
6
# Create a virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install pytest pytest-bdd selenium webdriver-manager
The key to stable E2E tests lies in proper setup and following established patterns. Let’s explore the essential components step by step.
Best Practices for Stable Tests
1. Use Explicit Waits
Always use explicit waits instead of implicit waits or sleep statements:
1
2
3
4
5
6
7
8
9
10
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# Good approach
element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, "submit-button"))
)
# Avoid this
time.sleep(5) # Arbitrary wait
2. Implement Page Object Model
Create maintainable page objects:
1
2
3
4
5
6
7
8
9
10
11
12
13
class LoginPage:
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def enter_credentials(self, username, password):
username_field = self.wait.until(
EC.presence_of_element_located((By.ID, "username"))
)
username_field.send_keys(username)
password_field = self.driver.find_element(By.ID, "password")
password_field.send_keys(password)
3. Handle Dynamic Content
Create utilities for handling dynamic content and asynchronous operations:
1
2
3
4
5
def wait_for_element_text_to_change(driver, locator, old_text, timeout=10):
"""Wait for element text to change from old_text"""
return WebDriverWait(driver, timeout).until(
lambda d: d.find_element(*locator).text != old_text
)
Writing Feature Files
Here’s an example of a well-structured feature file:
1
2
3
4
5
6
7
8
9
10
11
Feature: User Authentication
As a user
I want to authenticate with the application
So that I can access my account securely
Scenario: Successful login with valid credentials
Given I am on the login page
When I enter valid credentials
And I click the login button
Then I should be redirected to the dashboard
And I should see a welcome message
Implementing Step Definitions
Create clear and reusable step definitions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pytest_bdd import given, when, then, scenarios
scenarios('../features/login.feature')
@given('I am on the login page')
def navigate_to_login_page(browser):
browser.get("https://example.com/login")
@when('I enter valid credentials')
def enter_valid_credentials(browser):
login_page = LoginPage(browser)
login_page.enter_credentials("testuser", "password123")
@then('I should be redirected to the dashboard')
def verify_dashboard_redirect(browser):
WebDriverWait(browser, 10).until(
EC.url_contains("/dashboard")
)
Configuration and Test Management
Set up robust configuration:
1
2
3
4
5
6
7
8
9
10
# conftest.py
import pytest
from selenium import webdriver
@pytest.fixture(scope="function")
def browser():
driver = webdriver.Chrome()
driver.maximize_window()
yield driver
driver.quit()
Running and Debugging Tests
Execute your tests with proper reporting:
1
2
3
4
5
6
7
8
# Run all tests
pytest
# Run specific feature
pytest tests/features/login.feature
# Run with HTML report
pytest --html=reports/report.html
Advanced Techniques
Parallel Execution
Use pytest-xdist for parallel test execution:
1
pytest -n 4 # Run with 4 parallel workers
Retry Mechanisms
Implement retry logic for flaky operations:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from functools import wraps
import time
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise e
time.sleep(delay)
return None
return wrapper
return decorator
Common Pitfalls and Solutions
- Flaky Tests: Use proper waits and retry mechanisms
- Slow Execution: Implement parallel execution and optimize selectors
- Maintenance Overhead: Use Page Object Model and reusable components
- Poor Debugging: Add screenshots and detailed logging
Integration with CI/CD
Example GitHub Actions configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run E2E tests
run: pytest tests/ --html=reports/report.html
Conclusion
Writing stable E2E tests with pytest-bdd requires:
- Proper setup with explicit waits and robust configuration
- Clear test structure using BDD principles and Gherkin syntax
- Maintainable code with Page Object Model
- Good practices for handling dynamic content and failures
- Proper CI/CD integration for continuous feedback
By following these practices, you can create a reliable E2E testing suite that provides valuable feedback about your application’s quality while being maintainable and scalable.
Ready to implement these practices in your project? Join our community to discuss E2E testing strategies and share your experiences with fellow QA engineers!
QA Hive