playwright 拆解行为测试,并行测试

之前我们讲到了页面对象模型(Page Object Model, POM)重构交互代码,本文继续分享如何将不同的独特行为分离为独立的、原子的测试,从而让代码结构上更加清晰。

1. 识别独特行为

当前,我们有一个测试执行了五个步骤:

  1.  加载应用

  2. 创建一个新看板

  3. 创建一个新列表

  4. 向列表添加卡片

  5. 导航到主页 每个步骤都涵盖了一个独特的行为。如果其中任何一个行为失败,它将导致整个测试失败。此外,失败可能会阻止某些功能被覆盖。例如,如果创建新卡片失败,那么主页导航将不会被尝试。这并不理想:每个行为都应该有自己的测试来报告其结果。

每个行为都可以成为一个单独的测试。每个行为也可能包含相关但不同的行为。例如,卡片行为包括:

  • • 添加一张卡片

  • • 添加多张卡片

  • • 删除一张卡片

  • • 编辑卡片名称

  • • 移动卡片以改变列表的顺序

  • • 将卡片移动到不同的列表 所有这些行为都有一个共同的起点:必须有一个带有列表(或多个列表)的看板。这些测试可以共享常见的设置步骤。其他行为可能需要相同的设置步骤,但它们可能也有其他需求。

2. 将行为拆分为单独的测试

随着我们添加越来越多的测试,保持它们的原子性和独立性将变得越来越重要。让我们将一个大的测试拆分为多个涵盖各自行为的小测试。

2.1 测试结构

当前,trello-test.ts 模块有两个定义:

一个 test.beforeAll 函数,用于在所有测试之前清除数据库。 一个包含一个测试用例的 test 函数。 我们需要将测试拆分为多个测试。通常在使用 Playwright 时,最好将相关的测试组合在一起,以便它们可以共享常见对象并生成分层报告。

我们可以使用 test.describe 定义将测试组合在一起。为我们的新测试添加一个:

import { test, expect } from './fixtures/trello-test';

test.describe('Trello-like board', () => {
    // ...
});

由于我们的测试将共享常见的名称,如“Chores”“TODO”,让我们在 test.describe 块内将它们添加为常量:

const boardName = 'Chores';
const listName = 'TODO';

2.2 Before钩子

我们还应该转换设置步骤。之前,我们有一个 test.beforeAll 定义,它在所有测试之前运行一次。现在,我们需要在每个测试之前运行的设置。

此外,我们需要做的不仅仅是重置数据库。由于每个测试需要加载应用程序并创建一个看板,因此我们应该将这些交互添加到常见设置中。这将使我们避免在每个测试中重复步骤。

如下重写 before 钩子,并将其放入 test.describe 块中:

test.beforeEach(async ({ request, getStartedPage }) => {
    await request.post('http://localhost:3000/api/reset');
    await getStartedPage.load();
    await getStartedPage.createFirstBoard(boardName);
});

2.3 测试看板创建

第一个测试应该确保创建新看板正常工作。由于 before 钩子已经执行了创建看板的交互,这个测试只需要进行断言。

在 test.describe 定义中添加以下测试定义:

test('should create the first board', async ({ boardPage }) => {
    await boardPage.expectNewBoardLoaded(boardName);
});

编写仅包含断言的测试似乎有些奇怪。通常,我们希望在测试用例主体中包含交互(如 getStartedPage.createFirstBoard)以定义交互和验证。然而,由于这一组中的其他测试都需要一个已创建的看板作为起点,依赖 before 钩子创建看板只是为了方便。

2.4 测试列表创建

创建新看板后,用户可以在该看板上创建一个新列表。添加一个这样的测试:

test('should create the first list in a board', async ({ boardPage }) => {
    await boardPage.addList(listName);
    await expect(boardPage.listName).toHaveValue(listName);
});

这个测试不需要重复前一个测试的断言。相反,它可以专注于手头的行为:在看板上创建一个新列表。

2.5 测试卡片创建

一旦创建了看板,也可以测试卡片创建。添加一个这样的测试:

test('should create a list with multiple cards', async ({ boardPage }) => {
    await boardPage.addList(listName);
    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']);
});

这个测试确实需要在添加卡片之前在看板上创建一个列表。

2.6 测试主页导航

最后一个测试是主页导航。无论看板上有什么或没有什么,都无关紧要:我们可以独立于列表和卡片来测试导航。添加一个这样的测试:

