GUIDE / automation
How to Build a Test Automation Framework from Scratch
Learn how to build a test automation framework from scratch with layers, design patterns, reporting, CI/CD hooks, and a practical starter architecture.
If you need to build a test automation framework from scratch, do not start by creating twenty abstract base classes. Start by deciding what the framework must make easy: writing reliable tests, running them in CI, diagnosing failures, and changing the product without rewriting the suite. A framework is not a trophy architecture. It is a set of conventions and shared tools that keep automation valuable after the demo day glow fades.
This guide shows a practical way to design a hybrid, scalable test automation framework: goals, layers, folder structure, design patterns, configuration, reporting, logging, CI integration, and the mistakes that create unmaintainable platforms. The examples lean toward Playwright and TypeScript, but the architecture applies to Selenium, Cypress, REST clients, and mixed suites.
What a Test Automation Framework Really Is
A test automation framework is the reusable structure around your tests. It usually includes:
- A test runner and assertion library
- Project layout and naming conventions
- Shared setup and teardown
- UI page objects or API clients
- Test data strategy
- Configuration for environments and secrets
- Logging, screenshots, traces, and reports
- CI commands and artifact publishing
It is not merely "Selenium plus TestNG" or "Playwright plus a utils folder." Tools are ingredients. The framework is how your team cooks with them every day.
Framework vs Test Suite vs Tool
| Term | Meaning | Example |
|---|---|---|
| Tool | Browser or protocol driver | Playwright, Selenium, REST client |
| Suite | The tests you own | Smoke, regression, API contract packs |
| Framework | Shared architecture and conventions | Fixtures, POM, config, reporters, CI glue |
Teams often blame tools for framework problems. Switching from Selenium to Playwright will not fix random waits, shared mutable test users, or missing reports.
Decide Goals Before Structure
Write the non negotiable outcomes first.
Typical goals:
- Critical user journeys are covered by reliable end to end checks.
- Most regression signal comes from fast API or component tests.
- Any engineer can add a test without copying brittle setup.
- Failures in CI are diagnosable within minutes.
- Tests can run in parallel against multiple environments.
- Secrets never live in the repository.
Constraints matter as much as goals:
- Language the team already knows
- App architecture (monolith, microservices, SPA)
- Release cadence
- Browser matrix requirements
- Compliance or audit needs
If you skip this step, you will invent abstractions for imaginary future teams and under invest in the pain you have today.
Choose the Right Framework Shape
Linear Scripts
Fine for spikes and throwaway bug repros. Not a framework.
Modular Framework
Shared libraries for login, navigation, and assertions, but limited formal layering. Good early stage choice.
Data Driven Framework
Same flow, many data rows. Excellent for validation matrices and calculation heavy features.
Keyword Driven Framework
Business readable keywords map to actions. Powerful for some enterprises, heavy for most product teams.
Hybrid Framework
Combines page objects or API clients, data factories, fixtures, and selective data driven tables. This is the shape most modern product teams should target.
| Shape | Strength | Weakness | Best use |
|---|---|---|---|
| Modular | Fast to start | Can become messy | Small teams |
| Data driven | High input coverage | Weak for complex journeys | Forms, pricing, rules |
| Keyword driven | Non engineer authoring | High framework cost | Large manual heavy orgs |
| Hybrid | Balanced speed and scale | Needs discipline | Most product engineering teams |
The Layers of a Test Automation Framework
A clean hybrid architecture usually has these layers.
1. Spec Layer
Contains scenarios and expectations. Specs should read like product behavior.
test("buyer can checkout with valid card", async ({ buyer, shop }) => {
await shop.addItem("Trail Runner");
await shop.checkoutWithSavedCard();
await expect(shop.confirmation).toBeVisible();
});
2. Interaction Layer
UI page objects, component objects, and API clients live here. This layer knows how to talk to the system. See the full pattern guide on page object model.
3. Fixture and Lifecycle Layer
Creates authenticated contexts, browser state, temporary users, seeded records, and cleanup hooks.
4. Test Data Layer
Builders and factories produce realistic, unique data. Avoid a single shared spreadsheet user for parallel runs.
5. Configuration Layer
Environment URLs, feature flags, timeouts, browser projects, and secret injection.
6. Support Utilities
Date helpers, money formatting matchers, retry wrappers for known third party flakes, file download helpers.
When downloads are part of product value, design those helpers with automated file download testing in mind instead of leaving them as ad hoc assertions.
7. Reporting and Observability Layer
HTML reports, JUnit output, traces, screenshots, logs correlated by test name.
8. Execution Layer
Local scripts, CI workflows, sharding, and artifact upload. Wire this early with CI/CD for test automation with GitHub Actions.
Starter Architecture You Can Copy
automation/
package.json
playwright.config.ts
.env.example
src/
config/
env.ts
pages/
login.page.ts
checkout.page.ts
api/
users.client.ts
orders.client.ts
fixtures/
index.ts
auth.fixture.ts
data/
builders/
user.builder.ts
product.builder.ts
utils/
money.ts
wait.ts
tests/
smoke/
login.smoke.spec.ts
regression/
checkout.spec.ts
api/
orders.api.spec.ts
reports/
test-results/
Keep production app code and automation code boundaries clear. Some monorepos co-locate tests beside features. That can work if ownership is explicit. For many QA owned suites, a dedicated automation package remains easier to version and deploy in CI.
Step by Step: Build the Framework
Step 1: Create a Thin Runnable Skeleton
Install the runner, add one passing smoke test, and run it in CI on day one. A framework that only runs on laptops is incomplete.
Proof goals for day one:
npm testworks locally- One workflow runs on pull requests
- Failure artifacts are stored
Step 2: Centralize Configuration
// src/config/env.ts
export const env = {
baseURL: process.env.BASE_URL ?? "http://localhost:3000",
apiURL: process.env.API_URL ?? "http://localhost:3000/api",
adminEmail: process.env.ADMIN_EMAIL ?? "",
adminPassword: process.env.ADMIN_PASSWORD ?? "",
};
Rules:
- No production secrets in git
.env.exampledocuments required keys- Config fails fast when mandatory values are missing in CI
Step 3: Add Fixtures Before Utilities Bloat
Fixtures beat ad hoc setup helpers because they compose cleanly and clean up predictably.
import { test as base } from "@playwright/test";
import { LoginPage } from "../pages/login.page";
type Fixtures = {
loginPage: LoginPage;
buyerStorageState: string;
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
});
Later you can add authenticated contexts, API clients, and feature flags without rewriting every spec.
Step 4: Introduce Interaction Objects
Start with the highest traffic screens and APIs:
- Login
- Navigation shell
- Checkout
- User creation API
- Order seeding API
Do not page object the entire app on week one. Extract when duplication appears twice, not when a blog post says every button needs a class.
Step 5: Build Data Factories
let counter = 0;
export function buildUser(overrides: Partial<User> = {}): User {
counter += 1;
return {
email: `buyer.${Date.now()}.${counter}@example.test`,
password: "ValidPass#2026",
role: "buyer",
...overrides,
};
}
Unique data is essential for parallel CI. Shared static users create cross test pollution and mysterious flakes.
Step 6: Add Hybrid Setup Paths
Use API setup for speed:
test("order history shows completed order", async ({ page, api, buyer }) => {
const order = await api.orders.createCompletedOrder(buyer.id);
await page.goto(`/orders/${order.id}`);
await expect(page.getByText("Completed")).toBeVisible();
});
Use UI only when the UI itself is under test. This hybrid approach is the practical core of a scalable automation framework for CI/CD.
Step 7: Standardize Reporting and Logging
Minimum useful set:
- HTML report for humans
- JUnit or JSON for CI annotations
- Screenshots on failure
- Traces on failure for UI tests
- Request and response logging for API tests
Name artifacts by test title and timestamp so debugging does not require psychic powers.
Step 8: Codify Conventions in Docs and Lint Rules
Write a short CONTRIBUTING-TESTS.md:
- When to write UI vs API tests
- Locator strategy
- Data cleanup expectations
- How to quarantine flaky tests
- Folder placement rules
Conventions that live only in one senior engineer's head are not framework features.
Design Patterns That Earn Their Keep
Page Object Model
Best default for UI structure. Keep methods intent based. Full guide: page object model.
Facade for APIs
export class ShopApi {
constructor(private readonly request: APIRequestContext) {}
users = new UsersClient(this.request);
orders = new OrdersClient(this.request);
}
Specs depend on shopApi.orders.create(), not on scattered raw request calls.
Builder / Factory for Data
Readable overrides beat giant optional parameter lists.
Strategy for Environment Behavior
Local may use seeded Docker services. Staging may use shared integrations. Production smoke may be read only. Encode those differences in strategy modules instead of if (env === "prod") soup inside tests.
Object Mother for Domain Setup
When creating a "buyer with two past orders and an expired card" becomes common, give that recipe a name. Named recipes reduce setup mistakes.
Framework Design With Reporting and Logging
Reporting is part of the product quality loop, not an afterthought.
What Good Failure Output Includes
- Test name and file
- Environment and browser or runtime
- Assertion message
- Screenshot or response body
- Trace or log correlation id
- Seeded entity ids when relevant
Logging Guidelines
- Log arrange steps at info level
- Log sensitive tokens only in redacted form
- Prefer structured logs over decorative print lines
- Attach logs to the test result, not only console history
Report Consumers
Different people need different views:
| Consumer | Needs |
|---|---|
| Author of the failing PR | Exact assertion and reproduction path |
| QA lead | Trend of failures by suite and area |
| Engineering manager | Signal reliability and runtime cost |
| On call engineer | Whether smoke is red in production checks |
If your report only says "expected true, received false," your framework is incomplete.
Making the Framework CI Native
Local green is not enough. Design for pipeline realities.
Parallel Safety
- Unique test data
- No shared writable accounts
- Stateless tests where possible
- Explicit cleanup or disposable tenants
Runtime Budgets
Track:
- Total suite duration
- P95 test duration
- Failure rate
- Flake rate
A framework that cannot answer "are we getting slower?" will eventually become shelfware.
Sharding and Selection
Support commands like:
npm run test:smoke
npm run test:api
npm run test:ui -- --shard=1/4
Pull requests may run smoke plus affected packs. Nightly runs may run full regression. Fail fast and full suite strategies both have a place. Details live in the GitHub Actions guide linked above.
Sample playwright.config.ts Backbone
import { defineConfig, devices } from "@playwright/test";
import { env } from "./src/config/env";
export default defineConfig({
testDir: "./tests",
timeout: 60_000,
expect: { timeout: 10_000 },
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [
["list"],
["html", { open: "never" }],
["junit", { outputFile: "reports/junit.xml" }],
],
use: {
baseURL: env.baseURL,
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
],
});
This is not the whole framework, but it encodes important policy: parallel by default, limited CI retries, and artifacts on failure.
Hybrid Test Automation Framework Architecture
A realistic hybrid model:
+-------------------+
| CI Workflows |
+---------+---------+
|
v
+-------------------+
| Test Selection |
| smoke/api/ui/full |
+---------+---------+
|
+---------------+---------------+
| |
v v
+----------------+ +----------------+
| API Specs | | UI Specs |
+--------+-------+ +--------+-------+
| |
v v
+----------------+ +----------------+
| API Facades | | Page Objects |
+--------+-------+ +--------+-------+
| |
+---------------+----------------+
|
v
+-------------------+
| Fixtures / Data |
| Factories / Env |
+-------------------+
API and UI share config, data, auth, and reporting. They do not share bad habits. UI tests should not become API tests with a browser open for decoration.
Ownership, Quality Gates, and Team Process
Technology fails without ownership.
Define:
- Who approves framework changes
- Who triages nightly failures
- How flaky tests are quarantined
- What "done" means for a new automated check
A useful definition of done for an automated test:
- Protects a real risk
- Has stable data setup
- Passes three consecutive CI runs
- Produces clear artifacts on failure
- Is documented enough for another engineer to update
Common Mistakes When You Build a Test Automation Framework
Mistake 1: Abstract Base Class Theatre
Deep inheritance trees look enterprise grade and age poorly. Prefer composition with fixtures and small helpers.
Mistake 2: Automating Everything Through the UI
End to end coverage is expensive. Push validation left to API, component, and unit layers when those layers can catch the risk faster.
Mistake 3: No Data Strategy
Hardcoded users and mutable shared state are the top cause of "framework works on my machine" syndrome.
Mistake 4: Silent Failures Without Artifacts
If CI only prints a stack trace, people stop trusting and eventually stop running the suite.
Mistake 5: Infinite Utility Folders
utils/helpers/common/misc.ts becomes a junk drawer. Name modules by purpose and delete dead code aggressively.
Mistake 6: Treating Retries as a Fix
Retries can reduce noise while you investigate, but they are not architecture. Root cause work belongs in how to fix flaky tests.
Mistake 7: Building for Imaginary Scale
You do not need multi region device farms and a custom DSL in month one. You need trustworthy smoke tests and a path to grow.
Implementation Checklist
Use this checklist while building from scratch:
- Goals and constraints written down
- Runner chosen with team skill fit
- One smoke test green locally and in CI
- Environment config centralized
- Secrets excluded from git
- Folder conventions documented
- Fixtures for auth and common setup
- Page objects or API clients for top flows
- Unique data factories
- HTML plus machine readable reports
- Failure artifacts uploaded in CI
- Smoke, regression, and API commands defined
- Parallel safety reviewed
- Flake triage process defined
- Contribution guide for new tests
30 / 60 / 90 Day Build Plan
Days 1-30
- Skeleton project
- CI smoke
- Login and one critical journey
- Basic page objects and env config
- Failure screenshots
Days 31-60
- API setup helpers
- Data builders
- Expanded regression pack
- Report publishing
- Initial parallelization
Days 61-90
- Test selection strategy for PRs vs nightly
- Flake dashboard or simple flake log
- Component or API depth where UI is overkill
- Onboarding docs and sample tests
- Runtime budget tracking
This plan produces value continuously. It avoids the six month framework program that ships no product protection.
How to Evaluate Whether Your Framework Is Working
Ask monthly:
- Do developers trust red builds?
- How long does a PR suite take?
- What percentage of failures are real defects vs flakes or env issues?
- How long does a new engineer need to add a test?
- Are high risk product areas covered?
If trust is low, improve stability and diagnostics before adding more tests. Volume without trust is liability.
Practice: Build a Mini Framework on a Real App
Pick a small application, even a QABattle arena app under test, and practice the whole loop:
- Create a repo skeleton.
- Add two smoke tests.
- Extract one page object.
- Add an API or fixture based setup.
- Run in a pipeline.
- Break a selector on purpose and confirm the failure is obvious.
Then open QABattle tracks and use automation challenges to pressure test your structure against messy UI reality. Frameworks that only work on perfect demo apps are not ready.
Naming Conventions That Reduce Friction
Frameworks fail socially when nobody can find anything. Agree on names early.
Spec Names
checkout.guest.happy.spec.ts
checkout.coupon.invalid.spec.ts
orders.list.pagination.api.spec.ts
Include product area, scenario intent, and layer. Avoid test1.spec.ts and final_final_v3.spec.ts.
Page and Client Names
LoginPage
CheckoutPage
OrdersApiClient
BillingComponent
Match product language. If the business says "workspace," do not invent "tenant console page" unless that is the real domain term.
Tag Names
@smoke
@regression
@api
@ui
@checkout
@flaky
Tags power CI selection. Keep the vocabulary short enough that people remember it.
Security and Secrets in Framework Design
Automation often needs privileged credentials. Design for least privilege.
- Separate admin setup credentials from buyer journey credentials
- Rotate secrets used in CI
- Redact auth headers in logs and HTML reports
- Avoid printing full response bodies when they contain PII
- Provide a fake payment mode for non production environments
A framework that leaks tokens in artifacts is not "done," no matter how elegant the folder tree looks.
Example: Adding a New Feature Without Chaos
Suppose the product adds "save for later" on cart items. Framework workflow:
- Identify risk: item state transitions, inventory, logged in vs guest.
- Decide layers: API tests for state transitions, one UI smoke for the drawer.
- Add data builder support if new entities appear.
- Extend
CartPagewithsaveForLater(itemName)andmoveToCart(itemName). - Add API client methods for setup if UI arrange is slow.
- Tag the critical path
@smokeand deeper cases@regression. - Confirm artifacts on intentional failure.
- Document the new methods in the contribution guide if the pattern is new.
That is framework use in the wild. The architecture exists so this process stays boring and repeatable.
Final Takeaway
To build a test automation framework from scratch, optimize for team speed and signal quality, not for pattern count. Define goals, create a thin runnable core, layer specs above interaction objects, invest in fixtures and unique data, and make CI reporting excellent. Grow hybrid API plus UI capabilities as soon as setup time becomes the bottleneck.
A great framework feels boring in the best way: new tests are easy to add, failures are easy to read, and product changes do not require archaeological refactors. Start small, keep the layers honest, and let real suite pain decide the next abstraction.
FAQ
Questions testers ask
How do you design a test automation framework from scratch?
Start from goals and constraints, not folders. Choose the test layers you need, pick a runner, define package structure, add fixtures and data factories, introduce page objects or API clients, then wire reporting and CI. Grow only the abstractions your suite actually reuses.
What are the layers of a test automation framework?
Common layers are test specs, page or API interaction objects, fixtures and setup, test data, utilities, configuration, reporting, and CI execution. Each layer has one job. Specs describe intent. Interaction objects talk to the system. Infrastructure supports repeatable runs.
What design patterns are used in automation frameworks?
Frequent patterns include Page Object Model, Screenplay style tasks, factory builders for test data, fixture based dependency injection, facade clients for APIs, and strategy objects for environment configuration. Use patterns to remove duplication, not to impress architecture diagrams.
Should a framework support UI and API tests together?
Yes for most product teams. A hybrid framework lets API calls arrange state quickly while UI tests verify critical journeys. Share authentication helpers, config, reporting, and data factories so the suite stays coherent without forcing every check through the browser.
When is a custom framework overkill?
If you have fewer than a handful of tests, no CI pipeline, and one maintainer, a thin project with clear folders beats a platform. Build structure when reuse, onboarding, multi environment runs, or flaky failure analysis start costing real time.
What should every scalable automation framework include?
Reliable config per environment, isolated test data, stable locators or API contracts, parallel safe design, useful logs and artifacts, deterministic setup and teardown, and a CI path that publishes failures humans can diagnose quickly.
RELATED GUIDES
Continue the route
Page Object Model: A Pattern for Maintainable Tests
Learn the page object model for maintainable UI automation tests, with Playwright examples, best practices, and common POM mistakes to avoid.
Flaky Tests: Causes and How to Fix Them
Learn how to fix flaky tests with root cause analysis, stable waits, quarantine strategy, CI retries policy, and practical Playwright examples.
CI/CD for Test Automation with GitHub Actions
Learn CI/CD for test automation with GitHub Actions: Playwright workflows, reports, sharding, and PR vs nightly pipeline strategies that scale.
Playwright Tutorial: End-to-End Testing from Scratch
Playwright tutorial for beginners: install, write your first test, TypeScript setup, codegen, fixtures, API testing, debugging, and CI tips in one guide.
Selenium vs Playwright vs Cypress in 2026
Compare Selenium vs Playwright vs Cypress in 2026 for speed, browsers, CI, flakiness, ecosystem, and which web automation framework beginners should choose.