home / skills / aj-geddes / useful-ai-prompts / test-automation-framework

test-automation-framework skill

/skills/test-automation-framework

This skill helps you design scalable test automation frameworks using page objects, fixtures, and reporting to accelerate reliable UI testing.

npx playbooks add skill aj-geddes/useful-ai-prompts --skill test-automation-framework

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
15.0 KB
---
name: test-automation-framework
description: Design and implement scalable test automation frameworks with Page Object Model, fixtures, and reporting. Use for test framework, page object pattern, test architecture, test organization, and automation infrastructure.
---

# Test Automation Framework

## Overview

A test automation framework provides structure, reusability, and maintainability for automated tests. It defines patterns for organizing tests, managing test data, handling dependencies, and generating reports. A well-designed framework reduces duplication, improves reliability, and accelerates test development.

## When to Use

- Setting up new test automation
- Scaling existing test suites
- Standardizing test practices across teams
- Reducing test maintenance burden
- Improving test reliability and speed
- Organizing large test codebases
- Implementing reusable test utilities
- Creating consistent reporting

## Framework Components

- **Test Organization**: Structure and hierarchy
- **Page Objects**: UI element abstraction
- **Test Data Management**: Fixtures and factories
- **Configuration**: Environment-specific settings
- **Utilities**: Shared helpers and functions
- **Reporting**: Test results and metrics
- **CI/CD Integration**: Automated execution

## Instructions

### 1. **Page Object Model (Playwright/TypeScript)**

```typescript
// framework/pages/BasePage.ts
import { Page, Locator } from '@playwright/test';

export abstract class BasePage {
  constructor(protected page: Page) {}

  async goto(path: string) {
    await this.page.goto(path);
  }

  async waitForPageLoad() {
    await this.page.waitForLoadState('networkidle');
  }

  async takeScreenshot(name: string) {
    await this.page.screenshot({ path: `screenshots/${name}.png` });
  }

  protected async clickAndWait(locator: Locator) {
    await Promise.all([
      this.page.waitForResponse(resp => resp.status() === 200),
      locator.click()
    ]);
  }
}

// framework/pages/LoginPage.ts
export class LoginPage extends BasePage {
  // Locators
  private readonly emailInput = this.page.locator('[name="email"]');
  private readonly passwordInput = this.page.locator('[name="password"]');
  private readonly submitButton = this.page.locator('button[type="submit"]');
  private readonly errorMessage = this.page.locator('.error-message');

  async goto() {
    await super.goto('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  async loginWithValidation(email: string, password: string) {
    await this.login(email, password);
    await this.page.waitForURL('/dashboard');
  }

  async getErrorMessage(): Promise<string> {
    return await this.errorMessage.textContent() || '';
  }

  async isLoggedIn(): Promise<boolean> {
    return this.page.url().includes('/dashboard');
  }
}

// framework/pages/ProductPage.ts
export class ProductPage extends BasePage {
  private readonly addToCartButton = this.page.locator('[data-testid="add-to-cart"]');
  private readonly quantityInput = this.page.locator('[name="quantity"]');
  private readonly priceLabel = this.page.locator('.price');

  async goto(productId: string) {
    await super.goto(`/products/${productId}`);
  }

  async addToCart(quantity: number = 1) {
    if (quantity > 1) {
      await this.quantityInput.fill(String(quantity));
    }
    await this.addToCartButton.click();
  }

  async getPrice(): Promise<number> {
    const priceText = await this.priceLabel.textContent();
    return parseFloat(priceText?.replace(/[^0-9.]/g, '') || '0');
  }
}

// tests/checkout.test.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../framework/pages/LoginPage';
import { ProductPage } from '../framework/pages/ProductPage';
import { CartPage } from '../framework/pages/CartPage';
import { CheckoutPage } from '../framework/pages/CheckoutPage';

test.describe('Checkout Flow', () => {
  let loginPage: LoginPage;
  let productPage: ProductPage;
  let cartPage: CartPage;
  let checkoutPage: CheckoutPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    productPage = new ProductPage(page);
    cartPage = new CartPage(page);
    checkoutPage = new CheckoutPage(page);

    await loginPage.goto();
    await loginPage.loginWithValidation('[email protected]', 'password123');
  });

  test('complete checkout process', async () => {
    // Add product to cart
    await productPage.goto('product-1');
    await productPage.addToCart(2);

    // Verify cart
    await cartPage.goto();
    expect(await cartPage.getItemCount()).toBe(2);

    // Checkout
    await checkoutPage.goto();
    await checkoutPage.fillShippingInfo({
      name: 'John Doe',
      address: '123 Main St',
      city: 'San Francisco',
      zip: '94105'
    });

    await checkoutPage.fillPaymentInfo({
      cardNumber: '4242424242424242',
      expiry: '12/25',
      cvc: '123'
    });

    await checkoutPage.placeOrder();

    expect(await checkoutPage.isOrderConfirmed()).toBe(true);
  });
});
```