test('should navigate home from a board', async ({ boardPage, myBoardsPage }) => {
    await boardPage.goHome();
    await myBoardsPage.expectLoaded([boardName]);
});

2.7 重新运行最终代码

最终测试代码应如下所示:

import { test, expect } from './fixtures/trello-test';

test.describe('Trello-like board', () => {
    const boardName = 'Chores';
    const listName = 'TODO';

    test.beforeEach(async ({ request, getStartedPage }) => {
        await request.post('http://localhost:3000/api/reset');
        await getStartedPage.load();
        await getStartedPage.createFirstBoard(boardName);
    });
    
    test('should create the first board', async ({ boardPage }) => {
        await boardPage.expectNewBoardLoaded(boardName);
    });

    test('should create the first list in a board', async ({ boardPage }) => {
        await boardPage.addList(listName);
        await expect(boardPage.listName).toHaveValue(listName);
    });

    test('should create a list with multiple cards', async ({ boardPage }) => {
        await boardPage.addList(listName);
        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']);
    });

    test('should navigate home from a board', async ({ boardPage, myBoardsPage }) => {
        await boardPage.goHome();
        await myBoardsPage.expectLoaded([boardName]);
    });
});

运行这些新测试(npx playwright test --ui 或 npx playwright test tests/trello.spec.ts --workers 1)以确保它们通过。(如果从UI模式运行它们,请确保一次运行一个!)覆盖范围与之前相同。主要好处是每个独特行为都是独立测试和报告的。

2.8 优化设置步骤

优化端到端测试的最佳方法之一是使用API调用来为测试设置数据。API交互比UI交互快得多且不易出错。例如,我们可以使用API调用在每个测试之前创建一个新看板,而不是依赖UI交互来输入文本并按下回车键。我们已经使用API调用在每个测试之前重置数据库。

这种方法有一些权衡:

  • • 编写API调用可能比编写UI交互更复杂。

  • • 在所有情况下,API调用可能不会比UI交互节省时间。

  • • API调用应仅用于设置和清理,而不是用于主要行为。

请根据您的最佳判断,在何时何地尝试使用API调用优化测试设置和清理。

小结

在本章中,我们通过将不同的行为分离为独立的测试,成功地将一个大测试拆解成多个小测试。这种方法不仅使每个测试更易于维护,还提高了测试的可读性和可靠性。我们还探讨了如何通过API调用来优化测试的设置步骤,以提高测试的效率和稳定性。

在下一篇文章中,我们将深入探讨“并行执行”的主题,了解如何利用Playwright的特性来同时运行多个测试,从而大幅缩短测试的总执行时间。

并行执行扩展测试

 

3. 尝试并行执行

自从我们开始为类似Trello的应用编写测试以来,我们一直是串行运行测试,也就是一次运行一个。现在试着并行运行它们,看看会发生什么:

npx playwright test tests/trello.spec.ts

不可避免地,一些测试将因测试数据的冲突而失败。在每个测试之前,测试套件都会重置整个数据库。所有测试都针对运行在本地机器上的一个Trello-like应用实例,因此重置数据库会影响所有正在运行的测试。在错误的时刻重置可能会破坏测试所需的板、列表和卡片。这不是我们想要的:每个行为都应该有自己的测试,以报告其结果。

4. 评估数据管理策略

Playwright在一组工作线程中并行执行测试。工作线程并行运行,但每个单独工作线程上的测试串行执行。有几种方法可以避免这些工作线程之间的测试数据冲突。

  • • 一种方法是使用多个Web应用实例。我们可以为每个工作线程运行一个应用实例,其中每个应用实例都有自己的数据库。在这种情况下,一个工作线程不可能影响另一个工作线程的应用或数据。然而,启动多个应用实例可能难以设置,并且可能会使用大量系统资源。

  • • 第二种方法是使用每个工作线程的单独测试数据集。例如,在许多应用中,一个用户无法访问另一个用户的数据。如果每个工作线程专门使用其自己的用户账户,则工作线程不会发生冲突。在Playwright中,我们可以将工作线程的并行索引映射到特定的数据集(如用户)。单独的测试数据集可能比多个应用实例更容易管理,但仍需要仔细管理。

  • • 第三种方法是编写测试,使其测试数据不会发生冲突。这意味着任何多个测试(或工作线程)可能访问的数据必须视为不可变(或常量)。任何测试动态创建的数据必须由该测试独占使用。这是当管理环境和数据太困难(或不可能)时最简单的策略。然而,它通常限制了测试的覆盖范围。某些测试需要修改共享数据,这些测试必须要么被跳过,要么在并行执行之外串行运行。

