如何在Playwright中实现带可复用认证的API请求基础服务类
基于Playwright实现BaseService类的API请求方案设计
需求
- 在多个服务间复用来自
storageState的认证令牌 - 设置独立于UI的
baseUrl的共享基础API地址 - 遵循已有的UI交互页面对象模型(POM)结构
现有配置
全局认证
已通过storageState实现全局令牌管理,UI测试中运行正常:
// auth setup import { LoginPage } from '@e2e/pages/login/login.page'; import { expect, test as setup } from '@playwright/test'; const authFile = 'playwright/.auth/user.json'; setup('authenticate', async ({ page }) => { let loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login(process.env.USERNAME, process.env.PASSWORD); await expect(page.locator('.user-container')).toBeVisible(); await page.context().storageState({ path: authFile }); });
Playwright配置(playwright.config.ts)
import { defineConfig, devices } from '@playwright/test'; import { environment as environmentLocal } from './src/environments/environment'; import { environment as environmentStage } from './src/environments/environment.stage'; require('dotenv').config(); const environment = process.env.APP_ENV === 'stage' ? environmentStage : environmentLocal; export default defineConfig({ testDir: './e2e/specs', timeout: 60_000, expect: { timeout: 60_000 }, fullyParallel: false, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { baseURL: environment.appUrl, trace: 'on-first-retry', ignoreHTTPSErrors: true }, projects: [ { name: 'setup', testMatch: /.*\.setup\.ts/ }, { name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json', }, dependencies: ['setup'], }, ], });
现有POM结构
// base.page.ts import { Locator, Page } from '@playwright/test'; export abstract class BasePage { protected readonly page: Page; protected abstract relativeURL: string; public readonly toastMessageContainer: Locator; constructor(page: Page) { this.page = page; this.toastMessageContainer = this.page.locator('nb-toastr-container'); } public async goto(params?: URLSearchParams): Promise<void> { const url = params ? `${this.relativeURL}?${decodeURIComponent(params.toString())}` : this.relativeURL; await this.page.goto(url); } } // clients.page.ts export default class ClientsPage extends BasePage { protected relativeURL = 'pages/clients'; constructor(page: Page) { super(page); // Initialize locators or components here } async addNewClient(clientInfo: ClientParams) { // Logic for adding a client } } // Test usage test.describe('Clients Page', () => { let clientsPage: ClientsPage; test.beforeAll(async ({ browser }) => { const page = await browser.newPage(); clientsPage = new ClientsPage(page); }); test('should create a new client', async () => { await clientsPage.goto(); await clientsPage.addNewClient({ name: 'Test Client', businessId: '12345678', address: 'Test City', description: 'Test Description', category: 'Hotels', subCategory: 'Seasonal', }); await expect(clientsPage.toastMessageContainer).toContainText('Client successfully saved.'); }); });
挑战与问题
- 令牌管理:如何从
storageState提取access_token并自动注入到API请求中? - 服务初始化:如何像初始化页面类一样,无需手动传递令牌或Cookie即可初始化
BaseService及其子类? - 基础地址设置:如何管理与UI
baseURL独立的API请求baseApiURL?
已尝试方案
已实现直接在测试中创建带认证的请求上下文,但希望对齐POM框架:
test.beforeAll(async ({ browser, playwright }) => { page = await browser.newPage(); const storageStatePath = path.resolve(__dirname, '../../playwright/.auth/user.json'); const storageState = JSON.parse(fs.readFileSync(storageStatePath, 'utf-8')); const accessToken = storageState.origins[0].localStorage.find( (item: any) => item.name === 'oidc.user:someUrl' )?.value; const parsedAccessToken = accessToken ? JSON.parse(accessToken).access_token : null; apiContext = await playwright.request.newContext({ baseURL: baseApiURL, extraHTTPHeaders: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${parsedAccessToken}` }, storageState: storageStatePath }); }); const response = await apiContext.get(`/clients`);
解决方案
1. 配置独立的API基础地址
修改playwright.config.ts,从环境文件中读取API地址并加入全局配置:
// playwright.config.ts export default defineConfig({ // ...其他配置 use: { baseURL: environment.appUrl, baseApiURL: environment.apiUrl, // 添加独立API地址 trace: 'on-first-retry', ignoreHTTPSErrors: true }, // ...其他配置 });
2. 设计BaseService类
对齐BasePage的POM结构,封装通用API请求逻辑:
// base.service.ts import { APIRequestContext } from '@playwright/test'; export abstract class BaseService { protected readonly request: APIRequestContext; protected readonly baseApiURL: string; protected abstract endpoint: string; // 对应API资源的基础端点 constructor(request: APIRequestContext, baseApiURL: string) { this.request = request; this.baseApiURL = baseApiURL; } // 通用GET请求封装 protected async get(path = '', options?: any) { return this.request.get(`${this.baseApiURL}/${this.endpoint}/${path}`, options); } // 通用POST请求封装 protected async post(path = '', data: any, options?: any) { return this.request.post(`${this.baseApiURL}/${this.endpoint}/${path}`, { data, ...options }); } // 可扩展PUT/DELETE等其他请求方法 }
3. 实现具体服务类(如ClientService)
继承BaseService,实现对应API资源的具体方法:
// client.service.ts import { APIRequestContext } from '@playwright/test'; import { BaseService } from './base.service'; export default class ClientService extends BaseService { protected endpoint = 'clients'; // API资源端点 constructor(request: APIRequestContext, baseApiURL: string) { super(request, baseApiURL); } // 添加客户端的API方法 async addClient(clientInfo: any) { return this.post('', clientInfo); } // 获取客户端列表 async getClients() { return this.get(''); } }
4. 测试中初始化服务类
复用全局storageState创建认证请求上下文,初始化方式与页面类一致:
// 测试用例示例 import { test, expect } from '@playwright/test'; import ClientService from '@e2e/services/client.service'; import fs from 'fs'; test.describe('Clients API Tests', () => { let clientService: ClientService; test.beforeAll(async ({ playwright, use }) => { // 从全局配置获取API基础地址 const baseApiURL = await use('baseApiURL'); const storageStatePath = 'playwright/.auth/user.json'; // 从storageState提取Bearer令牌(根据实际存储结构调整) const storageState = JSON.parse(fs.readFileSync(storageStatePath, 'utf-8')); const accessTokenItem = storageState.origins[0].localStorage.find( (item: any) => item.name === 'oidc.user:someUrl' ); const accessToken = accessTokenItem ? JSON.parse(accessTokenItem.value).access_token : ''; // 创建带认证的请求上下文 const requestContext = await playwright.request.newContext({ baseURL: baseApiURL, extraHTTPHeaders: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, storageState: storageStatePath // 保留Cookie认证支持(如果需要) }); // 初始化服务类 clientService = new ClientService(requestContext, baseApiURL); }); test('should create a new client via API', async () => { const clientInfo = { name: 'Test Client', businessId: '12345678', address: 'Test City', description: 'Test Description', category: 'Hotels', subCategory: 'Seasonal', }; const response = await clientService.addClient(clientInfo); expect(response.ok()).toBeTruthy(); const createdClient = await response.json(); expect(createdClient.name).toBe(clientInfo.name); }); });
关键优势
- 完全对齐现有POM结构,保持代码风格统一
- 复用全局
storageState,无需在每个测试中重复处理认证逻辑 - API基础地址与UI地址分离,通过环境配置统一管理
- 服务类封装通用请求逻辑,减少重复代码,便于维护
内容的提问来源于stack exchange,提问作者Ivan Simeonov
相关产品推荐
相关产品推荐

