Project Structure
Learn how to organize your Selenium test automation project for maintainability and scalability.
Selenium 3 & 4 Stable
A well-organized project structure is crucial for maintaining and scaling your test automation framework. This guide covers recommended structures for each language.
Recommended Project Structures
Java (Maven/Gradle)
selenium-project/├── pom.xml (or build.gradle)├── src/│ ├── main/│ │ └── java/│ │ └── com/example/│ │ ├── pages/ # Page Objects│ │ │ ├── BasePage.java│ │ │ ├── LoginPage.java│ │ │ └── HomePage.java│ │ ├── utils/ # Utility classes│ │ │ ├── DriverFactory.java│ │ │ ├── ConfigReader.java│ │ │ └── WaitHelper.java│ │ └── constants/ # Constants and enums│ │ └── Browsers.java│ └── test/│ ├── java/│ │ └── com/example/│ │ ├── tests/ # Test classes│ │ │ ├── BaseTest.java│ │ │ ├── LoginTest.java│ │ │ └── SearchTest.java│ │ └── data/ # Test data providers│ │ └── LoginDataProvider.java│ └── resources/│ ├── config.properties # Configuration│ ├── testng.xml # TestNG suite│ └── testdata/ # Test data files│ └── users.json├── reports/ # Test reports (gitignored)└── screenshots/ # Failure screenshots (gitignored)Python (pytest)
selenium-project/├── requirements.txt├── pytest.ini├── conftest.py # pytest fixtures├── pages/ # Page Objects│ ├── __init__.py│ ├── base_page.py│ ├── login_page.py│ └── home_page.py├── tests/ # Test files│ ├── __init__.py│ ├── conftest.py # Test-specific fixtures│ ├── test_login.py│ └── test_search.py├── utils/ # Utilities│ ├── __init__.py│ ├── driver_factory.py│ ├── config.py│ └── wait_helper.py├── data/ # Test data│ ├── users.json│ └── test_config.yaml├── reports/ # Test reports (gitignored)└── screenshots/ # Screenshots (gitignored)JavaScript (Mocha/Jest)
selenium-project/├── package.json├── .env # Environment variables├── config/│ ├── config.js│ └── browsers.js├── pages/ # Page Objects│ ├── BasePage.js│ ├── LoginPage.js│ └── HomePage.js├── tests/ # Test files│ ├── login.test.js│ └── search.test.js├── utils/ # Utilities│ ├── driverFactory.js│ ├── helpers.js│ └── waitHelper.js├── data/ # Test data│ └── testData.json├── reports/ # Reports (gitignored)└── screenshots/ # Screenshots (gitignored)C# (.NET)
SeleniumProject/├── SeleniumProject.sln├── SeleniumProject/│ ├── SeleniumProject.csproj│ ├── Pages/ # Page Objects│ │ ├── BasePage.cs│ │ ├── LoginPage.cs│ │ └── HomePage.cs│ ├── Utils/ # Utilities│ │ ├── DriverFactory.cs│ │ ├── ConfigReader.cs│ │ └── WaitHelper.cs│ ├── Tests/ # Test classes│ │ ├── BaseTest.cs│ │ ├── LoginTests.cs│ │ └── SearchTests.cs│ ├── Data/ # Test data│ │ └── TestData.cs│ └── appsettings.json # Configuration├── Reports/ # Reports (gitignored)└── Screenshots/ # Screenshots (gitignored)Essential Configuration Files
Configuration Management
Configuration Reader
Selenium 3 & 4 Stable
package com.example.utils;
import java.io.FileInputStream;import java.io.IOException;import java.util.Properties;
public class ConfigReader { private static Properties properties;
static { try { properties = new Properties(); FileInputStream fis = new FileInputStream( "src/test/resources/config.properties" ); properties.load(fis); } catch (IOException e) { throw new RuntimeException("Config file not found", e); } }
public static String get(String key) { return properties.getProperty(key); }
public static String getBaseUrl() { return get("base.url"); }
public static String getBrowser() { return get("browser"); }
public static int getTimeout() { return Integer.parseInt(get("timeout")); }}
// config.properties# base.url=https://example.com# browser=chrome# timeout=10# headless=falseimport osimport yamlfrom pathlib import Path
class Config: _instance = None _config = None
def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._load_config() return cls._instance
@classmethod def _load_config(cls): config_path = Path(__file__).parent.parent / "data" / "config.yaml" with open(config_path) as f: cls._config = yaml.safe_load(f)
@property def base_url(self): return os.getenv("BASE_URL", self._config["base_url"])
@property def browser(self): return os.getenv("BROWSER", self._config["browser"])
@property def timeout(self): return int(os.getenv("TIMEOUT", self._config["timeout"]))
@property def headless(self): return os.getenv("HEADLESS", str(self._config["headless"])).lower() == "true"
config = Config()
# data/config.yaml# base_url: https://example.com# browser: chrome# timeout: 10# headless: falserequire('dotenv').config();
const config = { baseUrl: process.env.BASE_URL || 'https://example.com', browser: process.env.BROWSER || 'chrome', timeout: parseInt(process.env.TIMEOUT) || 10000, headless: process.env.HEADLESS === 'true',
// Environment-specific settings environments: { dev: { baseUrl: 'https://dev.example.com' }, staging: { baseUrl: 'https://staging.example.com' }, prod: { baseUrl: 'https://example.com' } },
getEnvConfig(env = process.env.ENV || 'dev') { return { ...this, ...this.environments[env] }; }};
module.exports = config;
// .env file// BASE_URL=https://example.com// BROWSER=chrome// TIMEOUT=10000// HEADLESS=falseusing Microsoft.Extensions.Configuration;
public class ConfigReader{ private static IConfiguration _config;
static ConfigReader() { _config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false) .AddEnvironmentVariables() .Build(); }
public static string BaseUrl => Environment.GetEnvironmentVariable("BASE_URL") ?? _config["BaseUrl"];
public static string Browser => Environment.GetEnvironmentVariable("BROWSER") ?? _config["Browser"];
public static int Timeout => int.Parse(Environment.GetEnvironmentVariable("TIMEOUT") ?? _config["Timeout"]);
public static bool Headless => bool.Parse(Environment.GetEnvironmentVariable("HEADLESS") ?? _config["Headless"]);}
// appsettings.json// {// "BaseUrl": "https://example.com",// "Browser": "chrome",// "Timeout": "10",// "Headless": "false"// }Driver Factory
Driver Factory Pattern
Selenium 4 Stable
package com.example.utils;
import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.chrome.ChromeOptions;import org.openqa.selenium.firefox.FirefoxDriver;import org.openqa.selenium.firefox.FirefoxOptions;import org.openqa.selenium.edge.EdgeDriver;
public class DriverFactory { private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();
public static WebDriver getDriver() { if (driver.get() == null) { driver.set(createDriver()); } return driver.get(); }
private static WebDriver createDriver() { String browser = ConfigReader.getBrowser().toLowerCase(); boolean headless = Boolean.parseBoolean(ConfigReader.get("headless"));
switch (browser) { case "chrome": ChromeOptions chromeOptions = new ChromeOptions(); if (headless) chromeOptions.addArguments("--headless"); return new ChromeDriver(chromeOptions);
case "firefox": FirefoxOptions firefoxOptions = new FirefoxOptions(); if (headless) firefoxOptions.addArguments("--headless"); return new FirefoxDriver(firefoxOptions);
case "edge": return new EdgeDriver();
default: throw new IllegalArgumentException("Unknown browser: " + browser); } }
public static void quitDriver() { if (driver.get() != null) { driver.get().quit(); driver.remove(); } }}from selenium import webdriverfrom selenium.webdriver.chrome.options import Options as ChromeOptionsfrom selenium.webdriver.firefox.options import Options as FirefoxOptionsfrom utils.config import config
class DriverFactory: _driver = None
@classmethod def get_driver(cls): if cls._driver is None: cls._driver = cls._create_driver() return cls._driver
@classmethod def _create_driver(cls): browser = config.browser.lower() headless = config.headless
if browser == "chrome": options = ChromeOptions() if headless: options.add_argument("--headless") return webdriver.Chrome(options=options)
elif browser == "firefox": options = FirefoxOptions() if headless: options.add_argument("--headless") return webdriver.Firefox(options=options)
elif browser == "edge": return webdriver.Edge()
else: raise ValueError(f"Unknown browser: {browser}")
@classmethod def quit_driver(cls): if cls._driver: cls._driver.quit() cls._driver = Noneconst { Builder } = require('selenium-webdriver');const chrome = require('selenium-webdriver/chrome');const firefox = require('selenium-webdriver/firefox');const config = require('../config/config');
let driver = null;
async function getDriver() { if (!driver) { driver = await createDriver(); } return driver;}
async function createDriver() { const browser = config.browser.toLowerCase(); const headless = config.headless;
let builder = new Builder();
switch (browser) { case 'chrome': const chromeOptions = new chrome.Options(); if (headless) chromeOptions.addArguments('--headless'); return builder.forBrowser('chrome') .setChromeOptions(chromeOptions) .build();
case 'firefox': const firefoxOptions = new firefox.Options(); if (headless) firefoxOptions.addArguments('--headless'); return builder.forBrowser('firefox') .setFirefoxOptions(firefoxOptions) .build();
case 'edge': return builder.forBrowser('MicrosoftEdge').build();
default: throw new Error(`Unknown browser: ${browser}`); }}
async function quitDriver() { if (driver) { await driver.quit(); driver = null; }}
module.exports = { getDriver, quitDriver };using OpenQA.Selenium;using OpenQA.Selenium.Chrome;using OpenQA.Selenium.Firefox;using OpenQA.Selenium.Edge;
public class DriverFactory{ private static ThreadLocal<IWebDriver> _driver = new ThreadLocal<IWebDriver>();
public static IWebDriver GetDriver() { if (_driver.Value == null) { _driver.Value = CreateDriver(); } return _driver.Value; }
private static IWebDriver CreateDriver() { string browser = ConfigReader.Browser.ToLower(); bool headless = ConfigReader.Headless;
switch (browser) { case "chrome": var chromeOptions = new ChromeOptions(); if (headless) chromeOptions.AddArgument("--headless"); return new ChromeDriver(chromeOptions);
case "firefox": var firefoxOptions = new FirefoxOptions(); if (headless) firefoxOptions.AddArgument("--headless"); return new FirefoxDriver(firefoxOptions);
case "edge": return new EdgeDriver();
default: throw new ArgumentException($"Unknown browser: {browser}"); } }
public static void QuitDriver() { if (_driver.Value != null) { _driver.Value.Quit(); _driver.Value = null; } }}Base Test Class
Base Test Setup
Selenium 3 & 4 Stable
package com.example.tests;
import com.example.utils.DriverFactory;import com.example.utils.ConfigReader;import org.openqa.selenium.WebDriver;import org.testng.annotations.*;
public class BaseTest { protected WebDriver driver;
@BeforeMethod public void setUp() { driver = DriverFactory.getDriver(); driver.manage().window().maximize(); driver.get(ConfigReader.getBaseUrl()); }
@AfterMethod public void tearDown() { DriverFactory.quitDriver(); }}import pytestfrom utils.driver_factory import DriverFactoryfrom utils.config import config
@pytest.fixturedef driver(): """Provide a WebDriver instance for each test.""" driver = DriverFactory.get_driver() driver.maximize_window() driver.get(config.base_url)
yield driver
DriverFactory.quit_driver()
@pytest.fixture(scope="session")def base_url(): """Provide the base URL for tests.""" return config.base_urlconst { getDriver, quitDriver } = require('../utils/driverFactory');const config = require('../config/config');
let driver;
beforeEach(async function() { this.timeout(30000); driver = await getDriver(); await driver.manage().window().maximize(); await driver.get(config.baseUrl);});
afterEach(async function() { await quitDriver();});
module.exports = { getDriver: () => driver };using NUnit.Framework;using OpenQA.Selenium;
[TestFixture]public class BaseTest{ protected IWebDriver Driver;
[SetUp] public void SetUp() { Driver = DriverFactory.GetDriver(); Driver.Manage().Window.Maximize(); Driver.Navigate().GoToUrl(ConfigReader.BaseUrl); }
[TearDown] public void TearDown() { DriverFactory.QuitDriver(); }}Best Practices
- Separate concerns - Keep pages, tests, and utilities in separate directories
- Use configuration files - Never hardcode URLs, credentials, or timeouts
- Environment variables - Override configs for different environments
- Thread safety - Use ThreadLocal for parallel test execution
- Gitignore properly - Exclude reports, screenshots, and sensitive configs
- Document your structure - Add a README explaining the organization
Next Steps
- Page Object Model - Structure your page interactions
- Browser Options - Configure browser behavior
- Parallel Execution - Run tests concurrently