Playwright 基础测试 #

测试的基本结构 #

test 函数 #

test 是 Playwright 中最基础的测试函数,用于定义一个测试用例:

typescript
import { test } from '@playwright/test';

test('测试描述', async ({ page }) => {
  // 测试代码
});

第一个测试 #

typescript
// tests/example.spec.ts
import { test, expect } from '@playwright/test';

test('首页标题测试', async ({ page }) => {
  // 导航到页面
  await page.goto('https://example.com');
  
  // 验证标题
  await expect(page).toHaveTitle(/Example Domain/);
});

test('页面内容测试', async ({ page }) => {
  await page.goto('https://example.com');
  
  // 验证元素存在
  const heading = page.locator('h1');
  await expect(heading).toContainText('Example Domain');
});

测试文件结构 #

text
tests/
├── example.spec.ts        # .spec.ts 后缀
├── example.test.ts        # 或 .test.ts 后缀
└── example.spec.js        # JavaScript 文件

组织测试 #

describe 块 #

使用 test.describe 将相关测试分组:

typescript
import { test, expect } from '@playwright/test';

test.describe('用户认证', () => {
  test.describe('登录功能', () => {
    test('使用有效凭据登录', async ({ page }) => {
      await page.goto('/login');
      await page.fill('#email', 'user@example.com');
      await page.fill('#password', 'password123');
      await page.click('button[type="submit"]');
      
      await expect(page).toHaveURL('/dashboard');
    });

    test('使用无效凭据登录失败', async ({ page }) => {
      await page.goto('/login');
      await page.fill('#email', 'wrong@example.com');
      await page.fill('#password', 'wrongpassword');
      await page.click('button[type="submit"]');
      
      await expect(page.locator('.error')).toBeVisible();
    });
  });

  test.describe('注册功能', () => {
    test('新用户注册', async ({ page }) => {
      // 注册测试
    });
  });
});

嵌套 describe #

typescript
test.describe('电子商务', () => {
  test.describe('购物车', () => {
    test.describe('添加商品', () => {
      test('添加单个商品', async ({ page }) => {
        // 测试代码
      });

      test('添加多个商品', async ({ page }) => {
        // 测试代码
      });
    });

    test.describe('删除商品', () => {
      test('删除单个商品', async ({ page }) => {
        // 测试代码
      });
    });
  });
});

测试输出结构 #

text
Running 5 tests using 1 worker

  电子商务
    购物车
      添加商品
        ✓ 添加单个商品 (2s)
        ✓ 添加多个商品 (3s)
      删除商品
        ✓ 删除单个商品 (1s)

  5 passed (10s)

测试生命周期 #

钩子函数 #

Playwright 提供多种钩子函数:

typescript
import { test, expect } from '@playwright/test';

test.describe('测试生命周期', () => {
  // 所有测试之前执行一次
  test.beforeAll(async ({ browser }) => {
    console.log('beforeAll - 所有测试之前');
  });

  // 每个测试之前执行
  test.beforeEach(async ({ page }) => {
    console.log('beforeEach - 每个测试之前');
    await page.goto('/');
  });

  test('第一个测试', async ({ page }) => {
    console.log('测试 1');
  });

  test('第二个测试', async ({ page }) => {
    console.log('测试 2');
  });

  // 每个测试之后执行
  test.afterEach(async ({ page }, testInfo) => {
    console.log('afterEach - 每个测试之后');
    console.log(`测试状态: ${testInfo.status}`);
  });

  // 所有测试之后执行一次
  test.afterAll(async ({ browser }) => {
    console.log('afterAll - 所有测试之后');
  });
});

执行顺序 #

text
beforeAll
├── beforeEach
│   └── 测试 1
├── afterEach
├── beforeEach
│   └── 测试 2
├── afterEach
└── afterAll

钩子函数的实际应用 #

typescript
import { test, expect } from '@playwright/test';

test.describe('购物车测试', () => {
  let cart;

  test.beforeEach(async ({ page }) => {
    // 每个测试前初始化购物车
    await page.goto('/shop');
    cart = page.locator('.cart');
  });

  test('添加商品到购物车', async ({ page }) => {
    await page.click('.product:first-child .add-to-cart');
    await expect(cart.locator('.item')).toHaveCount(1);
  });

  test('清空购物车', async ({ page }) => {
    // 先添加商品
    await page.click('.product:first-child .add-to-cart');
    // 然后清空
    await page.click('.clear-cart');
    await expect(cart.locator('.item')).toHaveCount(0);
  });

  test.afterEach(async ({ page }, testInfo) => {
    // 失败时截图
    if (testInfo.status !== testInfo.expectedStatus) {
      await page.screenshot({ 
        path: `screenshots/${testInfo.title}.png` 
      });
    }
  });
});

