ποΈ Test Design Patterns in Selenium
Building Your Test Automation Like a Master Architect
π¬ The Story: Building a LEGO City
Imagine youβre building an amazing LEGO city. You could just throw bricks everywhere randomlyβ¦ but that would be messy and hard to change later!
Smart builders organize their pieces:
- π Houses in one box
- π Cars in another box
- π³ Trees in a third box
When you want to change something, you know exactly where to look!
Test Design Patterns work the same way. They help you organize your test code so itβs:
- β Easy to understand
- β Easy to change
- β Easy to fix when something breaks
π¦ Page Object Model Pattern
What Is It?
Think of your favorite restaurant menu. You donβt need to know how to cook the foodβyou just point at what you want!
Page Object Model (POM) is like creating a menu for each webpage:
- The menu (Page Object) knows where everything is
- Your tests just say βI want thisβ without worrying about details
Why Use It?
Without POM (Messy):
βββ Test 1: Find button by ID "login-btn"
βββ Test 2: Find button by ID "login-btn"
βββ Test 3: Find button by ID "login-btn"
βββ Button ID changes? FIX 100 TESTS! π±
With POM (Clean):
βββ LoginPage: knows the button location
βββ Test 1: Uses LoginPage
βββ Test 2: Uses LoginPage
βββ Button ID changes? FIX 1 PLACE! π
Real Example
# WITHOUT POM - Messy! π
def test_login():
driver.find_element(
By.ID, "username"
).send_keys("john")
driver.find_element(
By.ID, "password"
).send_keys("secret")
driver.find_element(
By.ID, "login-btn"
).click()
# WITH POM - Clean! β¨
def test_login():
login_page.enter_username("john")
login_page.enter_password("secret")
login_page.click_login()
π¨ Page Class Design
The Blueprint
Every page in your website gets its own βhelper class.β Think of it like giving each room in your house a dedicated robot butler!
Three Golden Rules
graph TD A["π― Rule 1: One Page = One Class"] --> B["π Rule 2: Store Locators Together"] B --> C["π§ Rule 3: Create Action Methods"] C --> D["β Clean & Organized!"]
Simple Structure
class LoginPage:
# π Where things are
USERNAME_FIELD = (By.ID, "username")
PASSWORD_FIELD = (By.ID, "password")
LOGIN_BUTTON = (By.ID, "login-btn")
def __init__(self, driver):
self.driver = driver
# π§ What you can do
def enter_username(self, username):
self.driver.find_element(
*self.USERNAME_FIELD
).send_keys(username)
Naming Tips
| Element Type | Good Name | Bad Name |
|---|---|---|
| Button | LOGIN_BUTTON |
btn1 |
| Input | EMAIL_INPUT |
field |
| Link | FORGOT_PASSWORD_LINK |
a2 |
βοΈ Page Method Implementation
What Are Page Methods?
Page methods are the actions you can do on a page. Like pressing buttons on a TV remote!
TV Remote (Page Object):
βββ πΊ turnOn()
βββ π volumeUp()
βββ πΊ changeChannel(number)
βββ βΈοΈ pause()
Types of Methods
graph TD A["Page Methods"] --> B["π¬ Action Methods"] A --> C["β Query Methods"] A --> D["β Verification Methods"] B --> E["click, type, submit"] C --> F["getText, getValue"] D --> G["isDisplayed, isEnabled"]
Complete Example
class ProductPage:
ADD_TO_CART = (By.ID, "add-cart")
PRODUCT_NAME = (By.CLASS, "name")
CART_BADGE = (By.ID, "cart-count")
# π¬ Action Method
def add_to_cart(self):
self.driver.find_element(
*self.ADD_TO_CART
).click()
# β Query Method
def get_product_name(self):
return self.driver.find_element(
*self.PRODUCT_NAME
).text
# β
Verification Method
def is_added_to_cart(self):
badge = self.driver.find_element(
*self.CART_BADGE
)
return int(badge.text) > 0
Pro Tip: Method Chaining
Make your tests read like sentences!
# Chain methods together
checkout_page \
.enter_address("123 Main St") \
.select_shipping("Express") \
.apply_coupon("SAVE10") \
.place_order()
π Data-Driven Testing
The Problem
You want to test login with:
- Valid username + valid password β
- Invalid username + valid password β
- Valid username + invalid password β
- Empty username + empty password β
Writing 4 separate tests? Too much copy-paste!
The Solution: One Test, Many Inputs
graph TD A["π Test Data"] --> B["π Same Test Logic"] B --> C["Run with Data 1"] B --> D["Run with Data 2"] B --> E["Run with Data 3"] B --> F["Run with Data N..."]
Simple Example
# Test data - like a shopping list
test_data = [
("john", "pass123", True),
("wrong", "pass123", False),
("john", "wrong", False),
("", "", False),
]
# One test, many runs!
@pytest.mark.parametrize(
"user,password,should_pass",
test_data
)
def test_login(user, password, should_pass):
login_page.login(user, password)
if should_pass:
assert "Welcome" in driver.title
else:
assert login_page.has_error()
Benefits
| Without Data-Driven | With Data-Driven |
|---|---|
| 4 separate tests | 1 test |
| 40 lines of code | 15 lines of code |
| Hard to add cases | Easy to add cases |
| Lots of duplication | Zero duplication |
π Test Data Management
Where Does Test Data Live?
Think of test data like ingredients for cooking. You need a good pantry!
graph TD A["π Test Data Sources"] --> B["π CSV Files"] A --> C["π Excel Files"] A --> D["π§ JSON Files"] A --> E["ποΈ Database"] A --> F["π API Response"]
Option 1: JSON Files (Recommended for small data)
{
"valid_users": [
{
"username": "john_doe",
"password": "secure123",
"role": "admin"
},
{
"username": "jane_doe",
"password": "pass456",
"role": "user"
}
]
}
import json
def load_test_data(filename):
with open(filename) as f:
return json.load(f)
users = load_test_data("users.json")
Option 2: CSV Files (Great for tables)
username,password,expected_result
john,pass123,success
invalid,pass123,error
john,wrong,error
import csv
def load_csv_data(filename):
with open(filename) as f:
reader = csv.DictReader(f)
return list(reader)
Option 3: Environment-Specific Data
# Different data for different environments
data_files = {
"dev": "data/dev_users.json",
"staging": "data/staging_users.json",
"prod": "data/prod_users.json"
}
env = os.getenv("TEST_ENV", "dev")
data = load_test_data(data_files[env])
Data Organization Structure
π tests/
βββ π data/
β βββ π users.json
β βββ π products.csv
β βββ π config.json
βββ π pages/
β βββ π login_page.py
β βββ π product_page.py
βββ π tests/
βββ π test_login.py
βββ π test_products.py
π― Putting It All Together
Hereβs how all pieces work together:
graph TD A["π Test Data Files"] --> B["π Data-Driven Test"] C["π Page Objects"] --> B B --> D["π§ Page Methods"] D --> E["β Test Results"]
Complete Working Example
# 1. Page Object with Methods
class LoginPage:
USERNAME = (By.ID, "user")
PASSWORD = (By.ID, "pass")
SUBMIT = (By.ID, "login")
ERROR = (By.CLASS, "error")
def __init__(self, driver):
self.driver = driver
def login(self, user, pwd):
self.driver.find_element(
*self.USERNAME
).send_keys(user)
self.driver.find_element(
*self.PASSWORD
).send_keys(pwd)
self.driver.find_element(
*self.SUBMIT
).click()
return self
def has_error(self):
try:
self.driver.find_element(
*self.ERROR
)
return True
except:
return False
# 2. Test Data (from JSON file)
test_cases = [
("valid@email.com", "pass123", True),
("invalid", "pass123", False),
]
# 3. Data-Driven Test
@pytest.mark.parametrize(
"email,password,success",
test_cases
)
def test_login_scenarios(
driver, email, password, success
):
page = LoginPage(driver)
page.login(email, password)
assert page.has_error() == (not success)
π Quick Recap
| Pattern | Purpose | Remember It As |
|---|---|---|
| Page Object Model | Separate page info from tests | βMenu for each pageβ |
| Page Class Design | Organize locators & methods | βRobot butler per roomβ |
| Page Methods | Actions on pages | βTV remote buttonsβ |
| Data-Driven Testing | One test, many inputs | βRecipe with ingredients listβ |
| Test Data Management | Organize your data | βWell-organized pantryβ |
π‘ Final Wisdom
βGood tests are like good storiesβtheyβre easy to read, easy to change, and everyone understands them.β
Start small:
- Create ONE page object
- Add 2-3 methods
- Use it in ONE test
- Celebrate! π
Then repeat and grow your test automation empire! π
