playwright的各种规范

playwright的各种规范

· 2,672 词 · 14 分钟 读完 playwright进阶 翻译

Houseful 业务中的多个团队使用 Playwright 进行前端测试自动化。在构建我们的 Playwright 框架时,我们的目标是让代码易于阅读、维护和调试。为了帮助我们实现这一目标,我们作为一个团队共同努力,在使用 Playwright 的各个代码库中创建了统一的测试规范。

在各个代码库中统一测试标准为我们在可读性、可用性和准入门槛方面提供了好处。例如:

  • 提高可重用性 - 函数 / 定位器 / 共享步骤 / 其他测试代码可以轻松重用。对于在测试中工作的人来说,找到他们需要的函数和元素很简单。这减少了重复,并降低了代码审查开始后需要重新修改的可能性。

  • 简化 review - 代码可以更快地被 review。它减少了审查代码的心理负担,因为你可以轻松弄清楚测试代码在做什么。

  • 加快入职 - 命名约定帮助新人快速入职并感到舒适地为代码库做贡献。

以下是我们在 Houseful 创建 Playwright 测试时遵循的测试规范和指南。

Playwright 指南

端到端测试中的数据创建

运行端到端(e2e)测试成本很高。它们在资源方面成本高昂,可能需要大量精力来设置,并且对流程中任何地方的微小变化敏感,这可能使它们变得脆弱(容易报错)且难以长期维护。在选择这条路之前,请考虑其他选项。

如果你必须进行 e2e 测试:

好的做法 👍

  • 每个测试都有自己的数据创建。即它创建完成检查所需的所有数据
  • 每个测试都有一个后置步骤,即清理数据

不好的做法 ⚔️

  • 依赖现有数据来执行测试
  • 在测试后留下未清理的状态

页面对象模型 (POM)

每个页面都应该有一个相应的 POM 文件 来帮助我们测试的可维护性和可扩展性。POM 文件应包含和给出 POM 相关的所有选择器和函数。

所有交互都应通过页面对象完成,即测试用例中不应包含选择器。

所有断言都应在你的测试中完成,即 POM 中不应包含断言。(乙醇的评论:👀 这点见仁见智,很多时候 pom 中可能会出现一些保护性的等待断言,比如等待某个元素出现,如果不出现整个测试就失败了)

你可以在我们关于 软件测试设计模式 的博客中阅读更多关于我们如何利用 POM 的内容

好的做法 👍

// POM 文件 './pom/foo'
// 添加与页面相关的所有定位器和函数。
// 允许所有测试重用

import { Locator, Page } from "@playwright/test";

export class FooPage {
  readonly page: Page;
  readonly pageTitle: Locator;
  readonly buttonFoo: Locator;

  constructor(page: Page) {
    this.page = page;
    this.pageTitle = page.locator("text=My Page");
    this.buttonFoo = page.locator("text=Foo");
  }
}

// tests/foo.spec.ts
// 调用 POM 以使用定位器和函数
// 执行测试所需的所有断言

import { FooPage } from "./pom/foo";

let fooPage: FooPage;

describe("显示 foo bar", () => {
  beforeEach(({ page }) => {
    fooPage = new FooPage(page);
  });

  test("bar 是可见的", async () => {
    await fooPage.buttonFoo.click();
    await expect(fooPage.titlePage).toBeVisible();
  });
});

不好的做法 ⚔️

// tests/foo.spec.ts
// 直接在测试中包含定位器

import { Locator, Page } from "@playwright/test";

describe("显示 foo bar", () => {
  test("bar 是可见的", async () => {
    await page.locator("text=Foo").click();
    await expect(page.page.locator("text=My Page")).toBeVisible();
  });
});

测试结构 - Arrange, Act, Assert

在构建测试时遵循 AAA (Arrange, Act, Assert) 模式。在大多数情况下,Arrange 步骤可以包含在 Before 块中。

考虑添加注释以提高可读性。

好的做法 👍

// 安排,创建一个 let 属性

await createProperty();

// 执行,提出一个收费

await raiseCharge();

// 断言,确认收费已提出

expect(charge).ToBe("raised");

Linter

安装并使用 linting 规则。我们使用 eslint-playwright-plugin

推荐的配置将有助于执行本博文中描述的一些指南。

