You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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.');
  });
});

挑战与问题

  1. 令牌管理:如何从storageState提取access_token并自动注入到API请求中?
  2. 服务初始化:如何像初始化页面类一样,无需手动传递令牌或Cookie即可初始化BaseService及其子类?
  3. 基础地址设置:如何管理与UIbaseURL独立的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 15:50:54