Skip to main content
/tayyab/portfolio — zsh
tayyab
TA
// dispatch.read --classified=false --access-level: public

How to Build a Test Automation Framework from Scratch (2026 Guide)

March 3, 2026 EST. READ: 18 MIN #Test Automation

How to Build a Test Automation Framework from Scratch (2026 Guide)

Most QA engineers don't build frameworks—they inherit broken ones.

I've worked on 12 projects. In 10 of them, the existing test framework was either:

  • A tangled mess of hardcoded selectors
  • Unmaintainable because nobody documented architecture
  • Impossible to scale because they didn't use Page Object Model
  • Fragile because tests had dependencies on each other

Building a framework from scratch is rare. When it happens, most teams make the same mistakes again.

This guide walks you through architecting a production-grade framework that scales, stays maintainable, and serves as a competitive advantage.

Table of Contents

  1. Before You Start: Ask the Right Questions
  2. Choose Your Tech Stack
  3. Folder Structure & Organization
  4. Page Object Model Architecture
  5. Test Data Management
  6. CI/CD Integration
  7. Real Project Example: Wells Fargo
  8. Common Mistakes to Avoid
  9. FAQ

Before You Start: Ask the Right Questions

Framework architecture depends on your specific context. Ask yourself:

1. What are we testing?

  • Web UI (browsers) → Playwright or Cypress
  • REST APIs → Playwright APIRequestContext or K6
  • Mobile apps → Appium
  • Desktop apps → WinAppDriver

2. What's our test scope?

  • Unit tests (code level) → Jest, pytest
  • Integration tests (module level) → Playwright, Selenium
  • E2E tests (complete user flow) → Playwright, Cypress
  • Load testing → K6, JMeter

3. How many people will maintain this?

  • Solo engineer → Simpler, Playwright-based
  • Team of 3-5 → Need good documentation, Page Objects
  • Large team (10+) → Need strict patterns, linting, code reviews

4. What's the test volume?

  • < 50 tests → Simple structure fine
  • 50-200 tests → Need Page Objects, clear organization
  • 200+ tests → Need layers (unit, integration, E2E), parallelization

5. What's the environment?

  • Single app → Simpler framework
  • Multiple apps → Shared utilities, centralized config
  • Microservices → API testing focus, service mocking

Choose Your Tech Stack

Framework Decision Matrix

Scenario Recommendation Why
Web UI automation, greenfield Playwright + TypeScript Modern, fast, good docs
Legacy system, IE compatibility needed Selenium Java Mature, wide browser support
API + UI testing together Playwright (both built-in) Single language, single framework
Mobile testing Appium Cross-platform support
Performance/load testing K6 JavaScript-based, CI/CD friendly
Data-heavy validation Playwright (custom fixtures) Built-in test data management
Test Framework:  Playwright (TypeScript)
Test Runner:     Playwright Test
Reporting:       HTML Reports + Allure (optional)
Data Management: JSON fixtures + factories
CI/CD:           GitHub Actions
Parallelization: Built-in (Playwright)
Mocking:         MSW (Mock Service Worker)

Folder Structure & Organization

Scalable Directory Layout

project-root/
├── tests/
│   ├── e2e/                          # End-to-end tests
│   │   ├── auth.spec.ts             # Group by feature
│   │   ├── dashboard.spec.ts
│   │   └── checkout.spec.ts
│   ├── api/                          # API tests
│   │   ├── users.spec.ts
│   │   ├── products.spec.ts
│   │   └── payments.spec.ts
│   ├── pages/                        # Page Objects
│   │   ├── BasePage.ts              # Base class
│   │   ├── LoginPage.ts
│   │   ├── DashboardPage.ts
│   │   └── CheckoutPage.ts
│   ├── fixtures/                     # Test data
│   │   ├── users.json
│   │   ├── products.json
│   │   └── orders.json
│   ├── utils/                        # Shared utilities
│   │   ├── test-helpers.ts
│   │   ├── assertions.ts
│   │   └── wait-helpers.ts
│   ├── config/
│   │   └── test-config.ts           # Centralized config
│   └── playwright.config.ts         # Framework config
├── .env.example                      # Environment template
└── package.json

Page Object Model Architecture

The Pattern

Page Object Model separates test logic from UI element location:

// BasePage.ts - Reusable parent class
export class BasePage {
  constructor(public page: Page) {}

  async navigate(path: string): Promise<void> {
    await this.page.goto(`${process.env.BASE_URL}${path}`);
  }

  async waitForElement(selector: string, timeout = 5000): Promise<void> {
    await this.page.waitForSelector(selector, { timeout });
  }

  async click(selector: string): Promise<void> {
    await this.page.click(selector);
  }

  async fill(selector: string, text: string): Promise<void> {
    await this.page.fill(selector, text);
  }

  async getText(selector: string): Promise<string> {
    return this.page.textContent(selector) || '';
  }
}