npm install -D eslint-plugin-playwright

避免条件语句

避免在测试文件中使用条件逻辑。测试应该是确定性的,这意味着我们应该目标是拥有明确预期结果的测试。带有条件语句的测试可能难以维护和阅读。此外,除非你绝对确定应用程序的状态在进行断言时已经稳定,否则这类测试可能会不稳定。

有条件语句可能是一个测试用例做了太多事情的迹象,可以被拆分。

不好的做法 ⚔️

import { FooPage } from "./pom/foo";

let fooPage: FooPage;

describe("条件测试", () => {
  beforeEach(({ page }) => {
    fooPage = new FooPage(page);
  });

  test("bar 是可见的", async () => {
    const isButtonVisible = await fooPage.buttonFoo.isVisible();

    if (isButtonVisible) {
      // 如果 buttonFoo 可见,点击它使 pageTitle 可见
      await fooPage.buttonFoo.click();
      // 然后检查 pageTitle 是否可见
      await expect(fooPage.pageTitle).toBeVisible();
    } else {
      // 否则只检查 pageTitle 是否可见
      await expect(fooPage.pageTitle).toBeVisible();
    }
  });
});

好的做法 👍

// 为每个场景有单独的规范。
// 包含需要达到该状态的设置步骤

// fooPageVisible.spec.js

test("bar 最初是可见的", async () => {
  // 设置你的测试,使 bar 最初可见

  // 断言 bar 是可见的
  await expect(fooPage.pageTitle).toBeVisible();
});

// fooBarButton.spec.js

import { FooPage } from "./pom/foo";

let fooPage: FooPage;

test("点击 Foo 按钮后 bar 是可见的", async () => {
  // 设置你的测试,使按钮最初可见

  // 执行按钮操作
  await fooPage.buttonFoo.click();

  // 断言 bar 是可见的
  await expect(fooPage.pageTitle).toBeVisible();
});

等待

不要使用任何任意的等待。这可能导致不稳定的测试,因为你很少能确定等待时间是否足够。它还可能不必要地增加测试运行时间。相反,尝试:

  • 使用 Playwright 的 waitUntil: 'domcontentloaded'
  • 等待特定的网络请求解析
  • 等待页面状态稳定,例如元素在页面上可见/不可见

不好的做法 ⚔️

await page.waitForTimeout(5000);

好的做法 👍

// 在等待帮助文件中定义
// wait-helpers.ts

export const waitForAPIResponse = async (
  page: Page,
  url: string,
  statusCode: number
): Promise<void> => {
  await page.waitForResponse(
    (res) => res.url().includes(url) && res.status() === statusCode
  );
};

// 在你的测试文件中使用
//tests/foo.spec.ts

import { Locator, Page } from "@playwright/test";
import { waitForAPIResponse } from "../../helpers/wait-helpers";

describe("显示 foo bar", () => {
  test("bar 是可见的", async () => {
    await fooPage.buttonFoo.click();
    await waitForNewAPIResponse(this.page, "/Accounting/GetRaisedCharges", 200);
    await expect(fooPage.titlePage).toBeVisible();
  });
});

好的做法 👍

//tests/foo.spec.ts

import { Locator, Page } from "@playwright/test";
import { waitForAPIResponse } from "../../helpers/wait-helpers";

describe("显示 foo bar", () => {
  test("bar 是可见的", async () => {
    await page.goto(fooBarURL, {
      waitUntil: "domcontentloaded",
    });
  });
});

好的做法 👍

//tests/foo.spec.ts

import { Locator, Page } from "@playwright/test";
import { waitForAPIResponse } from "../../helpers/wait-helpers";

describe("导航到 foo bar", () => {
  test("页面已加载", async () => {
    await fooPage.buttonFoo.click();
    await expect(fooPage.titlePage).toBeVisible();
  });
});

选择器

避免使用与实现和页面结构绑定的选择器。

相反,我们根据 testing-library 指导原则 优先考虑以下内容

  • getByRole (这有助于可访问性,反映用户和辅助技术如何感知页面)
  • getByText
  • getByTestId (需要时添加)

不好的做法 ⚔️

page.locator(".opt-u > div > .summary > div:nth-child(4) > div");

好的做法 👍

