Build an Automation Framework From Scratch

Building your own automation framework does two things no course can: it teaches you how every layer fits together, and it gives you something real to show — and explain — in interviews. This roadmap takes you from an empty folder to a Jenkins-run, parallel, data-driven Selenium framework in about 30 days, one stage at a time, with the code for each stage and a checklist to finish before moving on. The finished design is described in The Complete Automation Framework Guide.

Advertisement

Before You Start

  • Prerequisites: core Java (OOP, collections, exceptions) and Selenium basics (locators, waits). If you need them: Java for Testers and The Complete Selenium Guide.
  • Application under test: a stable demo site built for automation, such as SauceDemo — login, products, cart and checkout make a perfect end-to-end flow.
  • Time: about 1–2 hours a day. Commit to Git at the end of every stage.

Stage 0 — Decide the Stack

Decision Recommended Why
Framework type Hybrid: Page Object Model + data-driven The most common enterprise design — and the easiest to explain
Language / tool Java + Selenium 4 Most requested in job descriptions
Test runner TestNG Groups, parameters, DataProviders, parallel runs, listeners
Build Maven Dependencies and one-command runs
Reports / logs Extent Reports, Log4j2 Readable results and CI debugging
Data Properties + JSON (or Excel via Apache POI) Config and test data outside the code
CI Git + Jenkins Automatic runs and history

Add BDD (Cucumber) or keyword-driven layers only if you have a real reason — and only claim them in interviews if you built them. Want to reason about the architecture before coding? Try the Test Architecture Design tool.

Stage 1 — Project Skeleton (Days 1–2)

shop-automation/
├── pom.xml
├── src/main/java/com/shop/
│   ├── base/      DriverFactory.java, BasePage.java
│   ├── pages/     LoginPage.java, ProductsPage.java, CartPage.java
│   └── utils/     ConfigReader.java, JsonData.java, ScreenshotUtil.java
├── src/test/java/com/shop/
│   ├── tests/     BaseTest.java, LoginTest.java, CheckoutTest.java
│   └── listeners/ TestListener.java
└── src/test/resources/
    ├── config/    qa.properties
    ├── testdata/  users.json
    ├── log4j2.xml
    └── testng.xml

Dependencies in pom.xml: selenium-java, testng, extentreports, log4j-core, jackson-databind (and poi-ooxml for Excel). Keep versions in <properties> and take the latest from Maven Central. Add the Maven Surefire plugin pointing at testng.xml. Selenium 4.6+ downloads browser drivers itself — no driver setup code.

✅ Done when: mvn test runs an empty TestNG class successfully.

Stage 2 — Configuration and Driver (Days 3–4)

# src/test/resources/config/qa.properties
baseUrl=https://www.saucedemo.com/
browser=chrome
headless=false
timeoutSeconds=10
public final class ConfigReader {
    private static final Properties PROPS = new Properties();

    static {
        String env = System.getProperty("env", "qa");
        try (InputStream in = ConfigReader.class.getResourceAsStream("/config/" + env + ".properties")) {
            PROPS.load(in);
        } catch (IOException | NullPointerException e) {
            throw new IllegalStateException("No config for env " + env, e);
        }
    }

    public static String get(String key) {
        return System.getProperty(key, PROPS.getProperty(key));
    }
}

public final class DriverFactory {
    private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();

    public static void init() {
        boolean headless = Boolean.parseBoolean(ConfigReader.get("headless"));
        WebDriver driver;

        if ("firefox".equals(ConfigReader.get("browser"))) {
            FirefoxOptions o = new FirefoxOptions();
            if (headless) o.addArguments("-headless");
            driver = new FirefoxDriver(o);
        } else {
            ChromeOptions o = new ChromeOptions();
            if (headless) o.addArguments("--headless=new", "--window-size=1920,1080");
            driver = new ChromeDriver(o);
        }

        DRIVER.set(driver);
    }

    public static WebDriver get() {
        return DRIVER.get();
    }

    public static void quit() {
        if (DRIVER.get() != null) {
            DRIVER.get().quit();
            DRIVER.remove();
        }
    }
}

