e2e-testing-automation

安装量: 207
排名: #4208

安装

npx skills add https://github.com/aj-geddes/useful-ai-prompts --skill e2e-testing-automation

E2E Testing Automation Overview

End-to-end (E2E) testing validates complete user workflows from the UI through all backend systems, ensuring the entire application stack works together correctly from a user's perspective. E2E tests simulate real user interactions with browsers, handling authentication, navigation, form submissions, and validating results.

When to Use Testing critical user journeys (signup, checkout, login) Validating multi-step workflows Testing across different browsers and devices Regression testing for UI changes Verifying frontend-backend integration Testing with real user interactions (clicks, typing, scrolling) Smoke testing deployments Instructions 1. Playwright E2E Tests // tests/e2e/checkout.spec.ts import { test, expect, Page } from '@playwright/test';

test.describe('E-commerce Checkout Flow', () => { let page: Page;

test.beforeEach(async ({ page: p }) => { page = p; await page.goto('/'); });

test('complete checkout flow as guest user', async () => { // 1. Browse and add product to cart await page.click('text=Shop Now'); await page.click('[data-testid="product-1"]'); await expect(page.locator('h1')).toContainText('Product Name');

await page.click('button:has-text("Add to Cart")');
await expect(page.locator('.cart-count')).toHaveText('1');

// 2. Go to cart and proceed to checkout
await page.click('[data-testid="cart-icon"]');
await expect(page.locator('.cart-item')).toHaveCount(1);
await page.click('text=Proceed to Checkout');

// 3. Fill shipping information
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="firstName"]', 'John');
await page.fill('[name="lastName"]', 'Doe');
await page.fill('[name="address"]', '123 Main St');
await page.fill('[name="city"]', 'San Francisco');
await page.selectOption('[name="state"]', 'CA');
await page.fill('[name="zip"]', '94105');

// 4. Enter payment information
await page.click('text=Continue to Payment');

// Wait for payment iframe to load
const paymentFrame = page.frameLocator('iframe[name="payment-frame"]');
await paymentFrame.locator('[name="cardNumber"]').fill('4242424242424242');
await paymentFrame.locator('[name="expiry"]').fill('12/25');
await paymentFrame.locator('[name="cvc"]').fill('123');

// 5. Complete order
await page.click('button:has-text("Place Order")');

// 6. Verify success
await expect(page).toHaveURL(/\/order\/confirmation/);
await expect(page.locator('.confirmation-message')).toContainText('Order placed successfully');

const orderNumber = await page.locator('[data-testid="order-number"]').textContent();
expect(orderNumber).toMatch(/^ORD-\d+$/);

});

test('checkout with existing user account', async () => { // Login first await page.click('text=Sign In'); await page.fill('[name="email"]', 'existing@example.com'); await page.fill('[name="password"]', 'Password123!'); await page.click('button[type="submit"]');

await expect(page.locator('.user-menu')).toContainText('existing@example.com');

// Add product and checkout with saved information
await page.click('[data-testid="product-2"]');
await page.click('button:has-text("Add to Cart")');
await page.click('[data-testid="cart-icon"]');
await page.click('text=Checkout');

// Verify saved address is pre-filled
await expect(page.locator('[name="address"]')).toHaveValue(/./);

// Complete checkout
await page.click('button:has-text("Use Saved Payment")');
await page.click('button:has-text("Place Order")');

await expect(page).toHaveURL(/\/order\/confirmation/);

});

test('handle out of stock product', async () => { await page.click('[data-testid="product-out-of-stock"]');

const addToCartButton = page.locator('button:has-text("Add to Cart")');
await expect(addToCartButton).toBeDisabled();
await expect(page.locator('.stock-status')).toHaveText('Out of Stock');

}); });

  1. Cypress E2E Tests // cypress/e2e/authentication.cy.js describe('User Authentication Flow', () => { beforeEach(() => { cy.visit('/'); });

it('should register a new user account', () => { cy.get('[data-cy="signup-button"]').click(); cy.url().should('include', '/signup');

// Fill registration form
const timestamp = Date.now();
cy.get('[name="email"]').type(`user${timestamp}@example.com`);
cy.get('[name="password"]').type('SecurePass123!');
cy.get('[name="confirmPassword"]').type('SecurePass123!');
cy.get('[name="firstName"]').type('Test');
cy.get('[name="lastName"]').type('User');

// Accept terms
cy.get('[name="acceptTerms"]').check();

// Submit form
cy.get('button[type="submit"]').click();

// Verify success
cy.url().should('include', '/dashboard');
cy.get('.welcome-message').should('contain', 'Welcome, Test!');

// Verify email sent (check via API)
cy.request(`/api/test/emails/${timestamp}@example.com`)
  .its('body')
  .should('have.property', 'subject', 'Welcome to Our App');

});