5. 重写测试以避免冲突

对于本教程,数据管理策略#3是最容易实现的。让我们更改代码以实现它。

认识到失去的覆盖 不幸的是,这种数据管理策略将要求我们跳过“Get Started!”页面。“Get Started!”页面仅在应用没有创建板时出现。由于每个测试都需要一个板,并且所有板都是共享的,我们不能强制“Get Started!”页面出现。

为了简化,我们将删除与“Get Started!”页面的测试和交互。我们还将更改前置钩子,以通过API为每个测试创建一个具有唯一名称的新板。这样,每个测试都可以有一个共同的起点。

添加与我的板页面的交互 由于我们将删除“Get Started!”页面,我们需要添加与“我的板”页面的新交互。

当每个测试启动并创建其板时,它将加载主页,应该重定向到“我的板”页面,而不是“Get Started!”页面。将以下方法添加到tests/pages/my-boards.ts

async load() {
    await this.page.goto('http://localhost:3000/');
}

要加载板页面,测试需要通过名称打开该板。也为此添加一个方法:

async openBoard(boardName: string) {
    await this.page.getByText(boardName).click();
}

重写测试设置 当前的test.beforeEach钩子重置整个数据库并创建第一个板。其代码如下:

const boardName = 'Chores';
const listName = 'TODO';

test.beforeEach(async ({ request, getStartedPage }) => {
    await request.post('http://localhost:3000/api/reset');
    await getStartedPage.load();
    await getStartedPage.createFirstBoard(boardName);
});

我们需要以几种方式更改它。首先,每个测试不能清除整个数据库。相反,每个测试需要创建自己的新板。每个板需要一个唯一的名称,以便其他测试不会意外使用它。一个简单的方法是为每个板分配一个随机数。

其次,每个测试需要加载Web应用并打开它创建的板。这就是MyBoardsPage的新方法将被调用的地方。

将代码更改为如下所示:

let boardName: string;
const listName = 'TODO';

test.beforeEach(async ({ request, myBoardsPage }) => {
    const randomNumber = Math.trunc(Math.random() * 1000000);
    boardName = 'Chores ' + `${randomNumber}`;

    await request.post('http://localhost:3000/api/boards', {data: {name: boardName}});
    await myBoardsPage.load();
    await myBoardsPage.openBoard(boardName);
});

6. 重写测试用例

值得庆幸的是,测试用例步骤可以保持不变,因为前置钩子为每个测试创建了一个唯一的板。

唯一需要更改的是更改其中一个测试名称:“应创建第一个板”不再作为标题有意义,因为验证的板可能不是创建的“第一个”板。让我们将其更改为“应显示新板”:

test('should display the new board', async ({ boardPage }) => {
    await boardPage.expectNewBoardLoaded(boardName);
});

理论上,我们还可以删除tests/pages/get-started.ts文件,并从tests/fixtures/trello-test.ts中删除它,因为它不再使用。

7. 大规模运行测试

使用更新的代码并行重新运行测试:

npx playwright test tests/trello.spec.ts

不仅Playwright会跨多个工作线程运行测试,它还会跨所有三个浏览器(ChromiumFirefoxWebKit)运行测试。

在并行运行测试时,请注意奇怪的、间歇性的失败。这些表明发生了冲突。

另一个需要考虑的因素是数据库清理。对于我们选择的数据策略,测试将继续添加越来越多的板。最终,数据库应重置,否则可能会破坏应用或运行应用的机器。数据库重置应定期在测试自动化代码之外进行。

总结

在本章中,我们通过在不同浏览器上并行运行测试,扩展了我们的测试规模。我们探讨了避免测试数据冲突的不同策略,并选择了最简单的策略:重写测试以避免数据冲突。尽管这一策略牺牲了一些测试覆盖范围,但它使我们的测试能够安全并行运行。

在下篇文章中,我们将深入探讨并行执行的高级主题,包括如何优化测试执行时间和管理并行测试环境。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员的世界你不懂

你的鼓励将是我创造的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值