Playwright自动化测试:使用storageState持久化登录状态提升效率
1. 项目概述告别重复登录让自动化测试飞起来做自动化测试的朋友尤其是用Playwright的估计都经历过这个场景每次跑测试用例第一件事就是吭哧吭哧地登录系统。输入账号、密码点登录按钮等页面跳转有时候还得输个验证码。一套流程下来十几二十秒就过去了。如果测试套件里有几十上百个需要登录态的用例那每次完整回归测试光花在登录上的时间就得好几分钟甚至十几分钟。这还没算上登录失败、网络波动、验证码识别错误这些幺蛾子带来的额外时间成本。更头疼的是在持续集成CI流水线里每次构建都重新登录不仅拖慢反馈速度还可能因为登录接口的并发限制导致测试失败。所以标题里说的“别再傻傻每次跑测试都登录了”这绝对是所有自动化测试工程师的心声。而“手把手教你用Playwright的storageState持久化登录状态”就是解决这个痛点的金钥匙。storageState是Playwright提供的一个核心功能它能将浏览器上下文Browser Context的认证状态——包括Cookies、本地存储Local Storage、会话存储Session Storage需特殊处理和IndexedDB数据——序列化保存到一个JSON文件里。下次启动测试时直接加载这个文件浏览器就能“继承”之前的登录状态直接进入已登录的页面省去了所有前端交互的登录步骤。这不仅仅是节省时间。从测试稳定性的角度看它减少了与登录UI交互的环节也就减少了因UI元素加载慢、定位失败导致测试中断的风险。从资源利用角度看特别是在需要多浏览器、多用户角色并行测试的复杂场景下预先准备好多个角色的认证状态文件可以极大地简化测试架构让测试逻辑更专注于业务验证本身。接下来我会带你从零开始彻底搞懂Playwright的storageState不止是基础的保存和加载还会深入探讨在不同测试策略下的高级应用以及那些官方文档可能没细说但实际工作中一定会遇到的坑和技巧。2. 核心原理与设计思路为什么是storageState在深入代码之前我们得先明白Playwright的隔离模型和storageState的工作原理这样才能用得明白出了问题也知道去哪排查。2.1 Playwright的隔离基石Browser ContextPlaywright测试不是在裸奔的浏览器Browser实例里直接跑的而是在一个叫浏览器上下文Browser Context的隔离环境里。你可以把它理解为一个独立的“隐身模式”会话。每个Context都有自己独立的Cookie、本地存储、缓存和证书存储。这带来了两个巨大好处测试可重复性每次测试都从一个干净、已知的状态开始不受其他测试残留数据的影响。这是自动化测试可靠性的生命线。测试独立性多个测试可以并行运行在不同的Context中互不干扰。这对于利用多核CPU加速测试执行至关重要。storageState操作的对象正是这个Browser Context。当你调用context.storageState({ path: ‘auth.json’ })时Playwright会把这个Context当前的所有“状态”快照下来保存成文件。反之在创建新的Context时通过browser.newContext({ storageState: ‘auth.json’ })传入这个文件新Context就会“克隆”文件里记录的所有状态瞬间达到相同的登录态。2.2 storageState里到底存了什么那个生成的JSON文件结构非常清晰。主要包含两大块{ “cookies”: [ { “name”: “sessionid”, “value”: “abc123...”, “domain”: “.your-app.com”, “path”: “/”, “expires”: 1743456789, “httpOnly”: true, “secure”: true, “sameSite”: “Lax” } // ... 其他cookies ], “origins”: [ { “origin”: “https://your-app.com”, “localStorage”: [ { “name”: “userToken”, “value”: “eyJhbGciOiJIUzI1NiIs...” } ] } ] }cookies: 这是维持登录状态最常见的方式。里面存了域名、路径、值、过期时间、安全标志等所有Cookie属性。像常见的sessionid、JWTtoken往往就在这里。origins: 这里存储了同源策略下的本地数据。最主要的是localStorage很多现代前端应用会把用户令牌Token或用户信息存在这里。注意默认情况下sessionStorage是不包含在内的因为它生命周期仅限于单个标签页但有些应用会误用它存登录态这就需要我们额外处理。重要提示这个auth.json文件是高度敏感的它包含了可以冒充用户身份的直接凭证Cookie或Token。绝对不要把它提交到Git仓库即使是私有仓库也有泄露风险。务必将其添加到.gitignore中例如playwright/.auth/目录。2.3 设计策略如何组织你的认证状态根据你的测试场景选择合适的管理策略这是用好storageState的关键。策略一全局共享单一状态基础模式这是最简单、最常用的模式。所有测试用例共享同一个用户比如一个测试专用账号的登录状态。何时用你的测试是只读的或者每个测试都会清理自己创建的数据不会互相影响。比如测试商品列表页、用户个人资料页的渲染。优点配置简单只需登录一次。缺点如果测试会修改服务器状态比如一个测试创建订单另一个测试验证订单列表并行运行时就会产生冲突。策略二每个工作进程一个独立状态中级模式Playwright Test运行器会启动多个工作进程Worker来并行执行测试。这个策略是为每个工作进程准备一个独立的测试账号和状态文件。何时用你的测试会修改共享的服务器状态并且需要并行运行以获得速度优势。优点实现了测试间的隔离避免了数据污染同时享受了并行化的好处。缺点需要管理多个测试账号Fixture配置稍复杂。策略三多角色状态高级模式你的系统有管理员、普通用户、VIP用户等多种角色。你需要针对不同角色测试不同的功能权限。何时用需要测试基于角色的访问控制RBAC。实现为每个角色预先准备好独立的状态文件在测试或测试组级别按需加载。理解了这些设计思路我们就能有的放矢地选择实现方案了。下面我们从最基础的全局共享模式开始手把手实现。3. 基础实战实现全局共享登录状态让我们从一个最常见的场景开始你有一个测试套件所有测试都用同一个测试账号且测试之间没有数据冲突。我们的目标是只登录一次后续所有测试直接复用这个状态。3.1 第一步创建认证Setup项目首先我们在项目中创建一个专门的“认证”测试文件。按照Playwright社区的常见约定可以放在tests目录下命名为auth.setup.ts或.spec.ts。// tests/auth.setup.ts import { test as setup, expect } from ‘playwright/test’; import path from ‘path’; // 定义认证状态文件的保存路径。放在 playwright/.auth/ 目录下是个好习惯。 const authFile path.join(__dirname, ‘../playwright/.auth/user.json’); // 使用 test 的别名 setup 来更清晰地表明这是设置步骤 setup(‘authenticate’, async ({ page }) { // 1. 导航到登录页面 // 替换成你实际应用的登录URL await page.goto(‘https://your-app.com/login’); // 2. 执行登录操作 // 这里需要替换为你应用的实际登录表单选择器和凭证 await page.getByLabel(‘Username’).fill(‘testuser’); await page.getByLabel(‘Password’).fill(‘SecurePass123!’); await page.getByRole(‘button’, { name: ‘Sign In’ }).click(); // 3. 等待登录成功 // 这是关键必须确保登录流程完全完成Cookie/Token已设置。 // 方法A等待跳转到登录后的特定URL推荐 await page.waitForURL(‘https://your-app.com/dashboard’); // 方法B等待一个登录后才会出现的元素 // await expect(page.getByText(‘Welcome, testuser’)).toBeVisible(); // 4. 保存浏览器上下文的状态到文件 await page.context().storageState({ path: authFile }); });这段代码做了几件关键事它声明了一个名为“authenticate”的测试虽然它不测试功能只做设置。它模拟了完整的用户登录交互。登录成功后通过page.context().storageState()将当前上下文的状态含Cookie等保存到指定的JSON文件。实操心得waitForURL比等待某个元素更可靠。因为登录成功后的重定向和Cookie设置是服务端控制的等待特定URL能确保整个登录流程包括可能的多次重定向彻底完成状态已稳定。如果应用登录后没有明确的URL变化再退而求其次用元素等待。3.2 第二步配置Playwright以复用状态光保存了状态还不够我们需要告诉Playwright在运行真正的测试时去加载这个状态文件。这需要在playwright.config.ts中配置。// playwright.config.ts import { defineConfig, devices } from ‘playwright/test’; export default defineConfig({ // ... 其他配置如全局超时、报告器等 projects: [ // 1. 定义“setup”项目专门运行我们的认证脚本 { name: ‘setup’, testMatch: /.*\.setup\.ts/, // 匹配所有以 .setup.ts 结尾的文件 }, // 2. 定义你的主要测试项目例如Chrome测试 { name: ‘chromium’, use: { ...devices[‘Desktop Chrome’], // 关键配置指定要使用的存储状态文件 storageState: ‘playwright/.auth/user.json’, }, // 关键配置声明依赖。这确保了‘setup’项目会在‘chromium’项目之前运行 dependencies: [‘setup’], }, // 你可以为其他浏览器定义类似的项目 { name: ‘firefox’, use: { ...devices[‘Desktop Firefox’], storageState: ‘playwright/.auth/user.json’, }, dependencies: [‘setup’], }, // 3. 可以定义一个不依赖认证的“未登录”测试项目 { name: ‘unauthenticated’, testMatch: ‘**/*.unauthed.spec.ts’, use: { ...devices[‘Desktop Chrome’] }, // 不设置 storageState或者显式设置为空状态 // use: { ...devices[‘Desktop Chrome’], storageState: undefined }, }, ], });这个配置的精髓在于dependencies字段。它建立了项目间的执行顺序关系。当你在命令行运行npx playwright test时Playwright会先执行完所有setup项目里的测试即我们的登录脚本生成user.json文件然后再启动chromium和firefox项目。这些项目在创建浏览器上下文时会自动加载storageState指定的文件从而使页面一开始就处于登录状态。3.3 第三步编写已认证的测试用例配置好后你的业务测试用例就变得非常简洁了。// tests/dashboard.spec.ts import { test, expect } from ‘playwright/test’; // 不需要任何登录代码 test(‘访问仪表盘并查看欢迎信息’, async ({ page }) { // 页面已经处于登录状态直接导航到目标页 await page.goto(‘https://your-app.com/dashboard’); // 直接进行业务断言 await expect(page.locator(‘h1’)).toHaveText(‘Dashboard’); await expect(page.getByText(‘Welcome, testuser’)).toBeVisible(); }); test(‘测试用户设置功能’, async ({ page }) { await page.goto(‘https://your-app.com/settings’); // ... 测试设置逻辑 });你会发现测试用例里完全没有了登录相关的代码逻辑更清晰执行速度也快了很多。第一次运行会执行登录并保存状态后续运行只要状态未过期就会直接复用。3.4 第四步管理状态文件与安全创建目录并忽略文件在项目根目录执行以下命令或手动操作。mkdir -p playwright/.auth echo -e “\nplaywright/.auth” .gitignore这确保了敏感的状态文件不会被意外提交。状态过期与更新Cookie和Token都有有效期。如果状态文件过期了测试就会失败。你需要一个更新机制。手动更新最简单的是定期比如每天或每周手动删除playwright/.auth/user.json文件然后重新运行一次测试它会自动重新登录生成新文件。自动化更新可以在CI流水线中在每天第一次运行或检测到认证失败时触发setup项目的单独运行。或者写一个简单的Node.js脚本定期清理旧文件。注意事项如果你的应用登出机制会清除客户端存储那么在你的测试中要避免触发登出操作除非你明确想测试登出流程。测试登出后记得重新生成状态文件。4. 中级进阶为并行测试准备独立状态当你的测试规模变大需要利用npx playwright test --workers4这样的并行执行来提速时基础模式就可能出问题。如果所有Worker都读写同一个user.json文件并操作同一个测试账号的数据就会导致数据竞争和测试失败。解决方案是为每个并行工作进程Worker准备独立的测试账号和状态文件。4.1 核心思路Worker级别的FixturePlaywright Test支持定义不同作用域Scope的Fixture。worker作用域的Fixture在每个Worker进程内只会初始化一次并在该Worker执行的所有测试中共享。我们可以利用这个特性在每个Worker首次启动时为其分配一个唯一账号并执行登录然后将状态文件路径保存下来供后续测试使用。4.2 实现Worker级认证Fixture我们不直接修改playwright.config.ts中的storageState而是通过自定义Fixture来覆盖它。// playwright/fixtures.ts import { test as baseTest, expect } from ‘playwright/test’; import fs from ‘fs’; import path from ‘path’; // 导出所有原生的test方法和断言方便后续导入 export * from ‘playwright/test’; // 扩展基础的 test fixture export const test baseTest.extend{}, { workerStorageState: string }({ // 1. 覆盖默认的 storageState fixture使其使用我们worker级别的状态文件 storageState: ({ workerStorageState }, use) { // workerStorageState 是下面定义的worker作用域fixture的返回值文件路径 use(workerStorageState); }, // 2. 定义worker作用域的 fixture用于执行一次性的认证 workerStorageState: [ async ({ browser }, use, testInfo) { // 每个Worker有一个唯一的并行索引parallelIndex const workerId testInfo.parallelIndex; // 为每个Worker生成唯一的状态文件名放在测试输出目录下运行后会自动清理 const stateFileName path.join(testInfo.project.outputDir, .auth/worker-${workerId}.json); // 检查是否已有可用的状态文件例如Worker重用或上次运行残留 if (fs.existsSync(stateFileName)) { // 直接使用现有的状态文件 await use(stateFileName); return; } // 重要创建一个全新的、无状态的浏览器上下文来进行登录 const freshContext await browser.newContext({ storageState: undefined }); const page await freshContext.newPage(); // **关键为当前Worker获取一个唯一的测试账号** // 这里需要你实现自己的账号获取逻辑。例如 // 方式A从预创建的账号池中按workerId分配 // 方式B动态注册一个新账号如果系统支持 const testAccount await acquireUniqueTestAccount(workerId); // 执行登录流程替换为你的实际登录步骤 await page.goto(‘https://your-app.com/login’); await page.getByLabel(‘Username’).fill(testAccount.username); await page.getByLabel(‘Password’).fill(testAccount.password); await page.getByRole(‘button’, { name: ‘Sign In’ }).click(); await page.waitForURL(‘https://your-app.com/dashboard’); // 可选的等待确保登录完成 await expect(page.getByText(‘Welcome,’)).toBeVisible(); // 保存这个Worker专属的认证状态 await freshContext.storageState({ path: stateFileName }); await freshContext.close(); // 将状态文件路径传递给 storageState fixture await use(stateFileName); }, { scope: ‘worker’ } // 指定此fixture的作用域为‘worker’ ], }); // 模拟函数你需要根据实际情况实现账号获取逻辑 async function acquireUniqueTestAccount(workerId: number): Promise{ username: string; password: string } { // 示例1使用预置账号列表 const accountPool [ { username: ‘test_user_1’, password: ‘pass1’ }, { username: ‘test_user_2’, password: ‘pass2’ }, // ... 准备足够多的账号至少 你的最大worker数 ]; return accountPool[workerId % accountPool.length]; // 示例2动态生成唯一用户名需后端支持注册或特定账号命名规则 // const username load_test_${Date.now()}_${workerId}; // const password ‘DefaultPassword123’; // 调用API注册这个账号确保幂等性 // return { username, password }; }4.3 在测试中使用自定义Fixture现在在你的测试文件中不再从playwright/test导入而是从我们自定义的fixtures.ts导入test。// tests/parallel-test.spec.ts // 重要从我们自定义的fixtures文件导入 import { test, expect } from ‘../playwright/fixtures’; test(‘Worker A 的测试创建订单’, async ({ page }) { await page.goto(‘https://your-app.com/orders/new’); // 这个page使用的是当前Worker专属账号的状态 await page.selectOption(‘#product’, ‘Laptop’); await page.click(‘button[type“submit”]’); await expect(page.locator(‘.alert-success’)).toBeVisible(); }); test(‘Worker B 的测试查看我的订单’, async ({ page }) { await page.goto(‘https://your-app.com/orders’); // 这个page使用的是另一个Worker专属账号的状态订单列表是独立的不会冲突 await expect(page.locator(‘.order-item’)).toHaveCount(0); // 新账号初始无订单 });4.4 配置与运行你的playwright.config.ts现在可以简化不需要在项目级别指定storageState和dependencies因为Fixture已经接管了状态管理。// playwright.config.ts import { defineConfig, devices } from ‘playwright/test’; export default defineConfig({ // 设置并行worker数例如4个 workers: process.env.CI ? 4 : 2, projects: [ { name: ‘chromium’, use: { ...devices[‘Desktop Chrome’] }, // 注意这里不再配置 storageState 和 dependencies }, ], });运行测试时Playwright会启动多个Worker。每个Worker第一次执行测试前都会运行一次workerStorageStatefixture获取或创建自己的唯一账号并登录生成独立的状态文件。该Worker后续的所有测试都复用这个状态从而实现了并行测试间的完全隔离。避坑技巧账号池管理是个学问。确保账号数量大于等于最大并行Worker数。如果使用动态注册要考虑账号清理问题避免测试数据无限增长。可以在Fixture的teardown阶段本例未展示可通过onTestFinished等方式或一个全局的teardown钩子中加入清理测试数据的逻辑。5. 高级场景与疑难杂症处理掌握了基础和中级用法你已经能解决90%的问题了。但真实世界总是更复杂一些下面我们看看那些“高级”场景和常见坑点。5.1 场景一通过API登录更快更稳定如果你的应用提供了登录API直接用API获取认证令牌如JWT然后手动构造storageState比通过UI操作更快、更稳定不依赖前端渲染。// tests/api-auth.setup.ts import { test as setup, request } from ‘playwright/test’; import fs from ‘fs’; const authFile ‘playwright/.auth/api-user.json’; setup(‘authenticate via API’, async ({ request }) { // 1. 调用登录API const response await request.post(‘https://your-app.com/api/v1/login’, { data: { username: ‘api_test_user’, password: ‘password123’ } }); if (!response.ok()) { throw new Error(API登录失败: ${response.status()} ${response.statusText()}); } const responseBody await response.json(); const authToken responseBody.token; // 假设返回JSON包含 token 字段 // 2. 手动构建 storageState 对象 const storageState { cookies: [ { name: ‘auth_token’, // 或者你的应用使用的Cookie名 value: authToken, domain: ‘.your-app.com’, path: ‘/’, expires: Math.floor(Date.now() / 1000) 86400, // 设置过期时间例如1天后 httpOnly: true, secure: true, sameSite: ‘Lax’ as const, } ], origins: [ { origin: ‘https://your-app.com’, localStorage: [ { name: ‘jwtToken’, // 如果应用将token存在localStorage value: authToken } ] } ] }; // 3. 将状态写入文件 fs.writeFileSync(authFile, JSON.stringify(storageState), ‘utf-8’); });然后在配置中让其他项目使用这个api-user.json文件即可。这种方式完全跳过了浏览器UI速度极快特别适合在CI/CD流水线中运行。5.2 场景二处理Session Storage如前所述storageState默认不包含sessionStorage。但有些老式或设计不当的应用会把登录信息放在sessionStorage里。这时你需要手动保存和注入。保存阶段在登录成功后// 在 auth.setup.ts 的登录步骤后 const sessionStorageData await page.evaluate(() { // 将当前页面的sessionStorage序列化为JSON字符串 return JSON.stringify(sessionStorage); }); // 将数据保存到单独的文件 fs.writeFileSync(‘playwright/.auth/session.json’, sessionStorageData, ‘utf-8’);加载阶段在配置或Fixture中创建Context时// 在 playwright.config.ts 的 use 部分或者自定义fixture的 newContext 时 import fs from ‘fs’; const sessionStorageData JSON.parse(fs.readFileSync(‘playwright/.auth/session.json’, ‘utf-8’)); const context await browser.newContext({ storageState: ‘playwright/.auth/user.json’, // 正常的cookie/localStorage状态 }); // 关键通过 addInitScript 在页面加载前注入sessionStorage await context.addInitScript((storage) { // 确保只在目标域名下注入 if (window.location.hostname ‘your-app.com’) { for (const [key, value] of Object.entries(storage)) { window.sessionStorage.setItem(key, value as string); } } }, sessionStorageData); // 将数据作为参数传入脚本5.3 场景三测试多角色管理员 vs 普通用户你需要为管理员和普通用户分别准备状态文件。// tests/multi-role-auth.setup.ts import { test as setup, expect } from ‘playwright/test’; const adminAuthFile ‘playwright/.auth/admin.json’; const userAuthFile ‘playwright/.auth/user.json’; setup(‘authenticate as admin’, async ({ page }) { await performLogin(page, ‘admin_user’, ‘admin_pass’); await page.context().storageState({ path: adminAuthFile }); }); setup(‘authenticate as regular user’, async ({ page }) { await performLogin(page, ‘test_user’, ‘user_pass’); await page.context().storageState({ path: userAuthFile }); }); async function performLogin(page, username, password) { await page.goto(‘https://your-app.com/login’); // ... 登录交互 await page.waitForURL(‘https://your-app.com/dashboard’); }在测试中你可以通过test.use()在文件或描述块级别指定使用的状态。// tests/admin-section.spec.ts import { test } from ‘playwright/test’; // 这个文件的所有测试都使用管理员状态 test.use({ storageState: ‘playwright/.auth/admin.json’ }); test(‘管理员可以访问用户管理页面’, async ({ page }) { await page.goto(‘https://your-app.com/admin/users’); // ... }); // tests/user-section.spec.ts import { test } from ‘playwright/test’; test.describe(‘普通用户功能’, () { // 这个describe块内的测试使用普通用户状态 test.use({ storageState: ‘playwright/.auth/user.json’ }); test(‘用户可以修改个人资料’, async ({ page }) { // ... }); });5.4 常见问题与排查技巧问题1加载了storageState但页面显示未登录。检查点1状态文件是否过期打开auth.json检查里面Cookie的expires时间戳Unix时间戳秒。如果已经过期需要重新生成。检查点2应用认证方式是否改变检查应用是否从Cookie认证换成了Token认证存在localStorage或请求头。你需要更新保存状态的方式可能需要在origins.localStorage里添加数据或者在测试中通过page.setExtraHTTPHeaders设置Authorization头。检查点3域名或路径是否匹配确保状态文件中Cookie的domain和path与你访问的页面URL匹配。domain前的点如.your-app.com表示包含子域名。检查点4是否漏了sessionStorage按5.2章节的方法检查并注入。问题2并行测试时出现数据串扰。排查你很可能还在使用全局共享的单状态文件。请切换到4.2章节的“每个Worker独立状态”模式。确认检查你的测试账号是否有唯一性。确保每个Worker操作的是不同的数据实体如不同的订单ID、不同的用户名。问题3在CI环境中认证状态经常失败。可能原因1CI环境与本地环境差异。CI服务器的时区、网络环境可能导致Cookie过期策略不同。尝试在CI脚本中设置明确的时区TZUTC。可能原因2CI是干净的工作空间。确保你的CI流水线步骤中在运行测试前要么保留了上一次生成的状态文件缓存playwright/.auth目录要么有一个专门的步骤来运行auth.setup项目。解决方案在CI配置中将认证步骤作为独立job并将其产出状态文件作为artifact缓存供后续的测试job使用。问题4如何强制某个测试在未登录状态下运行有时你需要测试登录页面本身或者测试未登录时的重定向逻辑。import { test } from ‘playwright/test’; test(‘测试未登录访问受限页面’, async ({ page }) { // 为这个测试单独创建一个全新的、无状态的上下文 const context await browser.newContext({ storageState: undefined }); const cleanPage await context.newPage(); await cleanPage.goto(‘https://your-app.com/profile’); // 期望被重定向到登录页 await expect(cleanPage).toHaveURL(/login/); await context.close(); }); // 或者在文件级别重置状态 test.describe(‘未登录测试套件’, () { test.use({ storageState: { cookies: [], origins: [] } }); // 传入空状态 test(‘...’, async ({ page }) { // page 是未登录状态 }); });问题5状态文件很大或包含二进制数据storageState保存的origins里可能包含IndexedDB的数据如果应用在本地存了大量数据如图片blob文件会很大。这可能导致加载变慢。可以考虑在登录后、保存状态前导航到一个“轻量级”的页面如首页避免将不必要的数据快照进去。或者研究是否可以在应用层面为测试账号减少本地数据存储。6. 工程化实践与性能优化将storageState用好在大型项目中还需要一些工程化的考量。6.1 状态文件的版本控制与清理虽然状态文件本身不纳入Git但管理它们仍然重要。命名规范化除了user.json可以考虑按角色、环境命名如admin-staging.json,user-prod.json。定期清理脚本写一个简单的Node脚本或在package.json中添加命令定期清理旧的或无效的状态文件。// package.json “scripts”: { “test:clean-auth”: “rimraf playwright/.auth/*.json”, “test:auth”: “playwright test --projectsetup”, “test:run”: “playwright test” }可以组合使用npm run test:clean-auth npm run test:auth npm run test:run6.2 与Page Object Model (POM) 结合在大型测试套件中通常会用POM模式。我们可以创建返回已认证页面的Fixture。// playwright/pom-fixtures.ts import { test as baseTest, Page, Browser } from ‘playwright/test’; // 声明自定义Fixture的类型 export type MyFixtures { adminPage: Page; userPage: Page; }; export const test baseTest.extendMyFixtures({ adminPage: async ({ browser }, use) { // 为管理员创建一个带状态的上下文和页面 const adminContext await browser.newContext({ storageState: ‘playwright/.auth/admin.json’, }); const adminPage await adminContext.newPage(); await use(adminPage); // 测试结束后清理上下文 await adminContext.close(); }, userPage: async ({ browser }, use) { const userContext await browser.newContext({ storageState: ‘playwright/.auth/user.json’, }); const userPage await userContext.newPage(); await use(userPage); await userContext.close(); }, }); // tests/using-pom-fixtures.spec.ts import { test, expect } from ‘../playwright/pom-fixtures’; test(‘管理员和用户交互测试’, async ({ adminPage, userPage }) { await adminPage.goto(‘https://your-app.com/admin’); await userPage.goto(‘https://your-app.com/profile’); // 现在你可以同时操作两个已登录不同角色的页面进行测试 await expect(adminPage.locator(‘#admin-panel’)).toBeVisible(); await expect(userPage.locator(‘#user-profile’)).toBeVisible(); });6.3 性能考量状态文件加载开销加载一个大的状态文件会有微小开销。对于超大型项目如果测试速度是毫秒必争的可以衡量一下。通常这个开销远小于登录操作。并行Worker数量在“每个Worker一个状态”模式下Worker数越多需要的独立测试账号也越多。确保你的测试账号池足够大或者账号生成/清理逻辑足够高效。网络依赖认证本身依赖于登录接口或UI。确保你的测试环境尤其是CI环境网络稳定登录服务可用。可以考虑在CI中增加对登录服务的健康检查作为前置步骤。6.4 一个完整的、可复用的配置示例最后分享一个我项目中常用的、结合了多种策略的playwright.config.ts配置import { defineConfig, devices } from ‘playwright/test’; export default defineConfig({ timeout: 30000, fullyParallel: true, // 完全并行 forbidOnly: !!process.env.CI, // CI环境下禁止使用 test.only retries: process.env.CI ? 2 : 0, // CI下重试2次 workers: process.env.CI ? 4 : undefined, // CI下4个worker本地根据CPU核心数自动决定 reporter: ‘html’, use: { baseURL: process.env.BASE_URL || ‘https://staging.your-app.com’, // 使用环境变量 trace: ‘on-first-retry’, screenshot: ‘only-on-failure’, }, projects: [ // 项目1: 认证设置 (只在需要时运行) { name: ‘setup-auth’, testMatch: ‘**/*.setup.ts’, teardown: ‘cleanup-temp’, // 可选关联一个清理项目 }, // 项目2: 清理临时数据 (可选) { name: ‘cleanup-temp’, testMatch: ‘**/*.teardown.ts’, }, // 项目3: 桌面端Chrome测试 (依赖认证) { name: ‘chromium’, use: { ...devices[‘Desktop Chrome’], // 状态文件路径也支持使用环境变量 storageState: process.env.AUTH_FILE || ‘playwright/.auth/default-user.json’, }, dependencies: [‘setup-auth’], // 显式依赖确保先认证 }, // 项目4: 移动端Safari测试 (共享同一认证状态) { name: ‘mobile-safari’, use: { ...devices[‘iPhone 14’], storageState: process.env.AUTH_FILE || ‘playwright/.auth/default-user.json’, }, dependencies: [‘setup-auth’], }, // 项目5: 未登录态测试 (不依赖认证) { name: ‘unauthenticated’, testMatch: ‘**/*.unauthed.spec.ts’, use: { ...devices[‘Desktop Chrome’] }, }, ], });这套配置提供了很大的灵活性可以通过环境变量BASE_URL切换测试环境通过AUTH_FILE切换不同的用户角色状态并且清晰地分离了设置、清理和不同套件的测试。归根结底storageState不是一个炫技的功能而是一个实实在在提升测试效率和稳定性的工程实践。花一点时间把它集成到你的Playwright测试流程中你会发现每天都能省下不少等待登录的时间测试脚本也更健壮、更专业。