// LoginPage.ts - Specific page with selectors
import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
  // Selectors as private constants
  private emailInput = 'input[name="email"]';
  private passwordInput = 'input[name="password"]';
  private submitButton = 'button[type="submit"]';
  private errorMessage = '[data-testid="error-message"]';

  async login(email: string, password: string): Promise<void> {
    await this.fill(this.emailInput, email);
    await this.fill(this.passwordInput, password);
    await this.click(this.submitButton);
    await this.page.waitForNavigation();
  }

  async getErrorMessage(): Promise<string> {
    return this.getText(this.errorMessage);
  }

  async isErrorDisplayed(): Promise<boolean> {
    return this.page.isVisible(this.errorMessage);
  }
}

// test.spec.ts - Clean test using page objects
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

test('User can login with valid credentials', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.navigate('/login');
  await loginPage.login('user@example.com', 'password123');
  
  // Now on dashboard
  expect(page.url()).toContain('/dashboard');
});

Benefits:

  • Tests are readable (business logic focused)
  • Selectors centralized (change once, update everywhere)
  • Reusable (multiple tests use same page object)
  • Maintainable (when UI changes, only update page object)

Test Data Management

// tests/fixtures/users.json
{
  "validUser": {
    "email": "user@example.com",
    "password": "SecurePassword123!",
    "name": "John Doe"
  },
  "adminUser": {
    "email": "admin@example.com",
    "password": "AdminPassword123!",
    "role": "admin"
  }
}
// test.spec.ts
import users from './fixtures/users.json';

test('Admin can access admin panel', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.login(users.adminUser.email, users.adminUser.password);
  // ... assertions
});

Approach 2: Factories (For complex/dynamic data)

// tests/factories/UserFactory.ts
export class UserFactory {
  static createUser(overrides = {}): User {
    return {
      email: `user_${Date.now()}@example.com`,
      password: 'TestPassword123!',
      name: 'Test User',
      role: 'user',
      ...overrides,
    };
  }

  static createAdmin(): User {
    return this.createUser({ role: 'admin' });
  }
}

// Usage
const testUser = UserFactory.createUser({ role: 'moderator' });

CI/CD Integration

GitHub Actions Workflow

name: Automated Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Install dependencies
        run: |
          npm install
          npx playwright install
      
      - name: Run tests (parallel)
        run: npm run test:e2e
      
      - name: Generate report
        if: always()
        run: npx playwright show-report
      
      - name: Upload results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/

Real Project Example: Wells Fargo

On the Wells Fargo financial automation project, our framework:

Structure:

  • 150 E2E tests organized by feature
  • 80 API tests for payment validation
  • 40 performance tests with K6

Tech Stack:

  • Playwright + TypeScript
  • Page Object Model (25 page objects)
  • JSON fixtures + factories for test data
  • Allure Reports for detailed results
  • GitHub Actions for CI/CD
  • Parallel execution (4 workers)

Results:

  • Test execution: 45 minutes → 12 minutes (parallelization)
  • Maintenance cost: 20% less (Page Objects)
  • Flaky tests: 40% → 2% (proper waits, no hardcoded sleeps)
  • Bug detection rate: 95% of regressions caught before production

Common Mistakes to Avoid

❌ Mistake 1: Hardcoded Selectors

// BAD
await page.click('button.submit-btn'); // Used in 20 tests
// If selector changes, update 20 tests

// GOOD
class LoginPage {
  private submitButton = 'button.submit-btn'; // One place to change
  async submit() {
    await this.page.click(this.submitButton);
  }
}

❌ Mistake 2: Test Dependencies

// BAD - tests depend on execution order
test('Create user', () => { /* creates user 123 */ });
test('Edit user', () => { /* depends on user 123 existing */ });

// GOOD - each test is independent
test('Create and edit user', () => {
  // Create user, then edit it
});

❌ Mistake 3: Sleeping Instead of Waiting

// BAD
await page.sleep(5000); // Always waits 5 seconds

// GOOD
await page.waitForSelector('button', { timeout: 5000 }); // Waits up to 5s

❌ Mistake 4: No Test Organization

// BAD
all_tests.spec.ts (5000 lines, 200 tests)

// GOOD
tests/e2e/auth.spec.ts        (40 lines, 5 tests)
tests/e2e/dashboard.spec.ts   (50 lines, 7 tests)
tests/e2e/checkout.spec.ts    (60 lines, 8 tests)

FAQ

Q: Should I use Page Objects for small projects?
A: Yes. Even for 10 tests, Page Objects save time when UI changes.

Q: Playwright or Selenium?
A: Playwright (2026). Faster, better API, built-in async handling. Selenium only if you need IE support.

Q: How many tests should I have?
A: Minimum: Cover critical user journeys. Rule of thumb: 70% happy path, 30% error cases.

Q: Should I parallelize tests?
A: Yes, if you have 50+ tests. Reduces execution from hours to minutes.

Q: How do I handle authentication in tests?
A: Store auth token in beforeAll hook, reuse for all tests in a worker. Don't log in for every test.

Tayyab Akmal
// author

Tayyab Akmal

AI & QA Automation Engineer

I've caught critical bugs in fintech, e-commerce, and SaaS platforms — then built the automation that prevents them from shipping again. 6+ years scaling test automation and AI-driven QA.

// feedback_channel

FOUND THIS USEFUL?

Share your thoughts or let's discuss automation testing strategies.

→ Start Conversation
Available for hire