Test code is code. It deserves the same engineering discipline as production code — because when test code is poorly structured, tests become unmaintainable, fragile, and eventually abandoned. The SOLID principles, originally defined for object-oriented software design, apply directly to test automation frameworks. Each principle addresses a specific maintenance problem that every growing test suite eventually encounters.
SOLID Principles for Test Automation
Each principle is illustrated with a test automation anti-pattern (violating the principle) and the correct pattern (applying it).
# SOLID principles applied to test automation
SOLID = [
{
"principle": "S — Single Responsibility Principle",
"definition": "A class should have only one reason to change",
"anti_pattern": (
"LoginPage class that contains locators, actions, assertions, "
"AND test data — changes to any of these four concerns require "
"modifying the same class"
),
"correct_pattern": (
"LoginPage has locators + actions only. Assertions live in tests. "
"Test data lives in fixtures. Configuration lives in settings. "
"Each concern changes independently."
),
"framework_example": "BasePage (interactions) | PageObjects (locators+actions) | Tests (assertions) | Fixtures (data)",
},
{
"principle": "O — Open/Closed Principle",
"definition": "Open for extension, closed for modification",
"anti_pattern": (
"Adding a new browser requires modifying DriverFactory's if/elif chain "
"and every test that references browser-specific logic"
),
"correct_pattern": (
"DriverFactory uses a registry/dictionary pattern. Adding a new browser "
"means registering a new entry — no existing code is modified. "
"driver_map['safari'] = create_safari_driver"
),
"framework_example": "DriverFactory with pluggable browser configurations",
},
{
"principle": "L — Liskov Substitution Principle",
"definition": "Subtypes must be substitutable for their base types",
"anti_pattern": (
"CheckoutPage inherits from BasePage but overrides click() to also "
"take a screenshot — code expecting BasePage.click() gets unexpected "
"side effects"
),
"correct_pattern": (
"CheckoutPage.click() behaves identically to BasePage.click(). "
"Screenshot logic is in a separate decorator or hook, not in the override."
),
"framework_example": "All PageObjects are interchangeable through BasePage interface",
},
{
"principle": "I — Interface Segregation Principle",
"definition": "Clients should not depend on interfaces they do not use",
"anti_pattern": (
"BasePage has 30 methods including mobile-specific methods (swipe, pinch). "
"Desktop PageObjects inherit all 30 but only use 15."
),
"correct_pattern": (
"BasePage has core methods (click, type, get_text). MobileBasePage "
"adds mobile methods (swipe, pinch). Desktop pages inherit BasePage; "
"mobile pages inherit MobileBasePage."
),
"framework_example": "Separate BasePage and MobileBasePage hierarchies",
},
{
"principle": "D — Dependency Inversion Principle",
"definition": "Depend on abstractions, not concrete implementations",
"anti_pattern": (
"Tests directly instantiate webdriver.Chrome() — tied to Chrome, "
"cannot switch to Firefox or Grid without modifying every test"
),
"correct_pattern": (
"Tests receive a 'driver' from a fixture/factory. The fixture decides "
"which browser to create based on configuration. Tests depend on the "
"WebDriver abstraction, not a specific browser class."
),
"framework_example": "DriverFactory + pytest fixtures inject the driver; tests never import webdriver",
},
]
for s in SOLID:
print(f"\n{'='*65}")
print(f" {s['principle']}")
print(f"{'='*65}")
print(f" Definition: {s['definition']}")
print(f" Anti-pattern: {s['anti_pattern']}")
print(f" Correct: {s['correct_pattern']}")
print(f" Example: {s['framework_example']}")
driver fixture instead of directly calling webdriver.Chrome(), switching from local Chrome to Selenium Grid to BrowserStack requires changing the fixture only. Every test, page object, and utility continues to work unchanged. This inversion is the single most valuable architectural decision in a test framework.Common Mistakes
Mistake 1 — Putting assertions in page objects (violates SRP)
❌ Wrong: LoginPage.verify_login_success() — the page object both performs actions AND verifies outcomes.
✅ Correct: LoginPage.login() returns DashboardPage. The test asserts: assert dashboard.get_username() == "Alice". Action and verification are separate responsibilities.
Mistake 2 — Tests creating their own WebDriver instances (violates DIP)
❌ Wrong: driver = webdriver.Chrome() at the start of every test — 200 tests directly depend on Chrome.
✅ Correct: def test_login(browser): — the browser fixture injects the driver. The test depends on the abstraction (a WebDriver instance), not the concrete implementation (Chrome).