fixtures 重构项目
在开始准备重构时,我们项目都是基于对象模型去抽象公用方法
页面对象模型
将页面内的公用对象和公用方法封装起来放到一个模型中,我们把这个模型叫做页面对象模型。是UI自动化测试中业务逻辑的复用和简化的重要设计方法;
页面对象模型提取的原则
- 可以被复用或者将要被复用的元素选中、快捷方法
- 比较复杂操作可以考虑提取到页面对象模型(也可以是提取单独的文件),较少业务用例代码的负责度(我们希望是开发输出页面对象模型,测试来输出业务用例)
- 公共组件的元素选中、方法,如表格、弹窗、抽屉等组件
在做设计之时,会根据各个模块建立对应的模型,起初这个方法很好用,很容易上手,代码也很好理解(基于面向对象封装)
例如:封装一个新增事件的网络请求方法
该方法许多用例都需要用到,故选择封装
那么实现逻辑:
import { APIRequestContext, APIResponse } from '@playwright/test';
class Modal {
constructor (readonly request: APIRequestContext)
// xxx请求
public async post (url: string, options?: PostOptions): Promise<APIResponse> {
return this.request.post(`${baseUrl.default}${url}`, {
ignoreHTTPSErrors: true,
...options,
headers: {
'content-type': 'application/json',
...options?.headers
}
});
}
}
问题出现
当越封越多的模块,那么编写一个用例所需要创建的实例则越来越多
例如:
import { test } from '@playwright/test';
import { X } from './X';
import { XX } from './XX';
import { XXX } from './XXX';
import { XXXX } from './XXXX';
import { XXXXX } from './XXXXX';
test.describe('just a temple', () => {
test('just a temple', async ({page}) => {
const xInstance = new X(page);
const xxInstance = new XX(page);
const xxxInstance = new XXX(page);
const xxxxInstance = new XXXX(page);
const xxxxxInstance = new XXXXX(page);
});
test('test2 xx', async () => {
// 类似的重复操作
});
});
可以看到,这将会造成每一个页面每一个测试用例就会编写很多重复的代码,且页面也会变得很长,变得难以维护和不堪入目
fixtures 引入
什么是fixtures?
Playwright Test 基于
fixtures
的概念。fixtures
用于为每个测试建立环境,为测试提供所需的一切,仅此而已。fixtures
在测试之间是隔离的。使用fixtures
,您可以根据它们的含义对测试进行分组,而不是根据它们的通用设置。
简而言之,就是当引入 fixtures
后,你将不用受 describe
和重复代码的烦恼,可以自用的根据 fixtures
语意去分组,去进行测试用例编写
代码优化
以上代码经过优化后:
// 封装为公共方法 utils/fixtures
import { test as base } from '@playwright/test';
import { X } from './X';
import { XX } from './XX';
import { XXX } from './XXX';
import { XXXX } from './XXXX';
import { XXXXX } from './XXXXX';
type TestFixtures = {
x: X;
xx: XX;
xxx: XXX;
xxxx: XXXX;
xxxxx: XXXXX;
}
// 将封装后的test导出
export const test = base.extend<TestFixtures>({
x: async ({page}}, use) => {
await use(new X(page));
},
xx: async ({page}}, use) => {
await use(new XX(page));
},
xxx: async ({page}}, use) => {
await use(new XXX(page));
},
xxxx: async ({page}}, use) => {
await use(new XXXX(page));
},
xxxxx: async ({page}}, use) => {
await use(new XXXXX(page));
},
});
在页面中使用:
import { test } from 'utils/fixtures';
test.describe('just a temple', () => {
test('just a temple', async ({x, xx, xxx, xxxx, xxxxx}) => {
// 只需书写业务代码
});
test('test2 xx', async (x, xx, xxx, xxxx, xxxxx) => {
// 只需书写业务代码
});
)};
经过更改后的代码,减少了一大堆重复代码,且使用起来更加方便,代码也看着更舒服!
分组优化
优化前:使用 describe
来为 test
增加前置后置操作
// todo.spec.js
const { test } = require('@playwright/test');
const { TodoPage } = require('./todo-page');
test.describe('todo tests', () => {
let todoPage;
// 前置操作
test.beforeEach(async ({ page }) => {
todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
});
// 后置操作
test.afterEach(async () => {
await todoPage.removeAll();
});
test('should add an item', async () => {
await todoPage.addToDo('my item');
// ...
});
test('should remove an item', async () => {
await todoPage.remove('item1');
// ...
});
});
优化后:不需要再添加不必要的 describe
,test
用例可以更加自由,灵活
// example.spec.ts
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
// 封装test提供 todoPage fixtures
const test = base.extend<{ todoPage: TodoPage }>({
todoPage: async ({ page }, use) => {
// 前置操作
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
// fixtures 使用值
await use(todoPage);
// 后置操作
await todoPage.removeAll();
},
});
test('should add an item', async ({ todoPage }) => {
await todoPage.addToDo('my item');
// ...
});
test('should remove an item', async ({ todoPage }) => {
await todoPage.remove('item1');
// ...
});
遵循页面对象模型模式的fixtures
封装todoPage
,settingsPage
// my-test.ts
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
import { SettingsPage } from './settings-page';
// 定义fixtures的类型
type MyFixtures = {
todoPage: TodoPage;
settingsPage: SettingsPage;
};
// 导出封装好的test
export const test = base.extend<MyFixtures>({
todoPage: async ({ page }, use) => {
// fixtures 注册前行为
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
// 在测试中使用该值
await use(todoPage);
// fixtures 卸载后行为
await todoPage.removeAll();
},
settingsPage: async ({ page }, use) => {
await use(new SettingsPage(page));
},
});
落实项目
将公共模块的页面对象模型全部封装,并应用到项目中
/**
* @author: hwc13375
* @description: fixtures
*/
import { test as base } from '@playwright/test';
import { RequestCommon } from './request';
import { GlobalCommon, ConditionFilter, Drawer, Message, Modal, Table } from 'e2e-modules/models';
type TestFixtures = {
requestCommon: RequestCommon;
globalCommon: GlobalCommon;
conditionFilter: ConditionFilter;
drawer: Drawer;
message: Message;
modal: Modal;
table: Table;
};
export const test = base.extend<TestFixtures>({
requestCommon: async ({request, browser}, use) => {
await use(new RequestCommon(request, browser));
},
globalCommon: async ({page}, use) => {
await use(new GlobalCommon(page));
},
conditionFilter: async ({page}, use) => {
await use(new ConditionFilter(page));
},
drawer: async ({page}, use) => {
await use(new Drawer(page));
},
message: async ({page}, use) => {
await use(new Message(page));
},
modal: async ({page}, use) => {
await use(new Modal(page));
},
table: async ({page}, use) => {
await use(new Table(page));
},
});
最终结果
根据重构提交 pr 查看,平均一个场景减少50行左右的重复代码
一共优化了5个场景200+行的代码减少,使页面代码更简洁易维护,改变了以往的开发模式