1. 创建一个新的Playwright
项目
以下为创建一个新的测试项目详细步骤
1.1 创建一个新目录
mkdir awesome-playwright-tests
cd awesome-playwright-tests
1.2 初始化Playwright
npm init playwright@latest
init
命令将询问一系列问题。接受问题的默认答案:
-
• 选择
TypeScript
作为项目语言。 -
• 选择
tests
作为测试目录。 -
• 对添加
GitHub Actions
工作流回答false
。 -
• 对安装
Playwright
浏览器回答true
。此命令将创建一系列新的项目文件,包括: -
• 一个包含
Playwright
包依赖项的package.json
文件 -
• 一个包含测试配置的
playwright.config.ts
文件 -
• 一个包含基本示例测试的
tests
目录 -
• 一个包含更广泛示例测试的
tests-examples
目录 该命令还将安装Playwright
浏览器项目:Chromium
、Firefox
和WebKit
。如果您的网络连接较慢,浏览器安装可能需要几分钟时间。
2. 可视化运行测试
Playwright
是一个现代的Web
测试框架,其功能补充了前端开发工作流程。其最有用的功能之一是UI
模式,可视化显示和执行测试。让我们使用它来探索自动生成的示例测试。
运行以下命令以打开UI
模式:
npx playwright test --ui
一个测试执行窗口应该会打开。左侧栏显示 tests
文件夹下的所有测试。点击测试在底部的“源代码”窗格中查看其源代码。
2.1 UI
模式与基本示例测试
example.spec.ts
文件中的代码应如下所示:
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// 期望标题包含一个子字符串。
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// 点击“Get started”链接。
await page.getByRole('link', { name: 'Get started' }).click();
// 期望URL包含“intro”。
await expect(page).toHaveURL(/.*intro/);
});
点击测试名称旁边的三角图标以运行测试。测试应通过。中间窗格将显示测试执行的逐步跟踪,以及每个步骤的截图。您还可以将鼠标悬停在顶部的时间线跟踪上,以查看随时间变化的截图。
2.2 UI
模式与更广泛的示例测试
要查看更多广泛的示例,请将 tests-examples/demo-todo-app.spec.ts
文件移动到 tests
目录,然后通过UI模式探索这些测试。
UI
模式在较大的测试套件中仍然可管理。它甚至会在代码更改时自动重新加载。
2.3 从命令行运行测试
在UI模式下运行测试有助于开发应用和测试,但在持续集成(CI
)系统中运行测试时并不理想。您可以直接从命令行运行测试,如下所示:
npx playwright test
测试需要几秒钟才能运行。结果应如下所示:
Running 78 tests using 4 workers
78 passed (15.0s)
要打开最后的HTML
报告,请运行:
npx playwright show-report
Playwright将自动并行运行测试,并跨多个工作者。仔细观察测试运行时的控制台输出,你还会发现 Playwright
会自动在三个浏览器项目中运行所有测试:Chromium
、Firefox
和 WebKit
。(因此,消息报告总共26 x 3 = 78个测试。)
您可以通过运行以下命令查看完整的测试报告:
npx playwright show-report
这将在浏览器中打开报告。你可以筛选测试并点击结果以查看更深入的跟踪。每个结果还带有目标浏览器的标签。
如果你只想针对一个浏览器而不是所有浏览器运行测试,可以添加 --project
参数和浏览器的名称。例如,仅针对 Chromium 运行测试,请运行:
npx playwright test --project chromium
默认情况下,Playwright
以无头模式运行测试,这意味着它不会打开浏览器窗口来可视化呈现页面。要在测试运行时查看呈现的页面,请添加 --headed
选项。然而,有头执行会减慢执行速度,主要应保留用于一次调试测试。
例如
npx playwright test tests/example.spec.ts --headed --workers 1 --project chromium
通常,对于调试,最好使用UI模式。
3. 生成测试代码
Playwright
还提供了一个测试生成器,使你可以在实时浏览器中记录交互。虽然测试生成器不会生成完美的测试脚本,但它可以帮助您快速找到定位器和交互方法。
要启动测试生成器,请运行:
npx playwright codegen
Playwright
将打开两个窗口:一个用于捕获交互的浏览器窗口,以及一个显示捕获交互的 Playwright
代码的窗口。
尝试加载网页并与其进行交互。你将看到实时生成的 Playwright
代码。记录完成后,你可以复制代码并将其改进为测试用例。
小结论
演示了如何从0到1创建新测试项目,撰写测试case,运行测试代码,以及利用测试生成器自动生成代码等操作。希望帮助你迅速上手Playwright
并开始相关的自动化测试任务
我们在之前的文章介绍了Web测试的目标,探索了Playwright
的特性,下面将带你从头开始构建web应用,并利用Playwright
完成相关测试。
4. 创建一个Trello
风格的Web
应用
为了为一个本地Trello
风格的Web
应用编写Playwright
测试,首先需要完成以下步骤:
4.1 克隆仓库
cd </some/different/path>
git clone https://github.com/filiphric/trelloapp-vue-vite-ts.git
4.2 安装依赖
cd trelloapp-vue-vite-ts
npm install
4.3 运行应用
npm start
成功运行后,应该会看到类似如下的输出:
vite v2.9.12 dev server running at:
> Local: http://localhost:3000/
> Network: use `--host` to expose
ready in 357ms.
4.4 打开应用
在浏览器中访问 http://localhost:3000/
查看应用的 "Get Started!"
页面。
5. 生成Playwright
脚本
使用Playwright
的测试生成器来记录交互操作:
5.1 启动录制器
npx playwright codegen
5.2 记录操作
-
• 加载应用
(http://localhost:3000/)
。 -
• 创建一个名为
"Chores"
的新看板。 -
• 添加一个标题为
"TODO"
的列表。 -
• 添加几张卡片:
"Buy groceries"
、"Mow the lawn"
和"Walk the dog"
。 -
• 返回到
"My Boards"
页面。
5.3生成脚本
得到类似脚本:
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('http://localhost:3000/');
await page.getByPlaceholder('Name of your first board').click();
await page.getByPlaceholder('Name of your first board').fill('Chores');
await page.getByPlaceholder('Name of your first board').press('Enter');
await page.getByPlaceholder('Enter list title...').click();
await page.getByPlaceholder('Enter list title...').fill('TODO');
await page.getByPlaceholder('Enter list title...').press('Enter');
await page.getByText('Add another card').click();
await page.getByPlaceholder('Enter a title for this card...').fill('Buy groceries');
await page.getByRole('button', { name: 'Add card' }).click();
await page.getByPlaceholder('Enter a title for this card...').click();
await page.getByPlaceholder('Enter a title for this card...').fill('Mow the lawn');
await page.getByRole('button', { name: 'Add card' }).click();
await page.getByPlaceholder('Enter a title for this card...').click();
await page.getByPlaceholder('Enter a title for this card...').fill('Walk the dog');
await page.getByRole('button', { name: 'Add card' }).click();
await page.getByRole('navigation').getByRole('button').click();
});
6. 将脚本转换为测试用例
6.1 创建测试文件
在 tests
目录下创建一个名为 trello.spec.ts
的文件,并粘贴生成的代码。
6.2 完善测试
重命名测试
test('Create a new board with a list and cards', async ({ page }) => {
加载应用
await page.goto('http://localhost:3000/');
创建新看板
await page.getByPlaceholder('Name of your first board').fill('Chores');
await page.getByPlaceholder('Name of your first board').press('Enter');
await expect(page.locator('[name="board-title"]')).toHaveValue('Chores');
await expect(page.getByPlaceholder('Enter list title...')).toBeVisible();
await expect(page.locator('[data-cy="list"]')).not.toBeVisible();
创建新列表
await page.getByPlaceholder('Enter list title...').fill('TODO');
await page.getByPlaceholder('Enter list title...').press('Enter');
await expect(page.locator('[data-cy="list-name"]')).toHaveValue('TODO');
添加卡片
await page.getByText('Add another card').click();
await page.getByPlaceholder('Enter a title for this card...').fill('Buy groceries');
await page.getByRole('button', { name: 'Add card' }).click();
await page.getByPlaceholder('Enter a title for this card...').fill('Mow the lawn');
await page.getByRole('button', { name: 'Add card' }).click();
await page.getByPlaceholder('Enter a title for this card...').fill('Walk the dog');
await page.getByRole('button', { name: 'Add card' }).click();
await expect(page.locator('[data-cy="card-text"]')).toHaveText(
['Buy groceries', 'Mow the lawn', 'Walk the dog']);
返回主页
await page.getByRole('navigation').getByRole('button').click();
await expect(page.getByText('My Boards')).toBeVisible();
await expect(page.getByText('Chores')).toBeVisible();
6.3 重置数据库
在运行测试前,确保数据库被重置为初始状态:
test.beforeAll(async ({ request }) => {
await request.post('http://localhost:3000/api/reset');
});
完整测试代码
import { test, expect } from '@playwright/test';
test.beforeAll(async ({ request }) => {
await request.post('http://localhost:3000/api/reset');
});
test('Create a new board with a list and cards', async ({ page }) => {
await page.goto('http://localhost:3000/');
await page.getByPlaceholder('Name of your first board').fill('Chores');
await page.getByPlaceholder('Name of your first board').press('Enter');
await expect(page.locator('[name="board-title"]')).toHaveValue('Chores');
await expect(page.getByPlaceholder('Enter list title...')).toBeVisible();
await expect(page.locator('[data-cy="list"]')).not.toBeVisible();
await page.getByPlaceholder('Enter list title...').fill('TODO');
await page.getByPlaceholder('Enter list title...').press('Enter');
await expect(page.locator('[data-cy="list-name"]')).toHaveValue('TODO');
await page.getByText('Add another card').click();
await page.getByPlaceholder('Enter a title for this card...').fill('Buy groceries');
await page.getByRole('button', { name: 'Add card' }).click();
await page.getByPlaceholder('Enter a title for this card...').fill('Mow the lawn');
await page.getByRole('button', { name: 'Add card' }).click();
await page.getByPlaceholder('Enter a title for this card...').fill('Walk the dog');
await page.getByRole('button', { name: 'Add card' }).click();
await expect(page.locator('[data-cy="card-text"]')).toHaveText(
['Buy groceries', 'Mow the lawn', 'Walk the dog']);
await page.getByRole('navigation').getByRole('button').click();
await expect(page.getByText('My Boards')).toBeVisible();
await expect(page.getByText('Chores')).toBeVisible();
});
6.4. 运行测试
在UI
模式下运行
npx playwright test --ui

从命令行运行
npx playwright test tests/trello.spec.ts --workers 1
通过这些步骤,可以为一个本地Trello
风格的Web
应用编写和运行第一个Playwright
测试,确保测试在一个干净的数据库环境中开始,并验证应用的关键功能。
回顾现有的交互代码
让我们先来看一下之前的测试代码:
// 加载应用
await page.goto('http://localhost:3000/');
// 创建一个新的看板
await page.getByPlaceholder('Name of your first board').fill('Chores');
await page.getByPlaceholder('Name of your first board').press('Enter');
await expect(page.locator('[name="board-title"]')).toHaveValue('Chores');
await expect(page.getByPlaceholder('Enter list title...')).toBeVisible();
await expect(page.locator('[data-cy="list"]')).not.toBeVisible();
// 创建一个新的列表
await page.getByPlaceholder('Enter list title...').fill('TODO');
await page.getByPlaceholder('Enter list title...').press('Enter');
await expect(page.locator('[data-cy="list-name"]')).toHaveValue('TODO');
// 向列表添加卡片
await page.getByText('Add another card').click();
await page.getByPlaceholder('Enter a title for this card...').fill('Buy groceries');
await page.getByRole('button', { name: 'Add card' }).click();
await page.getByPlaceholder('Enter a title for this card...').fill('Mow the lawn');
await page.getByRole('button', { name: 'Add card' }).click();
await page.getByPlaceholder('Enter a title for this card...').fill('Walk the dog');
await page.getByRole('button', { name: 'Add card' }).click();
await expect(page.locator('[data-cy="card-text"]')).toHaveText(['Buy groceries', 'Mow the lawn', 'Walk the dog']);
// 返回主页
await page.getByRole('navigation').getByRole('button').click();
await expect(page.getByText('My Boards')).toBeVisible();
await expect(page.getByText('Chores')).toBeVisible();
通过审查上述代码,我们发现许多选择器(locators)
重复使用多次,代码显得冗长且难以维护。引入页面对象模型可以帮助解决这些问题。
7. 创建页面对象类
7.1 Get Started
页面
首先,为 "Get Started!"
页面创建一个页面对象类。创建一个新的文件 get-started.ts
,并添加以下代码:
import { type Locator, type Page } from '@playwright/test';
export class GetStartedPage {
readonly page: Page;
readonly firstBoardInput: Locator;
constructor(page: Page) {
this.page = page;
this.firstBoardInput = page.getByPlaceholder('Name of your first board');
}
async load() {
await this.page.goto('http://localhost:3000/');
}
async createFirstBoard(name: string) {
await this.firstBoardInput.fill(name);
await this.firstBoardInput.press('Enter');
}
}
7.2 Board
页面
接下来,为看板页面创建一个页面对象类。创建文件 board.ts
,并添加以下代码:
import { expect, type Locator, type Page } from '@playwright/test';
export class BoardPage {
readonly page: Page;
readonly boardTitle: Locator;
readonly enterListTitle: Locator;
readonly boardLists: Locator;
readonly listName: Locator;
readonly addAnotherCard: Locator;
readonly enterCardTitle: Locator;
readonly addCard: Locator;
readonly cardTexts: Locator;
readonly homeButton: Locator;
constructor(page: Page) {
this.page = page;
this.boardTitle = page.locator('[name="board-title"]');
this.enterListTitle = page.getByPlaceholder('Enter list title...');
this.boardLists = page.locator('[data-cy="list"]');
this.listName = page.locator('[data-cy="list-name"]');
this.addAnotherCard = page.getByText('Add another card');
this.enterCardTitle = page.getByPlaceholder('Enter a title for this card...');
this.addCard = page.getByRole('button', { name: 'Add card' });
this.cardTexts = page.locator('[data-cy="card-text"]');
this.homeButton = page.getByRole('navigation').getByRole('button');
}
async expectNewBoardLoaded(name: string) {
await expect(this.boardTitle).toHaveValue(name);
await expect(this.enterListTitle).toBeVisible();
await expect(this.boardLists).not.toBeVisible();
}
async addList(name: string) {
await this.enterListTitle.fill(name);
await this.enterListTitle.press('Enter');
}
async addCardToList(listIndex: number, cardName: string) {
await this.boardTitle.click(); // 取消焦点
await this.addAnotherCard.nth(listIndex).click();
await this.enterCardTitle.fill(cardName);
await this.addCard.click();
}
async goHome() {
await this.homeButton.click();
}
}
7.3 My Boards
页面
最后,为 "My Boards"
页面创建一个页面对象类。创建文件 my-boards.ts
,并添加以下代码:
import { expect, type Locator, type Page } from '@playwright/test';
export class MyBoardsPage {
readonly page: Page;
readonly myBoardsTitle: Locator;
constructor(page: Page) {
this.page = page;
this.myBoardsTitle = page.getByText('My Boards');
}
async expectLoaded(boardNames: string[]) {
await expect(this.myBoardsTitle).toBeVisible();
for (const name of boardNames) {
await expect(this.page.getByText(name)).toBeVisible();
}
}
}
7.4 重写测试代码
在 trello.spec.ts
中导入这些页面对象,并重写测试代码:
import { GetStartedPage } from './pages/get-started';
import { BoardPage } from './pages/board';
import { MyBoardsPage } from './pages/my-boards';
test('Create a new board with a list and cards', async ({ page }) => {
const getStartedPage = new GetStartedPage(page);
const boardPage = new BoardPage(page);
const myBoardsPage = new MyBoardsPage(page);
// 加载应用
await getStartedPage.load();
// 创建一个新的看板
await getStartedPage.createFirstBoard('Chores');
await boardPage.expectNewBoardLoaded('Chores');
// 创建一个新的列表
await boardPage.addList('TODO');
await expect(boardPage.listName).toHaveValue('TODO');
// 向列表添加卡片
await boardPage.addCardToList(0, 'Buy groceries');
await boardPage.addCardToList(0, 'Mow the lawn');
await boardPage.addCardToList(0, 'Walk the dog');
await expect(boardPage.cardTexts).toHaveText(['Buy groceries', 'Mow the lawn', 'Walk the dog']);
// 返回主页
await boardPage.goHome();
await myBoardsPage.expectLoaded(['Chores']);
});
7.5 创建页面对象 Fixture
为了避免在每个测试函数中重复构造页面对象,可以创建一个 Fixture
来管理页面对象。创建文件 trello-test.ts
,并添加以下代码:
import { test as base } from '@playwright/test';
import { GetStartedPage } from '../pages/get-started';
import { BoardPage } from '../pages/board';
import { MyBoardsPage } from '../pages/my-boards';
type TrelloFixtures = {
getStartedPage: GetStartedPage;
boardPage: BoardPage;
myBoardsPage: MyBoardsPage;
};
export const test = base.extend<TrelloFixtures>({
getStartedPage: async ({ page }, use) => {
await use(new GetStartedPage(page));
},
boardPage: async ({ page }, use) => {
await use(new BoardPage(page));
},
myBoardsPage: async ({ page }, use) => {
await use(new MyBoardsPage(page));
},
});
export { expect } from '@playwright/test';
在 trello.spec.ts
中使用新的 Fixture
:
import { test, expect } from './fixtures/trello-test';
test.beforeAll(async ({ request }) => {
await request.post('http://localhost:3000/api/reset');
});
test('Create a new board with a list and cards', async ({ getStartedPage, boardPage, myBoardsPage }) => {
await getStartedPage.load();
await getStartedPage.createFirstBoard('Chores');
await boardPage.expectNewBoardLoaded('Chores');
await boardPage.addList('TODO');
await expect(boardPage.listName).toHaveValue('TODO');
await boardPage.addCardToList(0, 'Buy groceries');
await boardPage.addCardToList(0, 'Mow the lawn');
await boardPage.addCardToList(0, 'Walk the dog');
await expect(boardPage.cardTexts).toHaveText(['Buy groceries', 'Mow the lawn', 'Walk the dog']);
await boardPage.goHome();
await myBoardsPage.expectLoaded(['Chores']);
通过以上改造,从代码结构上看起来更清晰一些。
8. 测试执行
8.1 UI mode
npx playwright test --ui
8.2 命令行
npx playwright test tests/trello.spec.ts --workers 1
总结
基于前面介绍过的web
测试模板,playwright
基础特性,以及如何开始撰写第一个自动化测试代码之后,本文展示了如何利用页面对象项目对代码重构优化。下一章节将进一步结合不同的行为模式把测试细化为不同的原子操作,欢迎关注。