### 2. **Test Fixtures and Factories**

```typescript
// framework/fixtures/database.ts
import { test as base } from '@playwright/test';
import { PrismaClient } from '@prisma/client';

export const test = base.extend<{
  db: PrismaClient;
  testUser: User;
  cleanupData: () => Promise<void>;
}>({
  db: async ({}, use) => {
    const db = new PrismaClient();
    await use(db);
    await db.$disconnect();
  },

  testUser: async ({ db }, use) => {
    const user = await db.user.create({
      data: {
        email: `test-${Date.now()}@example.com`,
        name: 'Test User',
        password: await hashPassword('password123'),
      },
    });
    await use(user);
    await db.user.delete({ where: { id: user.id } });
  },

  cleanupData: async ({ db }, use) => {
    const cleanup = async () => {
      await db.order.deleteMany({});
      await db.product.deleteMany({});
    };
    await use(cleanup);
  },
});

export { expect } from '@playwright/test';

// Usage in tests
import { test, expect } from '../framework/fixtures/database';

test('user can create order', async ({ db, testUser }) => {
  const product = await db.product.create({
    data: { name: 'Test Product', price: 99.99 }
  });

  const order = await db.order.create({
    data: {
      userId: testUser.id,
      items: {
        create: [{ productId: product.id, quantity: 1 }]
      }
    }
  });

  expect(order.userId).toBe(testUser.id);
});
```

### 3. **Custom Test Utilities**

```typescript
// framework/utils/helpers.ts
import { Page, expect } from '@playwright/test';

export class TestHelpers {
  static async waitForAPIResponse(
    page: Page,
    urlPattern: string | RegExp,
    action: () => Promise<void>
  ) {
    const responsePromise = page.waitForResponse(urlPattern);
    await action();
    return await responsePromise;
  }

  static async mockAPIResponse(
    page: Page,
    url: string | RegExp,
    response: any,
    status: number = 200
  ) {
    await page.route(url, route => {
      route.fulfill({
        status,
        contentType: 'application/json',
        body: JSON.stringify(response),
      });
    });
  }

  static async fillForm(page: Page, formData: Record<string, string>) {
    for (const [name, value] of Object.entries(formData)) {
      await page.fill(`[name="${name}"]`, value);
    }
  }

  static generateTestEmail(): string {
    return `test-${Date.now()}-${Math.random().toString(36)}@example.com`;
  }

  static async verifyToastMessage(page: Page, message: string) {
    const toast = page.locator('.toast-message');
    await expect(toast).toContainText(message);
    await expect(toast).toBeVisible();
  }
}

// Usage
import { TestHelpers } from '../framework/utils/helpers';

test('form submission', async ({ page }) => {
  await page.goto('/contact');

  await TestHelpers.fillForm(page, {
    name: 'John Doe',
    email: TestHelpers.generateTestEmail(),
    message: 'Test message'
  });

  await page.click('button[type="submit"]');

  await TestHelpers.verifyToastMessage(page, 'Message sent successfully');
});
```

### 4. **Configuration Management**

