Playwright 测试进阶:用 Fixtures 优雅实现 Page Object Model

Playwright 测试进阶:用 Fixtures 优雅实现 Page Object Model

· 1,064 词 · 6 分钟 读完 playwright进阶 Page Object Fixture

在 Playwright 项目中,传统 Page Object Model 很容易导致代码重复和维护困难。

本文手把手教你如何利用 Playwright 的自定义 Fixtures,把 Page 对象自动注入测试,彻底消除每次使用page前都要手动去 new 的烦恼。

让测试代码更简洁、可复用、可扩展。

跟着做完,你的自动化测试框架将提升一个档次!特别适合用在大型的项目里面。

预计耗时:20–40 分钟

1. 理解两种常见写法及其问题

传统 POM 写法(大家最开始都会这么写)

// pages/LoginPage.ts
export class LoginPage {
  constructor(private page) {
    this.page = page;
  }

  async goto() {
    await this.page.goto("https://example.com/login");
  }

  async login(username: string, password: string) {
    await this.page.fill("#username", username);
    await this.page.fill("#password", password);
    await this.page.click("#login");
  }
}
// tests/login.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";

test("应该能正常登录", async ({ page }) => {
  const loginPage = new LoginPage(page); // 每份测试都要 new 一次

  await loginPage.goto();
  await loginPage.login("testuser", "password");

  await expect(page).toHaveURL("https://example.com/home");
});

痛点(项目大了之后特别明显):

  • 每份测试都要写 new XXXPage(page)
  • 改一次构造函数 → 所有测试文件都要改
  • 想做「已登录状态」的前置条件 → 每个测试重复写登录代码
  • 测试文件看起来很“重”

2. Playwright 推荐的现代写法:把 Page Object 做成 Fixture

核心思路:
不要在测试里手动 new Page Object,而是让 fixture 帮你创建并注入

步骤 1 – 保持 Page Object 类不变(或稍作调整)

// pages/LoginPage.ts
export class LoginPage {
  constructor(public page) {} // 保持 public 方便 fixture 里访问 page.url() 等

  locators = {
    username: "#username",
    password: "#password",
    loginBtn: "#login",
  };

  async goto() {
    await this.page.goto("https://example.com/login");
  }

  async fillCredentials(username: string, password: string) {
    await this.page.fill(this.locators.username, username);
    await this.page.fill(this.locators.password, password);
  }

  async submit() {
    await this.page.click(this.locators.loginBtn);
  }

  // 常用组合动作(可选)
  async login(username: string, password: string) {
    await this.goto();
    await this.fillCredentials(username, password);
    await this.submit();
  }
}

步骤 2 – 创建自定义 fixture(最核心的一步)

新建文件:fixtures/index.ts(或 utils/fixtures.ts

// fixtures/index.ts
import { test as base } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";

// 可以继续扩展其他 page 对象
type MyFixtures = {
  loginPage: LoginPage;
  // inventoryPage: InventoryPage;
  // cartPage: CartPage;
};

export const test = base.extend<MyFixtures>({
  // 普通注入(页面刚打开的状态)
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage); // 交给测试使用,用完自动清理(如果有 teardown 逻辑)
  },

  // 进阶:自动完成登录的 fixture(非常实用)
  // authenticatedLoginPage: async ({ page }, use) => {
  //   const loginPage = new LoginPage(page);
  //   await loginPage.login('standard_user', 'secret_sauce');
  //   await use(loginPage);
  // },
});

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

步骤 3 – 在测试文件中使用超级干净的写法

// tests/login.spec.ts
import { test, expect } from "../../fixtures"; // 注意路径

test("普通登录流程验证", async ({ loginPage }) => {
  await loginPage.goto();
  await loginPage.fillCredentials("testuser", "password");
  await loginPage.submit();

  await expect(loginPage.page).toHaveURL(/home$/);
});

// 更推荐的写法:把组合动作放在 Page 类里
test("使用封装的 login 方法", async ({ loginPage }) => {
  await loginPage.login("testuser", "password");

  await expect(loginPage.page).toHaveURL("https://example.com/home");
});

3. 进阶用法推荐(强烈建议掌握)

3.1 自动登录 fixture(最常用场景)

// fixtures/index.ts 继续扩展
export const test = base.extend<MyFixtures>({
  loginPage: async ({ page }, use) => { ... },

  loggedInPage: async ({ loginPage }, use) => {
    await loginPage.login('standard_user', 'secret_sauce');
    // 可选:等待页面稳定
    await loginPage.page.waitForURL(/inventory/);
    await use(loginPage.page);   // 这里注入 page,也可以注入 loginPage
  },
});

测试里直接用已登录状态:

test("已登录用户能看到商品列表", async ({ loggedInPage }) => {
  await expect(loggedInPage.locator(".inventory_list")).toBeVisible();
});

3.2 多个 Page Objects 一起使用

type MyFixtures = {
  loginPage: LoginPage;
  inventoryPage: InventoryPage;
  cartPage: CartPage;
};

export const test = base.extend<MyFixtures>({
  loginPage: async ({ page }, use) => { ... },
  inventoryPage: async ({ page }, use) => {
    await use(new InventoryPage(page));
  },
  // 可以依赖其他 fixture
  cartPage: async ({ inventoryPage }, use) => {
    // 假设从 inventory 页加商品后进入 cart
    await inventoryPage.addFirstItemToCart();
    await inventoryPage.gotoCart();
    await use(new CartPage(inventoryPage.page));
  },
});

4. 总结:为什么推荐这种写法?

技能点传统 new Page() 写法使用 Fixture + POM 写法
重复 new每个测试都要写只写一次(在 fixture 里)
构造函数修改所有测试文件改只改 fixture 文件
前置登录每个测试重复写登录代码做成 loggedInPage fixture,一行搞定
可读性中等很高(测试里只看到业务动作)
扩展性差(文件越来越多越乱)很好(fixtures 集中管理)
执行速度每次都重新创建对象Playwright 缓存复用(同 worker 内)

5. 快速上手 checklist

  • 创建 pages/ 目录,放所有 Page 类
  • 创建 fixtures/index.ts,用 test.extend() 定义 fixture
  • 测试文件里 import { test, expect } from '../fixtures'
  • 把常用前置操作(登录、导航)尽量做成 fixture
  • Page 类里多放组合方法(login、addToCart、checkout 等)

祝你写出优雅、可维护的 Playwright 测试!🚀