并行性

介绍

Playwright Test 在并行运行测试。为了实现这一点,它会运行多个 worker 进程,这些进程会同时运行。默认情况下,测试文件会并行执行。单个文件中的测试按顺序执行,并且在同一个 worker 进程中运行。

  • 您可以使用 test.describe.configure 来配置在单个文件中并行运行测试。

  • 您可以使用 testProject.fullyParallel 或 testConfig.fullyParallel 配置整个项目,使所有文件中的所有测试并行运行。

  • 要禁用并行性,可以将 worker 数量限制为一个。

您可以控制并行 worker 进程的数量,并限制整个测试套件中的失败次数,以提高效率。

Worker 进程

所有测试都在 worker 进程中运行。这些进程是操作系统进程,独立运行,由测试运行器协调。所有 worker 都有相同的环境,并且每个进程都会启动自己的浏览器。

进程之间无法进行通信。Playwright Test 会尽可能重用一个 worker 来加速测试,因此多个测试文件通常会依次在同一个 worker 中运行。

在【测试失败】后,所有的 worker 都会被关闭,以保证后续测试的环境是干净的。

限制 Worker 数量

您可以通过【命令行】或【配置文件】控制最大并行 worker 进程数量。

通过命令行:

npx playwright test --workers 4

在配置文件中:

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Limit the number of workers on CI, use default locally
  workers: process.env.CI ? 2 : undefined,
});

关闭并行

您可以通过始终只允许一个 worker 来禁用任何并行性。可以在配置文件中设置 workers: 1 选项,或者在命令行中传递 --workers=1

npx playwright test --workers=1

在单个文件中并行测试

默认情况下,单个文件中的测试是按顺序执行的。如果你在单个文件中有许多独立的测试,可能希望使用 test.describe.configure() 将它们并行执行。

请注意,并行测试是在独立的 worker 进程中执行的,它们不能共享任何状态或全局变量。每个测试都会执行与其相关的所有钩子,包括 beforeAllafterAll

import { test } from '@playwright/test';

test.describe.configure({ mode: 'parallel' });

test('runs in parallel 1', async ({ page }) => { /* ... */ });
test('runs in parallel 2', async ({ page }) => { /* ... */ });

或者,你可以选择在配置文件中将所有测试启用完全并行模式:

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  fullyParallel: true,
});

你也可以选择仅为某些项目启用完全并行模式:

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // runs all tests in all files of a specific project in parallel
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
      fullyParallel: true,
    },
  ]
});

串行模式

你可以将相互依赖的测试标记为串行。如果其中一个串行测试失败,所有后续的测试都会被跳过。整个测试组会一起重试。

不推荐使用串行模式。通常更好的做法是使测试彼此独立,这样它们就可以独立运行。

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

// Annotate entire file as serial.
test.describe.configure({ mode: 'serial' });

let page: Page;

test.beforeAll(async ({ browser }) => {
  page = await browser.newPage();
});

test.afterAll(async () => {
  await page.close();
});

test('runs first', async () => {
  await page.goto('https://playwright.dev/');
});

test('runs second', async () => {
  await page.getByText('Get Started').click();
});

在多台机器之间分片测试

Playwright Test 可以将测试套件进行分片,以便在多台机器上执行。有关更多详细信息,请参见【分片指南】。

npx playwright test --shard=2/3

限制失败次数并快速失败

你可以通过设置 maxFailures 配置选项或传递 --max-failures 命令行标志来限制整个测试套件中的失败测试数量。

当设置了 "最大失败次数" 时,Playwright Test 会在达到指定的失败测试数量后停止,并跳过任何尚未执行的测试。这对于避免在有问题的测试套件上浪费资源非常有用。

通过命令行选项传递:

npx playwright test --max-failures=10

配置文件中的设置:

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Limit the number of failures on CI to save resources
  maxFailures: process.env.CI ? 10 : undefined,
});

工作进程索引和并行索引

每个工作进程都会被分配两个 ID:一个唯一的工作进程索引(从 1 开始),以及一个介于 0 到 workers - 1 之间的并行索引。当工作进程被重启时(例如发生故障后),新的工作进程会拥有相同的 parallelIndex 和一个新的 workerIndex

