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

Playwright Worker级别Singleton Pact Mock Server重复启动/终止导致Pact崩溃问题求助

Playwright Worker级别Singleton Pact Mock Server重复启动/终止导致Pact崩溃问题求助

最近在做Pact消费者测试,用Playwright(TypeScript语言)当测试运行器,本来想实现每个Worker只启动一次Mock Server,让同一个Worker下的所有测试共享这个实例,结果踩了个大坑:发现start()和finalise()居然被调用了两次,就算我在Playwright配置里把workers设为1,跑两个测试用例还是会触发Pact崩溃的错误,实在搞不定了来求助!

环境信息

  • Node 20.12
  • @playwright/test 1.52.x
  • @pact-foundation/pact 16.1.0
  • macOS 15.x
  • Playwright配置:workers: 1,仅针对Pact的测试项目

报错内容

[17:49:08.419] ERROR (55095): pact-core@16.1.0:
!!!!!!!!! PACT CRASHED !!!!!!!!!
The pact consumer core returned false at 'withResponseBody'.
This should only happen if the core methods were invoked out of order
This is almost certainly a bug in pact-js-core.
It would be great if you could open a bug report at: https://github.com/pact-foundation/pact-js-core/issues so that we can fix it.
There is additional debugging information above. If you open a bug report, please rerun with logLevel: 'debug' set in the VerifierOptions, and include the full output.
SECURITY WARNING: Before including your log in the issue tracker, make sure you have removed sensitive info such as login credentials and urls that you don't want to share with the world.
We're sorry about this!

已经尝试过的方案

  • 对比使用scope: 'worker'和scope: 'test'两种 fixture 作用域
  • 在测试后调用pact.removeInteractions()清理交互
  • 每个测试后调用pact.verify()验证交互
  • 设置port: 0让系统自动分配端口,避免端口冲突
  • 确保HTTP客户端在Pact启动完成后再创建(依赖pactBaseUrl)
  • 保证所有测试都从同一个路径导入fixture,避免重复实例化

可复现代码

Fixture代码(最终迭代版)

import path from 'path';
import { APIRequestContext, request, test as base } from '@playwright/test';
import { Pact } from '@pact-foundation/pact';

type WorkerFixtures = {
  pact: Pact;
  pactBaseUrl: string;
  client: APIRequestContext;
};

let pactSingleton: Pact | null = null;
let starting: Promise<Pact> | null = null;
let refCount = 0;

async function startPact(): Promise<Pact> {
  if (pactSingleton) return pactSingleton;
  if (starting) return starting;

  starting = (async () => {
    const pact = new Pact({
      consumer: 'frontend',
      provider: 'user-service',
      dir: path.resolve(process.cwd(), 'pacts'),
      port: 0,
      logLevel: 'warn',
    });
    await pact.setup();
    pactSingleton = pact;
    return pact;
  })();
  return starting;
}

async function stopPact(): Promise<void> {
  if (!pactSingleton) return;
  try {
    console.log('Is pact finalize:', (pactSingleton as any).finalized);
    await pactSingleton.finalize();
  } catch (e) {
    pactSingleton.mockService.pactDetails.consumer.name;
    console.error('Double finalize');
    console.error(e);
  } finally {
    pactSingleton = null;
    starting = null;
  }
}

export const test = base.extend<NonNullable<unknown>, WorkerFixtures>({
  pact: [
    async ({}, use, testInfo) => {
      const pact = await startPact();
      refCount++;
      console.log('PACT STARTED, WORKER', testInfo.workerIndex);
      console.log('refCount: ', refCount);
      await use(pact);
      refCount--;
      console.log('refCount after use: ', refCount);
      if (refCount === 0) {
        console.log('STOP PACT, worker: ', testInfo.workerIndex);
        await stopPact();
      }
    },
    { scope: 'worker' },
  ],
  pactBaseUrl: [
    async ({ pact }, use) => {
      console.log('Pact baseUrl:', pact.mockService.baseUrl);
      await use(pact.mockService.baseUrl);
    },
    { scope: 'worker' },
  ],
  client: [
    async ({ pactBaseUrl }, use) => {
      const ctx = await request.newContext({
        baseURL: pactBaseUrl,
        extraHTTPHeaders: {
          Authorization: 'Basic sOmE-token==',
          'X-Auth-Verified': 'true',
        },
      });
      console.log('Pact client created with baseURL: ');
      await use(ctx);
      await ctx.dispose();
    },
    { scope: 'worker' },
  ],
});

export const expect = base.expect;

第一个测试用例

import { Matchers } from '@pact-foundation/pact';
import { expect } from '@playwright/test';
import { userUri } from '../../../http-client/uri';
import { test } from './fixture';

const { like, eachLike, iso8601DateTime } = Matchers;

test.describe('Pact test: create user', () => {
  test.afterAll(async ({ pact }) => {
    await pact.removeInteractions();
  });

  test('verify user creation (201)', async ({ pact, client }) => {
    await pact.addInteraction({
      state: 'provider accepts new user',
      uponReceiving: 'a POST request to create user',
      withRequest: {
        method: 'POST',
        path: `${userUri}`,
        headers: {
          'X-Auth-Verified': 'true',
          Authorization: like('Basic sOmE-token=='),
          'Content-Type': 'application/json',
        },
        body: like(withRequestBody),
      },
      willRespondWith: {
        status: 201,
        headers: {
          'Content-Type': 'application/json',
        },
        body: like(withResponseBody),
      },
    });

    const response = await client.post(userUri, request);
    const body = await response.json();
    expect(response.status()).toBe(201);
    expect(body).toHaveProperty('ID');
    expect(body.status).toBe('PENDING');
  });
});

const withRequestBody = { 
  // 自定义请求体数据
};
const withResponseBody = { 
  // 自定义响应体数据
};

第二个测试用例

import { expect } from '@playwright/test';
import { Matchers } from '@pact-foundation/pact';
import { userUri } from '../../../http-client/uri';
import { test } from './fixture';

const { like } = Matchers;

test.describe('Pact test: query user', () => {
  test.afterAll(async ({ pact }) => {
    await pact.removeInteractions();
  });

  test('verify user query (200)', async ({ pact, client }) => {
    await pact.addInteraction({
      state: 'user exists',
      uponReceiving: 'a GET request to query user',
      withRequest: {
        method: 'GET',
        path: `${userUri}/123`,
        headers: {
          'X-Auth-Verified': 'true',
          Authorization: like('Basic sOmE-token=='),
        },
      },
      willRespondWith: {
        status: 200,
        headers: {
          'Content-Type': 'application/json',
        },
        body: like({
          ID: '123',
          name: 'test user',
          status: 'ACTIVE'
        }),
      },
    });

    const response = await client.get(`${userUri}/123`);
    const body = await response.json();
    expect(response.status()).toBe(200);
    expect(body.ID).toBe('123');
  });
});

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 08:43:10