```typescript
// framework/config/config.ts
import * as dotenv from 'dotenv';

dotenv.config();

export interface TestConfig {
  baseUrl: string;
  apiUrl: string;
  timeout: number;
  headless: boolean;
  slowMo: number;
  screenshots: boolean;
  video: boolean;
  testUser: {
    email: string;
    password: string;
  };
}

const environments: Record<string, TestConfig> = {
  development: {
    baseUrl: 'http://localhost:3000',
    apiUrl: 'http://localhost:3001',
    timeout: 30000,
    headless: false,
    slowMo: 0,
    screenshots: true,
    video: false,
    testUser: {
      email: '[email protected]',
      password: 'devpass123',
    },
  },
  staging: {
    baseUrl: 'https://staging.example.com',
    apiUrl: 'https://api-staging.example.com',
    timeout: 60000,
    headless: true,
    slowMo: 0,
    screenshots: true,
    video: true,
    testUser: {
      email: process.env.STAGING_USER_EMAIL!,
      password: process.env.STAGING_USER_PASSWORD!,
    },
  },
  production: {
    baseUrl: 'https://example.com',
    apiUrl: 'https://api.example.com',
    timeout: 60000,
    headless: true,
    slowMo: 100,
    screenshots: true,
    video: true,
    testUser: {
      email: process.env.PROD_USER_EMAIL!,
      password: process.env.PROD_USER_PASSWORD!,
    },
  },
};

export const config: TestConfig =
  environments[process.env.TEST_ENV || 'development'];
```

### 5. **Custom Reporter**

```typescript
// framework/reporters/CustomReporter.ts
import { Reporter, TestCase, TestResult } from '@playwright/test/reporter';

class CustomReporter implements Reporter {
  private stats = {
    passed: 0,
    failed: 0,
    skipped: 0,
    total: 0,
  };

  onBegin() {
    console.log('Starting test run...');
  }

  onTestEnd(test: TestCase, result: TestResult) {
    this.stats.total++;

    if (result.status === 'passed') {
      this.stats.passed++;
      console.log(`✓ ${test.title}`);
    } else if (result.status === 'failed') {
      this.stats.failed++;
      console.log(`✗ ${test.title}`);
      console.log(`  Error: ${result.error?.message}`);
    } else if (result.status === 'skipped') {
      this.stats.skipped++;
      console.log(`⊘ ${test.title}`);
    }
  }

  onEnd() {
    console.log('\nTest Summary:');
    console.log(`  Total: ${this.stats.total}`);
    console.log(`  Passed: ${this.stats.passed}`);
    console.log(`  Failed: ${this.stats.failed}`);
    console.log(`  Skipped: ${this.stats.skipped}`);
  }
}

export default CustomReporter;
```

### 6. **pytest Framework (Python)**

```python
# framework/pages/base_page.py
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class BasePage:
    def __init__(self, driver: WebDriver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    def goto(self, path: str):
        self.driver.get(f"{self.base_url}{path}")

    def wait_for_element(self, locator):
        return self.wait.until(EC.presence_of_element_located(locator))

# framework/conftest.py
import pytest
from selenium import webdriver
from framework.config import config

@pytest.fixture(scope='session')
def browser():
    """Setup browser for test session."""
    driver = webdriver.Chrome()
    driver.implicitly_wait(10)
    yield driver
    driver.quit()

@pytest.fixture
def page(browser):
    """Provide clean page for each test."""
    browser.delete_all_cookies()
    return browser

@pytest.fixture
def test_user(db_session):
    """Create test user."""
    from framework.factories import UserFactory
    user = UserFactory.create()
    db_session.add(user)
    db_session.commit()
    yield user
    db_session.delete(user)
    db_session.commit()

# tests/test_login.py
from framework.pages.login_page import LoginPage

def test_login_success(page, test_user):
    """Test successful login."""
    login_page = LoginPage(page)
    login_page.goto()
    login_page.login(test_user.email, 'password123')

    assert login_page.is_logged_in()
```

### 7. **Test Organization**

```
test-automation/
├── framework/
│   ├── pages/
│   │   ├── BasePage.ts
│   │   ├── LoginPage.ts
│   │   ├── ProductPage.ts
│   │   └── CheckoutPage.ts
│   ├── fixtures/
│   │   ├── database.ts
│   │   └── api.ts
│   ├── utils/
│   │   ├── helpers.ts
│   │   ├── validators.ts
│   │   └── waiters.ts
│   ├── config/
│   │   └── config.ts
│   └── reporters/
│       └── CustomReporter.ts
├── tests/
│   ├── e2e/
│   │   ├── checkout.test.ts
│   │   └── search.test.ts
│   ├── integration/
│   │   └── api.test.ts
│   ├── visual/
│   │   └── components.test.ts
│   └── accessibility/
│       └── a11y.test.ts
├── data/
│   ├── fixtures/
│   └── test-data.json
├── playwright.config.ts
└── package.json
```

