Skip to main content
SeleniumDecoded

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.

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
src/main/java/com/example/utils/ConfigReader.java
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=false
utils/config.py
import os
import yaml
from 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: false
config/config.js
require('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=false
Utils/ConfigReader.cs
using 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
src/main/java/com/example/utils/DriverFactory.java
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();
}
}
}
utils/driver_factory.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from 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 = None
utils/driverFactory.js
const { 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 };
Utils/DriverFactory.cs
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
src/test/java/com/example/tests/BaseTest.java
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();
}
}
tests/conftest.py
import pytest
from utils.driver_factory import DriverFactory
from utils.config import config
@pytest.fixture
def 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_url
tests/setup.js
const { 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 };
Tests/BaseTest.cs
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

  1. Separate concerns - Keep pages, tests, and utilities in separate directories
  2. Use configuration files - Never hardcode URLs, credentials, or timeouts
  3. Environment variables - Override configs for different environments
  4. Thread safety - Use ThreadLocal for parallel test execution
  5. Gitignore properly - Exclude reports, screenshots, and sensitive configs
  6. Document your structure - Add a README explaining the organization

Next Steps