Python offers a robust ecosystem of libraries for test automation that can dramatically improve your testing efficiency. Let me share the most important ones and how to use them effectively.
Pytest
Pytest has become the leading testing framework in Python due to its simplicity and power. Unlike unittest, it requires minimal boilerplate code while providing sophisticated features.
The basic test syntax is straightforward:
def test_addition():
assert 1 + 1 == 2
def test_string_methods():
assert "hello".capitalize() == "Hello"
Pytest’s fixtures provide a clean way to handle setup and teardown:
import pytest
@pytest.fixture
def database_connection():
# Setup: establish connection
conn = connect_to_test_db()
yield conn
# Teardown: close connection
conn.close()
def test_database_query(database_connection):
# The fixture is automatically passed as an argument
result = database_connection.execute("SELECT * FROM users")
assert len(result) > 0
Parametrized tests make it easy to run the same test with different inputs:
@pytest.mark.parametrize("input_value,expected", [
(1, 1),
(2, 4),
(3, 9),
(4, 16)
])
def test_square(input_value, expected):
assert input_value ** 2 == expected
Selenium
Selenium allows us to automate browser actions for web testing. I’ve found it invaluable for ensuring our web applications function correctly across different browsers.
A basic example:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
def test_search_functionality():
# Initialize browser
driver = webdriver.Chrome()
try:
# Navigate to website
driver.get("https://www.google.com")
# Find search box and enter text
search_box = driver.find_element(By.NAME, "q")
search_box.send_keys("selenium python testing")
search_box.send_keys(Keys.RETURN)
# Verify results page has loaded
assert "selenium python testing" in driver.title
# Find and verify results are present
results = driver.find_elements(By.CSS_SELECTOR, "div.g")
assert len(results) > 0
finally:
driver.quit()
For more maintainable tests, the Page Object Model pattern is recommended:
class GoogleSearchPage:
def __init__(self, driver):
self.driver = driver
self.url = "https://www.google.com"
def navigate(self):
self.driver.get(self.url)
def search(self, query):
search_box = self.driver.find_element(By.NAME, "q")
search_box.send_keys(query)
search_box.send_keys(Keys.RETURN)
class ResultsPage:
def __init__(self, driver):
self.driver = driver
def get_result_count(self):
results = self.driver.find_elements(By.CSS_SELECTOR, "div.g")
return len(results)
def test_search_with_page_objects():
driver = webdriver.Chrome()
try:
search_page = GoogleSearchPage(driver)
search_page.navigate()
search_page.search("selenium python")
results_page = ResultsPage(driver)
assert results_page.get_result_count() > 0
finally:
driver.quit()
Robot Framework
Robot Framework offers a keyword-driven approach that non-programmers can understand. Its plain text format makes it accessible to the whole team.
A basic example (saved as login_test.robot
):
*** Settings ***
Library SeleniumLibrary
*** Variables ***
${BROWSER} chrome
${URL} https://demo.com/login
*** Test Cases ***
Valid Login
Open Browser ${URL} ${BROWSER}
Input Text id:username demo_user
Input Password id:password demo_pass
Click Button id:login-button
Page Should Contain Welcome, demo_user
Close Browser
Invalid Login
Open Browser ${URL} ${BROWSER}
Input Text id:username wrong_user
Input Password id:password wrong_pass
Click Button id:login-button
Page Should Contain Invalid credentials
Close Browser
You can extend Robot Framework with custom keywords:
# In a file named custom_keywords.py
from robot.api.deco import keyword
from SeleniumLibrary import SeleniumLibrary
class CustomKeywords:
def __init__(self):
self.selenium_lib = SeleniumLibrary()
@keyword
def login_with_credentials(self, username, password):
"""Custom keyword that handles the login process"""
self.selenium_lib.input_text("id:username", username)
self.selenium_lib.input_password("id:password", password)
self.selenium_lib.click_button("id:login-button")
*** Settings ***
Library SeleniumLibrary
Library custom_keywords.CustomKeywords
*** Test Cases ***
Login With Custom Keyword
Open Browser ${URL} ${BROWSER}
Login With Credentials demo_user demo_pass
Page Should Contain Welcome, demo_user
Close Browser
Behave
Behave implements BDD testing in Python, making tests readable for stakeholders while executable for developers.
A feature file (login.feature
):
Feature: User Authentication
Users should be able to log in with valid credentials
And should be prevented from logging in with invalid credentials
Scenario: Successful login
Given I am on the login page
When I enter "valid_user" as username
And I enter "valid_password" as password
And I click the login button
Then I should see the dashboard
And I should see "Welcome, valid_user" message
Scenario: Failed login
Given I am on the login page
When I enter "invalid_user" as username
And I enter "invalid_password" as password
And I click the login button
Then I should see the login page again
And I should see "Invalid credentials" message
The steps implementation (steps/login_steps.py
):
from behave import given, when, then
from selenium import webdriver
from selenium.webdriver.common.by import By
@given('I am on the login page')
def step_impl(context):
context.driver = webdriver.Chrome()
context.driver.get("https://demo.com/login")
@when('I enter "{username}" as username')
def step_impl(context, username):
username_field = context.driver.find_element(By.ID, "username")
username_field.send_keys(username)
@when('I enter "{password}" as password')
def step_impl(context, password):
password_field = context.driver.find_element(By.ID, "password")
password_field.send_keys(password)
@when('I click the login button')
def step_impl(context):
login_button = context.driver.find_element(By.ID, "login-button")
login_button.click()
@then('I should see the dashboard')
def step_impl(context):
# Verify we've reached the dashboard page
assert context.driver.current_url.endswith("/dashboard")
@then('I should see "{message}" message')
def step_impl(context, message):
# Check if message is present on page
assert message in context.driver.page_source
context.driver.quit()
@then('I should see the login page again')
def step_impl(context):
# Verify we're still on the login page
assert context.driver.current_url.endswith("/login")
Mock (unittest.mock)
Mock is essential for isolating your code during testing. When I first started using it, my tests became faster and more reliable because they no longer depended on external systems.
A basic example:
from unittest.mock import Mock, patch
# Function we want to test
def process_payment(payment_gateway, amount):
response = payment_gateway.charge(amount)
if response.status == "success":
return True
return False
# Test with mock
def test_process_payment_success():
# Create a mock payment gateway
mock_gateway = Mock()
# Configure the mock to return a success response
mock_response = Mock()
mock_response.status = "success"
mock_gateway.charge.return_value = mock_response
# Test the function
result = process_payment(mock_gateway, 100)
# Assertions
assert result is True
mock_gateway.charge.assert_called_once_with(100)
def test_process_payment_failure():
# Create a mock payment gateway
mock_gateway = Mock()
# Configure the mock to return a failure response
mock_response = Mock()
mock_response.status = "failed"
mock_gateway.charge.return_value = mock_response
# Test the function
result = process_payment(mock_gateway, 100)
# Assertions
assert result is False
mock_gateway.charge.assert_called_once_with(100)
Using patch to mock an imported module:
import requests
from unittest.mock import patch
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
if response.status_code == 200:
return response.json()
return None
@patch('requests.get')
def test_get_user_data(mock_get):
# Configure the mock
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"id": 1, "name": "John Doe"}
mock_get.return_value = mock_response
# Call the function
result = get_user_data(1)
# Assertions
assert result == {"id": 1, "name": "John Doe"}
mock_get.assert_called_once_with("https://api.example.com/users/1")
Locust
Locust helps test how your application performs under load. I’ve used it to simulate thousands of users on our production systems before big launches.
A basic load test:
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
# Wait between 1 and 5 seconds between tasks
wait_time = between(1, 5)
@task
def index_page(self):
self.client.get("/")
@task(3) # This task is 3 times more likely to be executed
def view_products(self):
self.client.get("/products")
@task(2)
def view_about(self):
self.client.get("/about")
@task
def add_to_cart(self):
self.client.post("/cart/add", json={"product_id": 1, "quantity": 1})
A more complex example with user login:
from locust import HttpUser, task, between, SequentialTaskSet
class UserBehavior(SequentialTaskSet):
def on_start(self):
self.login()
def login(self):
response = self.client.post("/login",
data={"username": "test_user", "password": "test_pass"})
if response.status_code != 200:
self.environment.runner.quit()
@task
def browse_products(self):
self.client.get("/products")
@task
def view_product_details(self):
self.client.get("/products/1")
@task
def add_to_cart(self):
self.client.post("/cart/add", json={"product_id": 1, "quantity": 1})
@task
def checkout(self):
self.client.get("/cart")
self.client.post("/checkout", json={"payment_method": "credit_card"})
def on_stop(self):
self.client.get("/logout")
class WebsiteUser(HttpUser):
tasks = [UserBehavior]
wait_time = between(5, 15)
host = "https://example.com"
To run this test, save it as locustfile.py
and execute locust
in the terminal. Then navigate to http://localhost:8089 to configure and start the test.
Appium
Appium extends testing capabilities to mobile apps. I’ve found it particularly useful for ensuring our apps work across different devices and OS versions.
Setting up an Android test:
from appium import webdriver
from appium.webdriver.common.mobileby import MobileBy
import pytest
@pytest.fixture
def driver():
# Set up desired capabilities
desired_caps = {
'platformName': 'Android',
'deviceName': 'Android Emulator',
'appPackage': 'com.example.myapp',
'appActivity': 'com.example.myapp.MainActivity',
'automationName': 'UiAutomator2'
}
# Connect to Appium server
driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
yield driver
driver.quit()
def test_login_functionality(driver):
# Find username and password fields and login button
username_field = driver.find_element(MobileBy.ID, 'com.example.myapp:id/username')
password_field = driver.find_element(MobileBy.ID, 'com.example.myapp:id/password')
login_button = driver.find_element(MobileBy.ID, 'com.example.myapp:id/login_button')
# Enter credentials and login
username_field.send_keys('test_user')
password_field.send_keys('test_pass')
login_button.click()
# Verify successful login
welcome_message = driver.find_element(MobileBy.ID, 'com.example.myapp:id/welcome_text')
assert 'Welcome' in welcome_message.text
iOS example:
def test_ios_app():
desired_caps = {
'platformName': 'iOS',
'platformVersion': '15.0',
'deviceName': 'iPhone Simulator',
'app': '/path/to/MyApp.app',
'automationName': 'XCUITest'
}
driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
try:
# Find elements using iOS predicates
username_field = driver.find_element(
MobileBy.IOS_PREDICATE, "type == 'XCUIElementTypeTextField' AND name == 'username'"
)
password_field = driver.find_element(
MobileBy.IOS_PREDICATE, "type == 'XCUIElementTypeSecureTextField' AND name == 'password'"
)
login_button = driver.find_element(
MobileBy.ACCESSIBILITY_ID, "Login"
)
# Interact with elements
username_field.send_keys('test_user')
password_field.send_keys('test_pass')
login_button.click()
# Verify login
welcome_element = driver.find_element(MobileBy.ACCESSIBILITY_ID, "WelcomeLabel")
assert welcome_element.is_displayed()
finally:
driver.quit()
Integrating Multiple Libraries for Comprehensive Testing
The real power comes from combining these libraries. Here’s how I’ve created a complete testing solution:
# conftest.py - Shared fixtures
import pytest
from selenium import webdriver
from appium import webdriver as appium_webdriver
@pytest.fixture(scope="session")
def web_driver():
driver = webdriver.Chrome()
driver.implicitly_wait(10)
yield driver
driver.quit()
@pytest.fixture(scope="session")
def mobile_driver():
desired_caps = {
'platformName': 'Android',
'deviceName': 'Android Emulator',
'automationName': 'UiAutomator2'
}
driver = appium_webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
yield driver
driver.quit()
# test_api.py - API tests using pytest and requests with mocking
import pytest
import requests
from unittest.mock import patch
@patch('services.payment_api.requests.post')
def test_payment_processing(mock_post):
from services.payment_api import process_payment
# Configure mock
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"status": "success", "transaction_id": "123456"}
mock_post.return_value = mock_response
# Execute function under test
result = process_payment("4111111111111111", "12/25", "123", 100.00)
# Assertions
assert result["success"] is True
assert result["transaction_id"] == "123456"
mock_post.assert_called_once()
# test_web_ui.py - Web UI tests using Selenium with pytest
def test_user_registration(web_driver):
web_driver.get("https://example.com/register")
# Fill out registration form
web_driver.find_element(By.ID, "username").send_keys(f"user_{int(time.time())}")
web_driver.find_element(By.ID, "email").send_keys(f"user_{int(time.time())}@example.com")
web_driver.find_element(By.ID, "password").send_keys("secure_password")
web_driver.find_element(By.ID, "confirm_password").send_keys("secure_password")
web_driver.find_element(By.ID, "register_button").click()
# Verify registration success
assert "Welcome" in web_driver.page_source
# test_mobile.py - Mobile tests with Appium
def test_mobile_login(mobile_driver):
# Test login flow on mobile app
username_field = mobile_driver.find_element(MobileBy.ACCESSIBILITY_ID, "username")
password_field = mobile_driver.find_element(MobileBy.ACCESSIBILITY_ID, "password")
username_field.send_keys("mobile_user")
password_field.send_keys("mobile_pass")
login_button = mobile_driver.find_element(MobileBy.ACCESSIBILITY_ID, "login")
login_button.click()
# Verify login
welcome_text = mobile_driver.find_element(MobileBy.ACCESSIBILITY_ID, "welcome_message")
assert welcome_text.is_displayed()
# performance_test.py - Load testing with Locust
from locust import HttpUser, task, between
class UserBehavior(HttpUser):
wait_time = between(1, 3)
@task
def load_homepage(self):
self.client.get("/")
@task(3)
def view_products(self):
self.client.get("/products")
@task
def api_call(self):
self.client.get("/api/v1/products")
Through these examples, we’ve seen how these Python libraries can transform your testing approach. Each one has its specific strengths, but combining them creates a comprehensive testing strategy that ensures your applications are reliable, performant, and user-friendly.
In my experience, investing time in setting up these automated testing tools pays off tremendously by catching bugs early, enabling continuous integration, and giving your team confidence to release new features quickly. The initial setup might seem daunting, but the long-term benefits of maintaining quality and reducing manual testing efforts are well worth it.