## Framework Patterns

### Singleton Pattern
```typescript
class TestContext {
  private static instance: TestContext;
  private data: Map<string, any> = new Map();

  private constructor() {}

  static getInstance(): TestContext {
    if (!TestContext.instance) {
      TestContext.instance = new TestContext();
    }
    return TestContext.instance;
  }

  set(key: string, value: any): void {
    this.data.set(key, value);
  }

  get(key: string): any {
    return this.data.get(key);
  }
}
```

### Builder Pattern
```typescript
class TestDataBuilder {
  private data: Partial<User> = {};

  withEmail(email: string): this {
    this.data.email = email;
    return this;
  }

  withName(name: string): this {
    this.data.name = name;
    return this;
  }

  withRole(role: string): this {
    this.data.role = role;
    return this;
  }

  build(): User {
    return {
      email: this.data.email || '[email protected]',
      name: this.data.name || 'Test User',
      role: this.data.role || 'user',
      ...this.data,
    } as User;
  }
}
```

## Best Practices

### ✅ DO
- Use Page Object Model for UI tests
- Create reusable test utilities
- Implement proper wait strategies
- Use fixtures for test data
- Configure for multiple environments
- Generate readable test reports
- Organize tests by feature/type
- Version control test framework

### ❌ DON'T
- Put test logic in page objects
- Use hard-coded waits (sleep)
- Duplicate test setup code
- Mix test data with test logic
- Skip error handling
- Ignore test flakiness
- Create overly complex abstractions
- Hardcode environment URLs

## Tools & Libraries

- **Playwright**: Modern browser automation
- **Selenium**: Cross-browser testing
- **Cypress**: JavaScript E2E framework
- **pytest**: Python testing framework
- **JUnit**: Java testing framework
- **TestNG**: Advanced Java framework
- **Robot Framework**: Keyword-driven testing

## Examples

See also: e2e-testing-automation, integration-testing, continuous-testing for implementing comprehensive test automation.

Overview

This skill designs and implements scalable test automation frameworks using Page Object Model, fixtures, and reporting. It delivers patterns for organizing tests, managing test data, integrating with CI, and producing actionable test reports. The goal is maintainable, reusable test architecture that speeds development and reduces flakiness.

How this skill works

It structures UI and API tests around Page Objects and shared utilities, provides fixture-driven test data and environment configuration, and plugs in custom reporters for run-time metrics. The skill includes helpers for API mocking, response waiting, form filling, and database fixtures so tests are deterministic and fast. CI/CD integration and environment-specific configs enable consistent runs across development, staging, and production-like pipelines.

When to use it

  • Starting a new automated test suite for web UI or API
  • Scaling or standardizing tests across multiple teams
  • Reducing flaky tests and maintenance overhead
  • Adding test data isolation with fixtures and factories
  • Integrating test runs and reports into CI/CD pipelines

Best practices

  • Use Page Object Model to encapsulate UI interactions and keep tests declarative
  • Prefer fixtures for setup/teardown to keep tests independent and fast
  • Centralize configuration per environment and avoid hard-coded values
  • Mock external APIs in unit/integration tests and reserve end-to-end runs for full integration
  • Implement reliable wait strategies and retry only where sensible
  • Collect structured test output and attach screenshots/videos for failures

Example use cases

  • End-to-end checkout flow implemented with Playwright pages, fixtures, and assertions
  • Database-backed tests that create and clean test users with Prisma fixtures
  • Custom utilities to wait for API responses and mock endpoints in UI tests
  • Environment-aware test runs with dotenv-based config and different timeouts
  • Custom reporter that prints concise summaries and error details after each run

FAQ

How do fixtures improve test reliability?

Fixtures isolate test data and lifecycle (create, yield, cleanup), preventing shared state and ensuring predictable test environments.

When should I mock APIs vs. hit real services?

Mock APIs for speed and determinism in unit/integration tests; reserve real service tests for scheduled end-to-end runs or staging environments.