Test Automation Framework Example: Build a Scalable Suite
Theory is easy. Seeing a real structure is better. Follow this test automation framework example and copy the parts that fit your project.
- Separate tests, page objects, data and config.
- Put each locator in one place only.
- Use fixtures to set up and clean data.
- Run the suite in CI and keep reports for failures.
Many teams start with a few tests in one file. It works for a week. Then the suite grows, locators repeat and one small UI change breaks twenty tests.
A good framework prevents that. Below is a simple, honest example using Playwright and TypeScript. The ideas work with any tool, so you can adapt them to Selenium, Cypress or another stack.
What we are building
We will automate a small online shop. The suite checks login, search and add to cart. We want it to be easy to read, quick to run and simple to extend when new pages appear.
Step 1: Set up the folder layout
A clear layout is half the battle. Here is a structure that works well for small and medium projects:
shop-tests/
├─ tests/
│ ├─ auth/login.spec.ts
│ ├─ cart/add-to-cart.spec.ts
│ └─ search/search.spec.ts
├─ pages/
│ ├─ LoginPage.ts
│ ├─ ProductPage.ts
│ └─ CartPage.ts
├─ fixtures/
│ └─ test-fixtures.ts
├─ data/
│ └─ users.json
├─ utils/
│ └─ api-helpers.ts
├─ playwright.config.ts
└─ .env.example
- tests/ holds the test files, grouped by feature.
- pages/ holds page objects with locators and actions.
- fixtures/ holds shared setup, such as a logged-in user.
- data/ holds test data, never hard-coded in tests.
- utils/ holds small helpers, such as calling the API to create data.
Step 2: Keep config and data outside the tests
Put the base URL, timeouts and browser list in one config file. Keep secrets in environment variables.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
retries: process.env.CI ? 1 : 0,
reporter: [['html'], ['list']],
use: {
baseURL: process.env.BASE_URL || 'https://staging.shop.example.com',
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
},
});
Store test users in a JSON file so you can change them without touching test code:
// data/users.json
{
"standard": { "email": "buyer@example.com", "password": "from-env" }
}
Step 3: Build page objects
A page object wraps one page. It keeps locators and actions in one spot. When the UI changes, you edit one file.
// pages/LoginPage.ts
import { Page, expect } from '@playwright/test';
export class LoginPage {
constructor(private page: Page) {}
async open() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Sign in' }).click();
}
async expectLoggedIn() {
await expect(this.page.getByText('My account')).toBeVisible();
}
}
Rule of thumb: Page objects hold actions and locators. Tests hold the checks that explain the business rule.
Step 4: Share setup with fixtures
Fixtures give every test the objects it needs, already set up. This removes copy and paste.
// fixtures/test-fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { CartPage } from '../pages/CartPage';
type Pages = { loginPage: LoginPage; cartPage: CartPage };
export const test = base.extend<Pages>({
loginPage: async ({ page }, use) => { await use(new LoginPage(page)); },
cartPage: async ({ page }, use) => { await use(new CartPage(page)); },
});
export { expect } from '@playwright/test';
Step 5: Write short, clear tests
// tests/cart/add-to-cart.spec.ts
import { test, expect } from '../../fixtures/test-fixtures';
test('shopper can add a product to the cart', async ({ page, cartPage }) => {
await page.goto('/products/running-shoes');
await page.getByRole('button', { name: 'Add to cart' }).click();
await cartPage.open();
await expect(cartPage.itemNames()).toContainText(['Running shoes']);
});
The test reads like a sentence. A new teammate can understand it in seconds. For more rules on stable tests, read our guide to end to end testing best practices.
Step 6: Add useful reports
A failing test should tell you why. Turn on screenshots, video or traces for failures only, so you keep storage small. The built-in HTML report from Playwright is a good start. Later, you can send results to a dashboard or chat channel.
Step 7: Run it in CI
Here is a simple GitHub Actions job that installs dependencies, runs the suite and saves the report:
name: e2e
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
New to pipelines? Our GitHub Actions guide explains each part.
How to keep the framework scalable
- One locator, one home. Never repeat a locator across tests.
- Small tests. One test, one behaviour.
- Independent tests. Create the data you need through the API, then clean it up.
- Tag your tests. Use tags like
@smoketo run a fast subset on every pull request. - Review often. Remove dead tests and fix flaky ones each sprint.
Still choosing a tool? See how to pick the best test automation framework first.
Frequently asked questions
What is a good test automation framework structure?
Separate your tests, page objects, fixtures, data and config into their own folders. This keeps each file small and easy to find.
Do I need the page object model?
It is not required, but it helps a lot once you have more than a handful of tests. It removes repeated locators and makes updates simple.
Can I use this layout with Selenium or Cypress?
Yes. The folders and ideas stay the same. Only the syntax inside the files changes.
How many tests should run on each pull request?
Run a small smoke set on every pull request and the full suite on merges or at night. That balances speed and safety.
Keep learning
Need help with testing, WordPress or SEO?
We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.