测试夹具(Fixtures) #

内置夹具 #

Playwright 提供多个内置夹具:

typescript
import { test, expect } from '@playwright/test';

test('内置夹具示例', async ({ 
  page,      // Page 对象
  context,   // BrowserContext
  browser,   // Browser 实例
  request,   // APIRequestContext
  browserName, // 当前浏览器名称
}) => {
  // 使用 page
  await page.goto('/');
  
  // 使用 context
  const newPage = await context.newPage();
  
  // 使用 request
  const response = await request.get('/api/users');
  
  // 使用 browserName
  console.log(`当前浏览器: ${browserName}`);
});

自定义夹具 #

typescript
// fixtures.ts
import { test as base } from '@playwright/test';

// 定义自定义夹具类型
type MyFixtures = {
  todoPage: TodoPage;
  settingsPage: SettingsPage;
};

// 创建扩展测试
export const test = base.extend<MyFixtures>({
  // 定义 todoPage 夹具
  todoPage: async ({ page }, use) => {
    const todoPage = new TodoPage(page);
    await todoPage.goto();
    await use(todoPage);
  },
  
  // 定义 settingsPage 夹具
  settingsPage: async ({ page }, use) => {
    const settingsPage = new SettingsPage(page);
    await settingsPage.goto();
    await use(settingsPage);
  },
});

// 使用自定义夹具
test('使用自定义夹具', async ({ todoPage, settingsPage }) => {
  await todoPage.addItem('Buy milk');
  await todoPage.addItem('Buy bread');
  
  await settingsPage.toggleDarkMode();
});

夹具的依赖关系 #

typescript
import { test as base } from '@playwright/test';

// 定义夹具
const test = base.extend<{
  authenticatedPage: { page: Page; user: User };
}>({
  authenticatedPage: async ({ page }, use) => {
    // 设置:登录用户
    await page.goto('/login');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password');
    await page.click('button[type="submit"]');
    
    const user = { id: 1, email: 'test@example.com' };
    
    // 使用
    await use({ page, user });
    
    // 清理:登出用户
    await page.click('#logout');
  },
});

test('需要认证的测试', async ({ authenticatedPage }) => {
  const { page, user } = authenticatedPage;
  
  await page.goto('/profile');
  await expect(page.locator('.email')).toHaveText(user.email);
});

跳过测试 #

skip 方法 #

typescript
import { test, expect } from '@playwright/test';

// 跳过单个测试
test.skip('这个测试被跳过', async ({ page }) => {
  // 不会执行
});

// 条件跳过
test('条件跳过', async ({ page, browserName }) => {
  test.skip(browserName === 'webkit', 'WebKit 不支持此功能');
  // 只在非 WebKit 浏览器执行
});

// 跳过整个 describe
test.describe.skip('这组测试被跳过', () => {
  test('测试 1', async ({ page }) => {});
  test('测试 2', async ({ page }) => {});
});

仅运行特定测试 #

typescript
// 只运行这一个测试
test.only('只运行这个测试', async ({ page }) => {
  // 只有这个测试会运行
});

// 只运行这个 describe 块
test.describe.only('只运行这组测试', () => {
  test('测试 1', async ({ page }) => {});
  test('测试 2', async ({ page }) => {});
});

// 其他测试都会被跳过
test('这个测试会被跳过', async ({ page }) => {});

标记测试 #

typescript
// 标记为慢测试
test('慢测试', async ({ page }) => {
  test.slow(); // 超时时间翻三倍
  // 耗时操作
});

// 标记预期失败
test.fail('预期失败的测试', async ({ page }) => {
  // 这个测试预期会失败
  expect(true).toBe(false);
});

// 条件标记
test('条件失败', async ({ page, browserName }) => {
  test.fail(browserName === 'firefox', 'Firefox 有已知问题');
  // 在 Firefox 上预期失败
});

测试标签 #

添加标签 #

typescript
// 单个标签
test('测试 @smoke', async ({ page }) => {});

// 多个标签
test('测试', {
  tag: ['@smoke', '@fast'],
}, async ({ page }) => {});

