playwright一直是我最看好的新一代自动化测试框架,2022年底playwright在npm上的下载量超过了100万,尽管不如selenium和cypress,不过势头还是相当强劲的。最近正好发现一篇文章简单的介绍了使用typescript,pageobject和fixture配合playwright进行用例编写的文章,这里把里面的精华拿出来分享一下。

老生常谈,playwright的优势

  • 有个好爹,微软出品,看好长期更新维护和迭代,但也可能突然被砍掉,毕竟大公司都在裁员
  • 运行速度快
  • 自动等待元素出现
  • 报告的呈现很多元化,可以设置重试机制,捕获执行日志,截屏录屏等
  • 支持多个浏览器并行执行
  • 提供自动生成代码能力以及Inspector GUI
  • 一套代码,跨浏览器执行的能力

目录结构

框架整体的目录结构如下。

.
├── config
│   ├── global-setup.ts
│   └── playwright.config.ts
├── package-lock.json
├── package.json
└── src
    ├── data
    │   └── data.json
    ├── fixtures
    │   ├── AxeFixture.ts
    │   └── TodoFixture.ts
    ├── pages
    │   └── TodoPage.ts
    └── tests
        ├── a11y.spec.ts
        └── demo-pom-todo-app.spec.ts

config目录

  • playwright.config.ts playwright的配置文件
  • ***global-setup.ts*** 在所有用例执行前运行一次,主要的目的是登录一次被测系统并保存浏览器的全局状态到storageState.json文件中。这样就不需要每个用例都去单独登录一次了。更多信息可以参考文档。https://playwright.dev/docs/test-advanced#global-setup-and-teardown

Page Object

po基本上是自建框架的必选项了。具体的实现如下

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

export class TodoDemoPage {
  readonly page: Page;
  readonly newTodoInput: Locator;
  readonly todoTitle: Locator;
  readonly todoCount: Locator

  constructor(page: Page) {
    this.page = page;
    this.newTodoInput = page.getByPlaceholder('What needs to be done?');
    this.todoTitle = page.getByTestId('todo-title');
    this.todoCount = page.getByTestId('todo-count');
  }

  async goto() {
    await this.page.goto('https://demo.playwright.dev/todomvc');
  }

  async addTodo(data: string) {
    await this.newTodoInput.fill(data)
    await this.newTodoInput.press("Enter")
  }

  async addDefaultTodos(todosItems: string[]) {
    for (const todo of todosItems) {
      await this.addTodo(todo)
    }
  }
...
}

关于po需要注意几点

  • 命名规则,确保页面上的元素和一些页面方法都有合适的名称
  • 一个方法只做一件事情,而且可以通过方法名推测出来
  • 对页面呈现的一些结果进行断言
  • dry,don’t repeat yourself

Fixtures

fixture可以简单理解为准备数据,设置上下文环境

import { test as base } from '@playwright/test';
import { TodoDemoPage } from '../pages/TodoPage';

type MyFixtures = {
  todoDemoPage: TodoDemoPage;
  noneExistingPage: any
};

export const todoDemoPage = async({page}, use) => {
  const todoDemoPage = new TodoDemoPage(page);
  // Set up the fixture.
  await todoDemoPage.goto();
  // Use the fixture value in the test.
  await use(todoDemoPage);
}

// we can create as many fixtures as we want, but I prefer to store them in separate files
export const noneExistingPage = async({page}, use) => {
  // Let's imagine we have another fixter-page set up here
}

export const test = base.extend<MyFixtures>({todoDemoPage, noneExistingPage});

上面的代码其实就是创建了TotoDemoPage,后面用例里就可以直接使用这个页面了。

fixture的好处还是很多的。

  • fixture让setup和teardown钩子函数在同一个地方进行定义,这样就比较好维护了
  • fixture可以重复使用
  • fixture按需使用,定义了你也可以不用
  • fixture可以组合使用

测试用例

用例相对就比较简单的,因为难的部分已经搞完了。

import { test } from '../fixtures/TodoFixture'

const TODO_ITEMS = [
    'buy some cheese',
    'buy bottle of wine, or two',
    'celebrate'
];

// our test is imported from fixtures folder
// so we can have access to tododDempPage and noneExistingPage objects
// in callback function trhough destructuring and we can use it for our needs
test.describe('New Todo', () => {
  test('should allow me to add todo items', async ({ todoDemoPage }) => {
    await todoDemoPage.addTodo(TODO_ITEMS[0])
    await todoDemoPage.checkInputIsEmpty();
    await todoDemoPage.addTodo(TODO_ITEMS[1])
    await todoDemoPage.checkAddedTodos([TODO_ITEMS[0], TODO_ITEMS[1]])
    await todoDemoPage.checkNumberOfTodosInLocalStorage(2);
  });

  test('should clear text input field when an item is added', async ({ todoDemoPage }) => {
    await todoDemoPage.addTodo(TODO_ITEMS[0])
    await todoDemoPage.checkInputIsEmpty();
    await todoDemoPage.checkNumberOfTodosInLocalStorage(1);
  });

  test('should append new items to the bottom of the list', async ({ todoDemoPage }) => {
    await todoDemoPage.addDefaultTodos(TODO_ITEMS);
    await todoDemoPage.checkDefaultAddedTodods(TODO_ITEMS);
    await todoDemoPage.checkAddedTodos(TODO_ITEMS)
    await todoDemoPage.checkNumberOfTodosInLocalStorage(3);
  });
});

用例很简洁易懂对吧。这里断言都封装在了page object里,所以整个流程全是对po实例进行调用,很统一,不过我不是很喜欢这种方式,我更喜欢把原生断言放在用例里,这样po层会更简洁一些,要不需要绞尽脑汁去给封装断言的方法取名。

CICD

可以在官方文档找到方法。https://playwright.dev/docs/ci

总结

https://github.com/eugeniuszG/playwright-starter 这里有框架模板,大家可以下载下来进行二次开发,最后感谢作者的总结和示例。