it('should handle validation errors', () => { cy.get('[data-cy="signup-button"]').click();

// Submit empty form
cy.get('button[type="submit"]').click();

// Check for validation errors
cy.get('.error-message').should('have.length.greaterThan', 0);
cy.get('[name="email"]')
  .parent()
  .should('contain', 'Email is required');

// Fill invalid email
cy.get('[name="email"]').type('invalid-email');
cy.get('[name="password"]').type('weak');
cy.get('button[type="submit"]').click();

cy.get('[name="email"]')
  .parent()
  .should('contain', 'Invalid email format');
cy.get('[name="password"]')
  .parent()
  .should('contain', 'Password must be at least 8 characters');

});

it('should login with valid credentials', () => { // Create test user first cy.request('POST', '/api/test/users', { email: 'test@example.com', password: 'Password123!', name: 'Test User' });

// Login
cy.get('[data-cy="login-button"]').click();
cy.get('[name="email"]').type('test@example.com');
cy.get('[name="password"]').type('Password123!');
cy.get('button[type="submit"]').click();

// Verify login successful
cy.url().should('include', '/dashboard');
cy.getCookie('auth_token').should('exist');

// Verify user menu
cy.get('[data-cy="user-menu"]').click();
cy.get('.user-email').should('contain', 'test@example.com');

});

it('should maintain session across page reloads', () => { // Login cy.loginViaAPI('test@example.com', 'Password123!'); cy.visit('/dashboard');

// Verify logged in
cy.get('.user-menu').should('exist');

// Reload page
cy.reload();

// Still logged in
cy.get('.user-menu').should('exist');
cy.getCookie('auth_token').should('exist');

});

it('should logout successfully', () => { cy.loginViaAPI('test@example.com', 'Password123!'); cy.visit('/dashboard');

cy.get('[data-cy="user-menu"]').click();
cy.get('[data-cy="logout-button"]').click();

cy.url().should('equal', Cypress.config().baseUrl + '/');
cy.getCookie('auth_token').should('not.exist');

}); });

// Custom command for login Cypress.Commands.add('loginViaAPI', (email, password) => { cy.request('POST', '/api/auth/login', { email, password }) .then((response) => { window.localStorage.setItem('auth_token', response.body.token); }); });

  1. Selenium with Python (pytest)

tests/e2e/test_search_functionality.py

import pytest from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.keys import Keys

class TestSearchFunctionality: @pytest.fixture def driver(self): """Setup and teardown browser.""" options = webdriver.ChromeOptions() options.add_argument('--headless') driver = webdriver.Chrome(options=options) driver.implicitly_wait(10) yield driver driver.quit()

def test_search_with_results(self, driver):
    """Test search functionality returns relevant results."""
    driver.get('http://localhost:3000')

    # Find search box and enter query
    search_box = driver.find_element(By.NAME, 'search')
    search_box.send_keys('laptop')
    search_box.send_keys(Keys.RETURN)

    # Wait for results
    wait = WebDriverWait(driver, 10)
    results = wait.until(
        EC.presence_of_all_elements_located((By.CLASS_NAME, 'search-result'))
    )

    # Verify results
    assert len(results) > 0
    assert 'laptop' in driver.page_source.lower()

    # Check first result has required elements
    first_result = results[0]
    assert first_result.find_element(By.CLASS_NAME, 'product-title')
    assert first_result.find_element(By.CLASS_NAME, 'product-price')
    assert first_result.find_element(By.CLASS_NAME, 'product-image')

def test_search_filters(self, driver):
    """Test applying filters to search results."""
    driver.get('http://localhost:3000/search?q=laptop')

    wait = WebDriverWait(driver, 10)

    # Wait for results to load
    wait.until(
        EC.presence_of_element_located((By.CLASS_NAME, 'search-result'))
    )

    initial_count = len(driver.find_elements(By.CLASS_NAME, 'search-result'))

    # Apply price filter
    price_filter = driver.find_element(By.ID, 'price-filter-500-1000')
    price_filter.click()

    # Wait for filtered results
    wait.until(
        EC.staleness_of(driver.find_element(By.CLASS_NAME, 'search-result'))
    )
    wait.until(
        EC.presence_of_element_located((By.CLASS_NAME, 'search-result'))
    )

    filtered_count = len(driver.find_elements(By.CLASS_NAME, 'search-result'))

    # Verify filter was applied
    assert filtered_count <= initial_count

    # Verify all prices are in range
    prices = driver.find_elements(By.CLASS_NAME, 'product-price')
    for price_elem in prices:
        price = float(price_elem.text.replace('$', '').replace(',', ''))
        assert 500 <= price <= 1000