// describe 标签
test.describe('一组测试', {
  tag: '@smoke',
}, () => {
  test('测试 1', async ({ page }) => {});
  test('测试 2', async ({ page }) => {});
});

按标签运行 #

bash
# 只运行带有 @smoke 标签的测试
npx playwright test --grep @smoke

# 排除带有 @slow 标签的测试
npx playwright test --grep-invert @slow

# 组合使用
npx playwright test --grep "@smoke|@fast"

测试注释 #

添加注释 #

typescript
test('带注释的测试', {
  annotation: {
    type: 'issue',
    description: 'https://github.com/repo/issues/123',
  },
}, async ({ page }) => {
  // 测试代码
});

// 多个注释
test('多个注释', {
  annotation: [
    { type: 'issue', description: 'https://github.com/repo/issues/123' },
    { type: 'docs', description: 'https://docs.example.com' },
  ],
}, async ({ page }) => {
  // 测试代码
});

测试参数化 #

使用 test.describe.configure #

typescript
test.describe('参数化测试', () => {
  // 配置模式
  test.describe.configure({ mode: 'parallel' });
  
  const users = [
    { name: 'Alice', role: 'admin' },
    { name: 'Bob', role: 'user' },
    { name: 'Charlie', role: 'guest' },
  ];

  for (const user of users) {
    test(`用户 ${user.name} 的权限测试`, async ({ page }) => {
      await page.goto('/profile');
      await expect(page.locator('.role')).toHaveText(user.role);
    });
  }
});

使用 forEach #

typescript
const testCases = [
  { input: 'hello', expected: 'HELLO' },
  { input: 'world', expected: 'WORLD' },
  { input: 'Playwright', expected: 'PLAYWRIGHT' },
];

test.describe('大写转换测试', () => {
  testCases.forEach(({ input, expected }) => {
    test(`"${input}" 应该转换为 "${expected}"`, async ({ page }) => {
      await page.goto('/');
      await page.fill('#input', input);
      await page.click('#convert');
      await expect(page.locator('#output')).toHaveText(expected);
    });
  });
});

测试隔离 #

默认隔离 #

Playwright 默认为每个测试创建独立的上下文:

typescript
test.describe('测试隔离', () => {
  test('测试 1', async ({ page }) => {
    // page 是全新的
    await page.goto('/');
    await page.click('#login');
    // 登录状态只在测试 1 中有效
  });

  test('测试 2', async ({ page }) => {
    // page 是全新的,没有登录状态
    await page.goto('/');
    // 需要重新登录
  });
});

共享上下文 #

typescript
test.describe.configure({ mode: 'serial' });

let page: Page;

test.beforeAll(async ({ browser }) => {
  page = await browser.newPage();
});

test.afterAll(async () => {
  await page.close();
});

test('步骤 1: 登录', async () => {
  await page.goto('/login');
  await page.fill('#email', 'user@example.com');
  await page.click('button[type="submit"]');
});

test('步骤 2: 访问仪表板', async () => {
  // 使用相同的 page,保持登录状态
  await page.goto('/dashboard');
  await expect(page.locator('.welcome')).toBeVisible();
});

测试模式 #

AAA 模式 #

Arrange(准备)- Act(执行)- Assert(断言):

typescript
test('添加商品到购物车', async ({ page }) => {
  // Arrange - 准备测试数据
  const productName = 'Test Product';
  const productPrice = 99.99;
  
  // Act - 执行被测试的操作
  await page.goto('/shop');
  await page.click(`text=${productName}`);
  await page.click('.add-to-cart');
  
  // Assert - 验证结果
  const cartItem = page.locator('.cart-item');
  await expect(cartItem).toContainText(productName);
  await expect(cartItem).toContainText(`$${productPrice}`);
});

Page Object 模式 #

typescript
// pages/LoginPage.ts
class LoginPage {
  constructor(private page: Page) {}
  
  async goto() {
    await this.page.goto('/login');
  }
  
  async login(email: string, password: string) {
    await this.page.fill('#email', email);
    await this.page.fill('#password', password);
    await this.page.click('button[type="submit"]');
  }
}

// tests/login.spec.ts
test('登录测试', async ({ page }) => {
  const loginPage = new LoginPage(page);
  
  await loginPage.goto();
  await loginPage.login('user@example.com', 'password');
  
  await expect(page).toHaveURL('/dashboard');
});

下一步 #

现在你已经掌握了 Playwright 基础测试的编写方法,接下来学习 元素定位 了解更多元素定位方式!

最后更新:2026-03-28