认证
简介
Playwright在称为browser contexts的隔离环境中执行测试。这种隔离模式提高了可重现性并防止级联测试失败。测试可以加载现有的认证状态,这消除了在每个测试中进行认证的需要,并加快了测试执行速度。
核心概念
无论您选择哪种身份验证策略,都可能需要将经过身份验证的浏览器状态存储在文件系统中。
我们建议创建playwright/.auth
目录并将其添加到您的.gitignore
中。您的身份验证流程将生成经过验证的浏览器状态,并将其保存到此playwright/.auth
目录中的文件中。之后,测试将重用此状态并直接以已验证状态启动。
- Bash
- PowerShell
- 批量处理
mkdir -p playwright/.auth
echo $'\nplaywright/.auth' >> .gitignore
New-Item -ItemType Directory -Force -Path playwright\.auth
Add-Content -path .gitignore "`r`nplaywright/.auth"
md playwright\.auth
echo. >> .gitignore
echo "playwright/.auth" >> .gitignore
基础:在所有测试中共享账户
这是针对无服务器端状态测试的推荐方法。在设置项目中进行一次身份验证,保存认证状态,然后重用它来引导每个已经通过认证的测试。
使用时机
- 当您可以想象所有测试使用同一账户同时运行,而不会相互影响时。
何时不应使用
- 您的测试会修改服务器端状态。例如,一个测试检查设置页面的渲染,而另一个测试正在更改设置,并且您并行运行测试。在这种情况下,测试必须使用不同的账户。
- 您的身份验证是与浏览器相关的。
详情
创建tests/auth.setup.ts
文件,该文件将为所有其他测试准备已认证的浏览器状态。
import { test as setup, expect } from '@playwright/test';
import path from 'path';
const authFile = path.join(__dirname, '../playwright/.auth/user.json');
setup('authenticate', async ({ page }) => {
// Perform authentication steps. Replace these actions with your own.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill('username');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
// End of authentication steps.
await page.context().storageState({ path: authFile });
});
在配置中创建一个新的setup
项目,并将其声明为所有测试项目的dependency依赖项。该项目将在所有测试之前运行并进行身份验证。所有测试项目都应使用经过身份验证的状态作为storageState
。
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Setup project
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
// Use prepared auth state.
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
// Use prepared auth state.
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
测试开始时已经处于认证状态,因为我们在配置中指定了storageState
。
import { test } from '@playwright/test';
test('test', async ({ page }) => {
// page is authenticated
});
请注意,当存储的状态过期时,您需要将其删除。如果您不需要在测试运行之间保留状态,请将浏览器状态写入testProject.outputDir下,该目录会在每次测试运行前自动清理。
在UI模式下进行身份验证
UI模式默认不会运行setup
项目以提高测试速度。我们建议在现有认证过期时,定期手动运行auth.setup.ts
来进行认证。
首先在筛选器中启用setup
项目,然后点击auth.setup.ts
文件旁边的三角形按钮,接着再次在筛选器中禁用setup
项目。
适度:每个并行工作线程一个账户
这是针对修改服务器端状态的测试推荐的方法。在Playwright中,工作进程是并行运行的。这种方法下,每个并行工作进程只需认证一次。该工作进程运行的所有测试都会复用相同的认证状态。我们需要多个测试账户,每个并行工作进程对应一个。
使用时机
- 您的测试修改了共享的服务器端状态。例如,一个测试检查设置页面的渲染,而另一个测试正在更改设置。
何时不应使用
- 您的测试不会修改任何共享的服务器端状态。在这种情况下,所有测试都可以使用单个共享账户。
详情
我们将为每个worker process进行一次身份验证,每个进程使用唯一的账户。
创建playwright/fixtures.ts
文件,该文件将重写storageState
夹具以便每个工作线程只需认证一次。使用testInfo.parallelIndex来区分不同工作线程。
import { test as baseTest, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';
export * from '@playwright/test';
export const test = baseTest.extend<{}, { workerStorageState: string }>({
// Use the same storage state for all tests in this worker.
storageState: ({ workerStorageState }, use) => use(workerStorageState),
// Authenticate once per worker with a worker-scoped fixture.
workerStorageState: [async ({ browser }, use) => {
// Use parallelIndex as a unique identifier for each worker.
const id = test.info().parallelIndex;
const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`);
if (fs.existsSync(fileName)) {
// Reuse existing authentication state if any.
await use(fileName);
return;
}
// Important: make sure we authenticate in a clean environment by unsetting storage state.
const page = await browser.newPage({ storageState: undefined });
// Acquire a unique account, for example create a new one.
// Alternatively, you can have a list of precreated accounts for testing.
// Make sure that accounts are unique, so that multiple team members
// can run tests at the same time without interference.
const account = await acquireAccount(id);
// Perform authentication steps. Replace these actions with your own.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill(account.username);
await page.getByLabel('Password').fill(account.password);
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
// End of authentication steps.
await page.context().storageState({ path: fileName });
await page.close();
await use(fileName);
}, { scope: 'worker' }],
});
现在,每个测试文件应该从我们的fixtures文件中导入test
,而不是@playwright/test
。配置中不需要做任何更改。
// Important: import our fixtures.
import { test, expect } from '../playwright/fixtures';
test('test', async ({ page }) => {
// page is authenticated
});
高级场景
通过API请求进行身份验证
使用时机
- 您的Web应用程序支持通过API进行身份验证,这比与应用UI交互更简单/更快。
详情
我们将使用APIRequestContext发送API请求,然后像往常一样保存认证状态。
在设置项目中:
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ request }) => {
// Send authentication request. Replace with your own.
await request.post('https://github.com/login', {
form: {
'user': 'user',
'password': 'password'
}
});
await request.storageState({ path: authFile });
});
或者,在worker fixture中:
import { test as baseTest, request } from '@playwright/test';
import fs from 'fs';
import path from 'path';
export * from '@playwright/test';
export const test = baseTest.extend<{}, { workerStorageState: string }>({
// Use the same storage state for all tests in this worker.
storageState: ({ workerStorageState }, use) => use(workerStorageState),
// Authenticate once per worker with a worker-scoped fixture.
workerStorageState: [async ({}, use) => {
// Use parallelIndex as a unique identifier for each worker.
const id = test.info().parallelIndex;
const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`);
if (fs.existsSync(fileName)) {
// Reuse existing authentication state if any.
await use(fileName);
return;
}
// Important: make sure we authenticate in a clean environment by unsetting storage state.
const context = await request.newContext({ storageState: undefined });
// Acquire a unique account, for example create a new one.
// Alternatively, you can have a list of precreated accounts for testing.
// Make sure that accounts are unique, so that multiple team members
// can run tests at the same time without interference.
const account = await acquireAccount(id);
// Send authentication request. Replace with your own.
await context.post('https://github.com/login', {
form: {
'user': 'user',
'password': 'password'
}
});
await context.storageState({ path: fileName });
await context.dispose();
await use(fileName);
}, { scope: 'worker' }],
});
多重登录角色
使用时机
- 在您的端到端测试中,您拥有多个角色,但您可以在所有测试中重复使用账户。
详情
我们将在设置项目中多次进行身份验证。
import { test as setup, expect } from '@playwright/test';
const adminFile = 'playwright/.auth/admin.json';
setup('authenticate as admin', async ({ page }) => {
// Perform authentication steps. Replace these actions with your own.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill('admin');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
// End of authentication steps.
await page.context().storageState({ path: adminFile });
});
const userFile = 'playwright/.auth/user.json';
setup('authenticate as user', async ({ page }) => {
// Perform authentication steps. Replace these actions with your own.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill('user');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
// End of authentication steps.
await page.context().storageState({ path: userFile });
});
之后,为每个测试文件或测试组指定storageState
,而不是在配置中设置它。
import { test } from '@playwright/test';
test.use({ storageState: 'playwright/.auth/admin.json' });
test('admin test', async ({ page }) => {
// page is authenticated as admin
});
test.describe(() => {
test.use({ storageState: 'playwright/.auth/user.json' });
test('user test', async ({ page }) => {
// page is authenticated as a user
});
});
另请参阅关于UI模式下的身份验证。
测试多个角色组合
使用时机
- 您需要测试多个经过认证的角色如何在单个测试中交互。
详情
在同一个测试中使用多个具有不同存储状态的BrowserContext和Page。
import { test } from '@playwright/test';
test('admin and user', async ({ browser }) => {
// adminContext and all pages inside, including adminPage, are signed in as "admin".
const adminContext = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
const adminPage = await adminContext.newPage();
// userContext and all pages inside, including userPage, are signed in as "user".
const userContext = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
const userPage = await userContext.newPage();
// ... interact with both adminPage and userPage ...
await adminContext.close();
await userContext.close();
});
使用POM fixtures测试多重角色
使用时机
- 您需要测试多个经过身份验证的角色如何在单个测试中交互。
详情
您可以引入fixture,这些fixture将提供以每个角色身份认证的页面。
以下是一个为两个页面对象模型(管理员POM和用户POM)创建夹具的示例。它假设在全局设置中已创建了adminStorageState.json
和userStorageState.json
文件。
import { test as base, type Page, type Locator } from '@playwright/test';
// Page Object Model for the "admin" page.
// Here you can add locators and helper methods specific to the admin page.
class AdminPage {
// Page signed in as "admin".
page: Page;
// Example locator pointing to "Welcome, Admin" greeting.
greeting: Locator;
constructor(page: Page) {
this.page = page;
this.greeting = page.locator('#greeting');
}
}
// Page Object Model for the "user" page.
// Here you can add locators and helper methods specific to the user page.
class UserPage {
// Page signed in as "user".
page: Page;
// Example locator pointing to "Welcome, User" greeting.
greeting: Locator;
constructor(page: Page) {
this.page = page;
this.greeting = page.locator('#greeting');
}
}
// Declare the types of your fixtures.
type MyFixtures = {
adminPage: AdminPage;
userPage: UserPage;
};
export * from '@playwright/test';
export const test = base.extend<MyFixtures>({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
const adminPage = new AdminPage(await context.newPage());
await use(adminPage);
await context.close();
},
userPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
const userPage = new UserPage(await context.newPage());
await use(userPage);
await context.close();
},
});
// Import test with our new fixtures.
import { test, expect } from '../playwright/fixtures';
// Use adminPage and userPage fixtures in the test.
test('admin and user', async ({ adminPage, userPage }) => {
// ... interact with both adminPage and userPage ...
await expect(adminPage.greeting).toHaveText('Welcome, Admin');
await expect(userPage.greeting).toHaveText('Welcome, User');
});
会话存储
复用认证状态涵盖基于cookies、local storage和IndexedDB的认证。极少数情况下,session storage会用于存储与登录状态相关的信息。会话存储是特定于某个域的,且不会在页面加载之间持久化。Playwright不提供持久化会话存储的API,但可以使用以下代码片段来保存/加载会话存储。
// Get session storage and store as env variable
const sessionStorage = await page.evaluate(() => JSON.stringify(sessionStorage));
fs.writeFileSync('playwright/.auth/session.json', sessionStorage, 'utf-8');
// Set session storage in a new context
const sessionStorage = JSON.parse(fs.readFileSync('playwright/.auth/session.json', 'utf-8'));
await context.addInitScript(storage => {
if (window.location.hostname === 'example.com') {
for (const [key, value] of Object.entries(storage))
window.sessionStorage.setItem(key, value);
}
}, sessionStorage);
在某些测试中避免身份验证
你可以在测试文件中重置存储状态,以避免为整个项目设置的认证。
import { test } from '@playwright/test';
// Reset storage state for this file to avoid being authenticated
test.use({ storageState: { cookies: [], origins: [] } });
test('not signed in test', async ({ page }) => {
// ...
});