def test_pagination(self, driver):
    """Test navigating through search result pages."""
    driver.get('http://localhost:3000/search?q=electronics')

    wait = WebDriverWait(driver, 10)

    # Get first page results
    first_page_results = driver.find_elements(By.CLASS_NAME, 'search-result')
    first_result_title = first_page_results[0].find_element(
        By.CLASS_NAME, 'product-title'
    ).text

    # Click next page
    next_button = driver.find_element(By.CSS_SELECTOR, '[aria-label="Next page"]')
    next_button.click()

    # Wait for new results
    wait.until(EC.staleness_of(first_page_results[0]))

    # Verify on page 2
    assert 'page=2' in driver.current_url

    second_page_results = driver.find_elements(By.CLASS_NAME, 'search-result')
    second_result_title = second_page_results[0].find_element(
        By.CLASS_NAME, 'product-title'
    ).text

    # Results should be different
    assert first_result_title != second_result_title

def test_empty_search_results(self, driver):
    """Test handling of searches with no results."""
    driver.get('http://localhost:3000')

    search_box = driver.find_element(By.NAME, 'search')
    search_box.send_keys('xyznonexistentproduct123')
    search_box.send_keys(Keys.RETURN)

    wait = WebDriverWait(driver, 10)
    no_results = wait.until(
        EC.presence_of_element_located((By.CLASS_NAME, 'no-results'))
    )

    assert 'no results found' in no_results.text.lower()
    assert len(driver.find_elements(By.CLASS_NAME, 'search-result')) == 0
  1. Page Object Model Pattern // pages/LoginPage.ts import { Page, Locator } from '@playwright/test';

export class LoginPage { readonly page: Page; readonly emailInput: Locator; readonly passwordInput: Locator; readonly loginButton: Locator; readonly errorMessage: Locator;

constructor(page: Page) { this.page = page; this.emailInput = page.locator('[name="email"]'); this.passwordInput = page.locator('[name="password"]'); this.loginButton = page.locator('button[type="submit"]'); this.errorMessage = page.locator('.error-message'); }

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

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

async getErrorMessage(): Promise { return await this.errorMessage.textContent(); } }

// tests/login.spec.ts import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage';

test('login with invalid credentials', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('invalid@example.com', 'wrongpassword');

const error = await loginPage.getErrorMessage(); expect(error).toContain('Invalid credentials'); });

Best Practices ✅ DO Use data-testid attributes for stable selectors Implement Page Object Model for maintainability Test critical user journeys thoroughly Run tests in multiple browsers (cross-browser testing) Use explicit waits instead of sleep/timeouts Clean up test data after each test Take screenshots on failures Parallelize test execution where possible ❌ DON'T Use brittle CSS selectors (like nth-child) Test every possible UI combination (focus on critical paths) Share state between tests Use fixed delays (sleep/timeout) Ignore flaky tests Run E2E tests for unit-level testing Test third-party UI components in detail Skip mobile/responsive testing Tools & Frameworks Playwright: Modern, fast, reliable (Node.js, Python, Java, .NET) Cypress: Developer-friendly, fast feedback loop (JavaScript) Selenium: Cross-browser, mature ecosystem (multiple languages) Puppeteer: Chrome DevTools Protocol automation (Node.js) WebDriverIO: Next-gen browser automation (Node.js) Configuration Examples // playwright.config.ts import { defineConfig } from '@playwright/test';

export default defineConfig({ testDir: './tests/e2e', timeout: 30000, retries: 2, workers: process.env.CI ? 2 : 4,

use: { baseURL: 'http://localhost:3000', screenshot: 'only-on-failure', video: 'retain-on-failure', trace: 'on-first-retry', },

projects: [ { name: 'chromium', use: { browserName: 'chromium' } }, { name: 'firefox', use: { browserName: 'firefox' } }, { name: 'webkit', use: { browserName: 'webkit' } }, ],

webServer: { command: 'npm run start', port: 3000, reuseExistingServer: !process.env.CI, }, });

Examples

See also: integration-testing, visual-regression-testing, accessibility-testing, test-automation-framework skills.

返回排行榜