你可以通过环境变量 process.env.TEST_WORKER_INDEXprocess.env.TEST_PARALLEL_INDEX 获取索引,或者通过 testInfo.workerIndextestInfo.parallelIndex 访问它们。

在并行工作进程之间隔离测试数据

你可以利用上面提到的 process.env.TEST_WORKER_INDEXtestInfo.workerIndex 来隔离在不同工作进程上运行的测试之间的数据库用户数据。所有由该工作进程运行的测试都会复用相同的用户。

创建 playwright/fixtures.ts 文件,该文件将创建一个 dbUserName fixture,并在测试数据库中初始化一个新的用户。使用 testInfo.workerIndex 来区分不同的工作进程。

playwright/fixtures.ts
import { test as baseTest, expect } from '@playwright/test';
// Import project utils for managing users in the test database.
import { createUserInTestDatabase, deleteUserFromTestDatabase } from './my-db-utils';

export * from '@playwright/test';
export const test = baseTest.extend<{}, { dbUserName: string }>({
  // Returns db user name unique for the worker.
  dbUserName: [async ({ }, use) => {
    // Use workerIndex as a unique identifier for each worker.
    const userName = `user-${test.info().workerIndex}`;
    // Initialize user in the database.
    await createUserInTestDatabase(userName);
    await use(userName);
    // Clean up after the tests are done.
    await deleteUserFromTestDatabase(userName);
  }, { scope: 'worker' }],
});

现在,每个测试文件应该从我们的 fixtures 文件中导入 test,而不是从 @playwright/test 导入。

tests/example.spec.ts
// Important: import our fixtures.
import { test, expect } from '../playwright/fixtures';

test('test', async ({ dbUserName }) => {
  // Use the user name in the test.
});

控制测试顺序

Playwright Test 会按照声明的顺序运行同一文件中的测试,除非你【在单个文件中并行化测试】。

由于 Playwright Test 默认并行运行测试文件,因此无法保证测试在文件间的执行顺序。然而,如果【禁用并行化】,你可以通过按字母顺序命名文件或使用 “测试列表” 文件来控制测试顺序。

按字母顺序排序测试文件

当你禁用并行测试执行时,Playwright Test 会按字母顺序运行测试文件。你可以使用一些命名约定来控制测试顺序,例如 001-user-signin-flow.spec.ts002-create-new-document.spec.ts 等。

使用 "test list" 文件

不推荐使用测试列表,并且只作为一种最佳努力来支持。某些功能,如 VS Code 扩展和追踪,可能无法与测试列表正常配合使用。

你可以将测试放在多个文件中的辅助函数中。考虑以下示例,其中测试不是直接在文件中定义,而是通过一个包装函数来定义。

feature-a.spec.ts
import { test, expect } from '@playwright/test';

export default function createTests() {
  test('feature-a example test', async ({ page }) => {
    // ... test goes here
  });
}
feature-b.spec.ts
import { test, expect } from '@playwright/test';

export default function createTests() {
  test.use({ viewport: { width: 500, height: 500 } });

  test('feature-b example test', async ({ page }) => {
    // ... test goes here
  });
}

你可以创建一个测试列表文件来控制测试的顺序——首先运行 feature-b 测试,然后运行 feature-a 测试。请注意,每个测试文件都被包装在一个 test.describe() 块中,该块调用定义测试的函数。通过这种方式,test.use() 的调用只会影响单个文件中的测试。

test.list.ts
import { test } from '@playwright/test';
import featureBTests from './feature-b.spec.ts';
import featureATests from './feature-a.spec.ts';

test.describe(featureBTests);
test.describe(featureATests);

现在,通过将 workers 设置为 1 来禁用并行执行,并指定你的测试列表文件。

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  workers: 1,
  testMatch: 'test.list.ts',
});

不要直接在辅助文件中定义测试。这可能会导致意外的结果,因为你的测试现在依赖于 import/require 语句的顺序。相反,应该将测试包装在一个函数中,并通过测试列表文件显式调用,正如上面的示例所示。