Selenium Project Structure — Folders, Configuration and Day-One Best Practices

A single test file works for learning, but a real Selenium project needs structure — organised folders for tests, page objects, utilities, configuration, and test data. Getting the project structure right on day one saves hours of refactoring later. This lesson establishes the standard folder layout, configuration management, and best practices that professional automation teams use to build Selenium projects that scale from 10 tests to 1,000.

Standard Selenium Project Structure

Whether you use Python or Java, the project structure follows the same logical organisation. Each folder has a single responsibility, making the codebase navigable for new team members and maintainable over time.

# ── Recommended Python + pytest project structure ──

PROJECT_STRUCTURE = {
    "selenium-project/": {
        "conftest.py": "Shared pytest fixtures (browser, base_url, config)",
        "pytest.ini": "pytest configuration (markers, options, paths)",
        "requirements.txt": "Python dependencies (selenium, pytest, etc.)",
        ".env": "Environment variables (base_url, credentials — NOT in Git)",

        "config/": {
            "__init__.py": "",
            "settings.py": "Load env vars, browser options, timeouts",
        },

        "pages/": {
            "__init__.py": "",
            "base_page.py": "BasePage class — shared methods (click, type, wait)",
            "login_page.py": "LoginPage — locators + actions for login page",
            "inventory_page.py": "InventoryPage — locators + actions for products",
        },

        "tests/": {
            "__init__.py": "",
            "test_login.py": "Login test cases (valid, invalid, empty)",
            "test_cart.py": "Cart test cases (add, remove, quantities)",
            "test_checkout.py": "Checkout test cases (complete flow)",
        },

        "utils/": {
            "__init__.py": "",
            "helpers.py": "Utility functions (screenshots, random data)",
            "custom_waits.py": "Custom expected conditions",
        },

        "test_data/": {
            "users.json": "Test user credentials and profiles",
            "products.json": "Test product data",
        },

        "reports/": {
            "": "Generated test reports (HTML, screenshots) — in .gitignore",
        },
    },
}

# ── conftest.py — the heart of the project ──
CONFTEST_EXAMPLE = '''
import pytest
from selenium import webdriver
from config.settings import Settings


@pytest.fixture(scope="function")
def browser():
    """Create a browser instance for each test."""
    options = webdriver.ChromeOptions()
    if Settings.HEADLESS:
        options.add_argument("--headless=new")
    options.add_argument("--no-sandbox")
    options.add_argument("--window-size=1920,1080")

    driver = webdriver.Chrome(options=options)
    driver.implicitly_wait(Settings.IMPLICIT_WAIT)

    yield driver

    driver.quit()


@pytest.fixture
def base_url():
    """Return the base URL from configuration."""
    return Settings.BASE_URL
'''

# ── config/settings.py ──
SETTINGS_EXAMPLE = '''
import os
from dotenv import load_dotenv

load_dotenv()  # Load .env file

class Settings:
    BASE_URL      = os.getenv("BASE_URL", "https://www.saucedemo.com")
    HEADLESS      = os.getenv("HEADLESS", "true").lower() == "true"
    IMPLICIT_WAIT = int(os.getenv("IMPLICIT_WAIT", "5"))
    EXPLICIT_WAIT = int(os.getenv("EXPLICIT_WAIT", "10"))
    BROWSER       = os.getenv("BROWSER", "chrome")
'''

# Print the structure
def print_structure(tree, indent=0):
    for name, content in tree.items():
        if isinstance(content, dict):
            print(f"{'  ' * indent}{name}")
            print_structure(content, indent + 1)
        else:
            desc = f"  # {content}" if content else ""
            print(f"{'  ' * indent}{name}{desc}")

print("Selenium Project Structure")
print("=" * 60)
print_structure(PROJECT_STRUCTURE)
Note: The conftest.py file is pytest’s built-in mechanism for sharing fixtures across multiple test files. Any fixture defined in conftest.py is automatically available to every test in the same directory and its subdirectories — no imports needed. This is where your browser fixture, base URL, and configuration should live. As your project grows, you can create additional conftest.py files in subdirectories for module-specific fixtures.
Tip: Store sensitive configuration (URLs, credentials, API keys) in a .env file that is listed in .gitignore — never commit credentials to version control. Use the python-dotenv library to load these values at runtime. For CI/CD, inject the same variables as pipeline secrets (GitHub Actions secrets, Jenkins credentials, GitLab CI variables). This pattern keeps your test code environment-agnostic: the same code runs against staging, QA, and production by changing only environment variables.
Warning: Do not put test data (usernames, product names, expected values) directly inside test functions. Hardcoded data makes tests brittle and difficult to update. Store test data in JSON or YAML files in the test_data/ folder and load it in your tests or fixtures. This separation means updating a test username requires changing one data file, not editing 30 test functions.

Common Mistakes

Mistake 1 — Putting all tests in a single file

❌ Wrong: A single test_everything.py file with 200 test functions covering login, cart, checkout, profile, and admin.

✅ Correct: One test file per feature or page: test_login.py, test_cart.py, test_checkout.py. This keeps files manageable, enables running tests by file, and allows multiple team members to work on different features without merge conflicts.

Mistake 2 — Committing browser driver binaries to version control

❌ Wrong: Checking chromedriver.exe into your Git repository, where it becomes outdated immediately and adds 20MB of binary to the repo.

✅ Correct: Relying on Selenium Manager (4.6+) to handle driver downloads automatically, or documenting the setup command in the README. Add driver binaries to .gitignore.

🧠 Test Yourself

Why should test credentials and base URLs be stored in a .env file rather than hardcoded in test scripts?