Use a ThreadLocal driver from day one — adding it later means touching every class. Keep passwords out of the properties file: read them from environment variables (System.getenv("APP_PASSWORD")) or Jenkins credentials.

✅ Done when: mvn test -Dbrowser=firefox -Denv=qa opens Firefox on the base URL.

Stage 3 — Page Object Model (Days 5–8) ⭐

public abstract class BasePage {
    protected final WebDriver driver = DriverFactory.get();

    private final WebDriverWait wait = new WebDriverWait(
        driver,
        Duration.ofSeconds(Long.parseLong(ConfigReader.get("timeoutSeconds")))
    );

    protected void click(By by) {
        wait.until(ExpectedConditions.elementToBeClickable(by)).click();
    }

    protected void type(By by, String text) {
        WebElement e = visible(by);
        e.clear();
        e.sendKeys(text);
    }

    protected String text(By by) {
        return visible(by).getText().trim();
    }

    protected WebElement visible(By by) {
        return wait.until(ExpectedConditions.visibilityOfElementLocated(by));
    }
}

public class LoginPage extends BasePage {
    private final By username = By.id("user-name");
    private final By password = By.id("password");
    private final By loginBtn = By.id("login-button");
    private final By error = By.cssSelector("[data-test='error']");

    public ProductsPage loginAs(String user, String pass) {
        type(username, user);
        type(password, pass);
        click(loginBtn);
        return new ProductsPage();
    }

    public String errorMessage() {
        return text(error);
    }
}

One class per page or component; locators as By fields; methods named after user actions; no assertions in page objects. Build LoginPage, ProductsPage, CartPage and CheckoutPage. More: Frameworks & POM.

✅ Done when: no test class contains a locator or findElement call.

Stage 4 — Tests With TestNG (Days 9–12)

public class BaseTest {

    @BeforeMethod(alwaysRun = true)
    public void setUp() {
        DriverFactory.init();
        DriverFactory.get().get(ConfigReader.get("baseUrl"));
    }

    @AfterMethod(alwaysRun = true)
    public void tearDown() {
        DriverFactory.quit();
    }
}

public class LoginTest extends BaseTest {

    @Test(groups = {"smoke"})
    public void validLoginShowsProducts() {
        ProductsPage products = new LoginPage()
            .loginAs("standard_user", System.getenv("APP_PASSWORD"));

        Assert.assertEquals(products.title(), "Products");
    }

    @Test(groups = {"regression"})
    public void lockedOutUserSeesError() {
        LoginPage login = new LoginPage();
        login.loginAs("locked_out_user", System.getenv("APP_PASSWORD"));

        Assert.assertTrue(login.errorMessage().contains("locked out"));
    }
}

Tag tests with groups, keep each test independent (fresh browser per test), and write an end-to-end checkout test. More: TestNG Essentials.

✅ Done when: mvn test -Dgroups=smoke runs only smoke tests, and every test passes when run alone or in any order.

Stage 5 — Data-Driven Testing (Days 13–15)

[
  { "username": "standard_user",   "expected": "Products" },
  { "username": "locked_out_user", "expected": "locked out" }
]
@DataProvider(name = "users")
public Object[][] users() throws IOException {
    List<Map<String, String>> rows = new ObjectMapper().readValue(
        getClass().getResourceAsStream("/testdata/users.json"),
        new TypeReference<>() {}
    );

    return rows.stream()
        .map(r -> new Object[]{ r })
        .toArray(Object[][]::new);
}

@Test(dataProvider = "users")
public void loginOutcome(Map<String, String> row) {
    /* log in as row.get("username"), check row.get("expected") */
}

List<Map<String, String>> — each map is one test record — is the structure interviewers expect you to recognize. Excel works the same way via Apache POI: Data-Driven Testing.

✅ Done when: adding a JSON row adds a test run without code changes.

Stage 6 — Reports, Logs and Screenshots (Days 16–19)

