Bootstrap

「自动化测试」playwright 前端自动化—— fixtures 项目重构优化思路

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');
    // ...
  });
});

优化后:不需要再添加不必要的 describetest用例可以更加自由,灵活

// 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封装todoPagesettingsPage

// 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+行的代码减少,使页面代码更简洁易维护,改变了以往的开发模式

;