page.locator("#foo-button");

page.getByText("OK");

标签

在 Playwright 中利用标签(tags)来对测试进行分组并进行有针对性的运行。标签测试的一些方法包括:

  • 按测试类型 (例如 功能性、视觉,...)
  • 按测试在流水线中运行的位置 (发布、回归,...)
  • 按功能 (例如 日历、登录, …)

你可以在我们关于 标签注释 的博文中读到更多关于我们如何使用标签的内容。

好的做法 - 按测试类型 👍

describe("显示 foo bar", () => {
  // 设置在每种测试类型之前运行的步骤
  beforeEach(async ({ page }) => {
    await page.goto("/foo/bar");
    fooPage.buttonFoo.click();
    await fooPage.titlePage.waitFor();
  });

  // 检查可访问性
  test("@accessibility", async ({ page }) => {
    await injectAxe(page);
    await checkA11y(page, undefined, a11yOpts);
  });

  // 对页面运行断言
  test("@functional @smoke", async ({ page }) => {
    await expect(fooPage.buttonFoo).toBeVisible();
    await expect(fooPage.titlePage).toBeVisible();
  });

  // 桌面的视觉快照
  test("@visual desktop", async ({ page, captureScreenshot }) => {
    await captureScreenshot("foo-desktop.png");
  });

  // 移动设备的视觉快照
  testMobile("@visual mobile", async ({ captureScreenshot }) => {
    await captureScreenshot("foo-mobile.png");
  });
});

乙醇的注释:👆 上面在测试标题里打标签的方式已经过时了,现在 playwright 提供了一种新的做法,更加的工程化和可视化一些,具体看这里

好的做法 - 按页面 / 功能 / 测试运行位置 👍

//tests/foo.spec.ts

describe("@foobar @smoke 导航到 foo bar", () => {
  test("页面已加载", async () => {
    await fooPage.buttonFoo.click();
    await expect(fooPage.titlePage).toBeVisible();
  });
});

不稳定的测试

应优先解决不稳定的测试。如果你当时无法解决它,用 .fixme 标签它们。这将跳过该测试。

//tests/foo.spec.ts

describe("显示 foo bar", () => {
  test.fixme("bar 是可见的", async () => {
    await fooPage.buttonFoo.click();
    await expect(fooPage.titlePage).toBeVisible();
  });
});

并行化和可重复性

构建测试以便在不干预的情况下重复运行。并与套件中的其他测试并行运行,即不干扰其他测试。

Playwright 通过启动同时运行的多个 workders 来实现开箱即用的并行测试。Playwright 可以根据可用资源扩展工作进程的数量。通过使用 大型 github runners,我们可以在 CI 流水线中并行运行更多测试。

命名约定

变量

使用 驼峰命名法 声明。

布尔值

以 'is', 'has', 'are', 'have' 开头。这有助于在浏览代码时识别这是一个布尔值。仍然使用 驼峰命名法 声明。

let isTurnedOn = false;

页面对象 / 类

使用 帕斯卡命名法 声明。

使用描述性命名,这可以帮助读者快速识别这是涵盖哪个页面或页面组件。根据需要使用你的产品中尽可能多的上下文来使名称有意义。

好的做法 👍

export class AddWorksOrderModal

不好的做法 ⚔️

export class newModal

定位器

使用描述性命名,这可以帮助读者快速识别定位器所针对的元素。

例如,你可以使用包含 "动作 / 元素名称" + "元素类型" 的命名结构。

定义元素类型 - 这些是你的基本 HTML 元素类型,它们将在设计系统中定义和命名,或者作为一个团队,你可以在元素的一致命名上达成一致。例如:checkbox, tickbox, button, tooltip

定义动作 / 名称 考虑与此元素交互时将执行什么动作。或者元素的任何现有名称/文本

好的做法 👍

//这个元素是一个保存按钮,位于属性的上下文中

readonly savePropertyButton: Locator;

好的做法 👍

//这是一个报告日期的字段
readonly reportedDateField: Locator;

函数名

函数名总是以 "动词" 开头,后跟函数正在交互的 "组件上下文",即它对哪个实体产生影响。

好的做法 👍

getWorksOrder();

printTransactions();

deleteProperty();

来源

URL 来源