Add a TestNG listener that creates an Extent test per method, marks pass/fail/skip, embeds a Base64 screenshot on failure and flushes the report at the end; configure Log4j2 with thread names in the pattern. The complete code is in Extent Reports, Parallel Runs & CI/CD.

✅ Done when: a deliberately failing test produces a report entry with its screenshot and a log line explaining the failure.

Stage 7 — Parallel Execution (Days 20–21)

<suite name="Regression" parallel="classes" thread-count="3">
  <listeners>
    <listener class-name="com.shop.listeners.TestListener"/>
  </listeners>

  <test name="All">
    <packages>
      <package name="com.shop.tests"/>
    </packages>
  </test>
</suite>

Because the driver is ThreadLocal and each test creates its own browser and data, parallel runs should just work. If tests fail only in parallel, look for shared static state or shared test data. More: Parallel, Grid & Cross-Browser.

✅ Done when: the suite passes three times in a row with thread-count="3" and runs noticeably faster.

Stage 8 — Git and Jenkins (Days 22–25)

Push to GitHub with a .gitignore for target/, reports, screenshots and logs. Add a Jenkinsfile that runs mvn clean test -Denv=qa -Dheadless=true, publishes the TestNG/JUnit results and the Extent report, and archives screenshots — smoke on every push, regression nightly. Optionally point it at a Selenium Grid in Docker. Guides: Git · Jenkins · Docker + Selenium Grid.

✅ Done when: a push triggers a headless Jenkins run with a published report.

Stage 9 — README and Explanation (Days 26–30)

  • Write a README: what it tests, the stack, the folder structure, how to run it (locally, by group, by environment, in parallel) and how to add a test.
  • Practice the 2-minute explanation: type and stack → layers → execution → reporting and CI → one challenge you solved.
  • Prepare follow-ups: where each OOP concept is used, how parallel runs are made safe, how flaky tests are handled, what you'd improve next.

Guides: How to Explain Your Framework · OOP in Your Framework.

Timeline at a Glance

Stage Days Deliverable
0 — Stack decided
1 1–2 Maven skeleton that runs
2 3–4 Config per environment + ThreadLocal driver
3 5–8 BasePage with waits + 4 page objects
4 9–12 Smoke and regression tests, end-to-end checkout
5 13–15 JSON/Excel-driven tests
6 16–19 Extent report, logs, failure screenshots
7 20–21 Stable parallel runs
8 22–25 GitHub + Jenkins pipeline
9 26–30 README and a confident explanation

Six Mistakes to Avoid

  1. Selenium code in test classes — keep it in page objects.
  2. Hard-coded URLs and credentials — config files and environment variables.
  3. Thread.sleep() — explicit-wait helpers in BasePage.
  4. A static shared driver — ThreadLocal from the start.
  5. Tests that depend on each other — each test sets up its own state.
  6. Claiming features you didn't build — interviewers ask follow-ups.

From Real Projects

In interviews for testing and automation roles, I've found the questions come back to real work: the application you tested, how you wrote and reviewed test cases, how you tracked execution and defects (for me, TestRail and Jira), and how your automation was structured (Selenium, Java, TestNG, POM and a business library). Prepare one short, honest example from your own projects for each topic on this page. On Canolog, every department — sales, inventory, finance, service and parts — connects to the others, so a change in one module had to be checked in the flows that depend on it. Build the framework in small steps and keep it running at every stage.

FAQs

How long does it take to build an automation framework from scratch?

About 3–5 weeks at 1–2 hours a day for a complete Selenium + TestNG framework with data, reports, parallel runs and Jenkins.

Which framework type should I build?

A hybrid of Page Object Model and data-driven testing — the most common design in real projects and the easiest to explain.

Should I use PageFactory or By locators?

Plain By locators with explicit-wait helpers are simpler and avoid NullPointerException and stale-element issues; PageFactory is fine if you understand initElements().

Which website should I automate?

One built for practice and stable enough to test repeatedly, such as SauceDemo — never a live commercial site.

What should a framework README include?

What it tests, the stack, the structure, how to run it with different options, and how to add a new test.

Next Steps