The difference between “I have Selenium tests” and “I have a test automation framework” is architecture. A framework is a layered system where each layer has a single responsibility: configuration management, browser lifecycle, page interactions, test logic, and reporting. When you change a URL, you edit the config layer — not 200 test files. When you add a new browser, you edit the driver layer — not the page objects. This separation of concerns is what makes a framework maintainable at scale.
The Five-Layer Framework Architecture
Every production-grade Selenium framework — regardless of language, tool, or team size — follows the same five-layer structure. Each layer depends only on the layer below it, never on the layers above it.
# The Five-Layer Framework Architecture
FRAMEWORK_LAYERS = [
{
"layer": "Layer 1 — Configuration",
"responsibility": "Environment URLs, credentials, browser settings, timeouts",
"files": ["config/settings.py", ".env", "config/browsers.py"],
"depends_on": "Nothing — this is the foundation",
"changes_when": "Environment changes (new staging URL, timeout adjustment)",
"example": (
"class Settings:\n"
" BASE_URL = os.getenv('BASE_URL', 'https://staging.app.com')\n"
" BROWSER = os.getenv('BROWSER', 'chrome')\n"
" HEADLESS = os.getenv('HEADLESS', 'true') == 'true'\n"
" TIMEOUT = int(os.getenv('TIMEOUT', '10'))"
),
},
{
"layer": "Layer 2 — Driver Management",
"responsibility": "Browser lifecycle: create, configure, and destroy WebDriver instances",
"files": ["conftest.py", "utils/driver_factory.py"],
"depends_on": "Layer 1 (Configuration)",
"changes_when": "New browser support, Grid integration, cloud provider switch",
"example": (
"@pytest.fixture(scope='function')\n"
"def browser():\n"
" driver = DriverFactory.create(Settings.BROWSER, Settings.HEADLESS)\n"
" yield driver\n"
" driver.quit()"
),
},
{
"layer": "Layer 3 — Page Objects",
"responsibility": "Page locators and user actions — the application model",
"files": ["pages/base_page.py", "pages/login_page.py", "pages/cart_page.py"],
"depends_on": "Layer 2 (Driver — receives driver instance)",
"changes_when": "UI changes (locator updates, new page features)",
"example": (
"class LoginPage(BasePage):\n"
" USERNAME = (By.ID, 'user-name')\n"
" def login(self, user, pwd):\n"
" self.type_text(self.USERNAME, user)\n"
" ..."
),
},
{
"layer": "Layer 4 — Test Cases",
"responsibility": "Business logic assertions — WHAT to verify",
"files": ["tests/test_login.py", "tests/test_checkout.py"],
"depends_on": "Layer 3 (Page Objects) + Layer 1 (test data)",
"changes_when": "Business rules change, new features added",
"example": (
"def test_valid_login(browser):\n"
" inventory = LoginPage(browser).open().login('user', 'pass')\n"
" assert inventory.get_product_count() == 6"
),
},
{
"layer": "Layer 5 — Reporting & Utilities",
"responsibility": "Test reports, logging, screenshots, test data generation",
"files": ["utils/reporter.py", "utils/helpers.py", "utils/data_generator.py"],
"depends_on": "Layers 1-4 (cross-cutting concern)",
"changes_when": "New reporting tool, additional diagnostics needed",
"example": (
"def on_test_failure(driver, test_name):\n"
" driver.save_screenshot(f'reports/{test_name}.png')\n"
" log.error(f'FAIL: {test_name}')"
),
},
]
# Complete project structure
PROJECT_MAP = """
selenium-framework/
config/
settings.py # Layer 1: env vars, URLs, timeouts
browsers.py # Layer 1: browser-specific options
pages/
base_page.py # Layer 3: shared methods (click, type, wait)
login_page.py # Layer 3: login locators + actions
inventory_page.py # Layer 3: product page locators + actions
cart_page.py # Layer 3: cart locators + actions
tests/
conftest.py # Layer 2: fixtures (browser, base_url)
test_login.py # Layer 4: login test cases
test_cart.py # Layer 4: cart test cases
test_checkout.py # Layer 4: checkout test cases
utils/
driver_factory.py # Layer 2: create drivers (local/Grid/cloud)
reporter.py # Layer 5: screenshot + log on failure
data_generator.py # Layer 5: random test data
helpers.py # Layer 5: scroll, retry, file utilities
test_data/
users.json # Test data files
products.csv # Test data files
reports/ # Generated (in .gitignore)
.env # Secrets (in .gitignore)
pytest.ini # pytest config
requirements.txt # Dependencies
Dockerfile # CI container
docker-compose.yml # Grid + tests
"""
print("Five-Layer Framework Architecture")
print("=" * 65)
for layer in FRAMEWORK_LAYERS:
print(f"\n {layer['layer']}")
print(f" Responsibility: {layer['responsibility']}")
print(f" Files: {', '.join(layer['files'])}")
print(f" Changes when: {layer['changes_when']}")
print(f"\n\nProject Structure:\n{PROJECT_MAP}")
DriverFactory class in Layer 2 to centralise all driver creation logic. The factory reads the browser name from configuration and returns the appropriate WebDriver — Chrome, Firefox, Edge, Remote (Grid), or cloud provider. When your team adds Safari support or switches from BrowserStack to LambdaTest, you edit the factory only. No test or page object changes. This factory pattern is the single most effective way to make your framework portable across environments.Common Mistakes
Mistake 1 — Putting driver creation logic inside page objects
❌ Wrong: LoginPage.__init__ creates a webdriver.Chrome() — the page object is now tied to Chrome and cannot be used with Grid or other browsers.
✅ Correct: The driver is created in a fixture (Layer 2) and passed to the page object constructor. The page object does not know or care how the driver was created.
Mistake 2 — Mixing configuration values across multiple layers
❌ Wrong: Base URL hardcoded in page objects, timeout hardcoded in tests, browser name hardcoded in conftest.py — three places to update for every environment.
✅ Correct: All configuration in config/settings.py loaded from environment variables. Every other layer reads from Settings. One source of truth.