本文目录导读:

我来详细介绍如何在PHP中进行Pact契约测试。
Pact基础概念
Pact是一种契约测试工具,用于确保服务提供者和消费者之间的API兼容性,主要有两个角色:
- Consumer(消费者):调用API的一方
- Provider(提供者):提供API的一方
环境准备
安装依赖
# 消费者端 composer require pact-foundation/pact-php # 提供者端 composer require --dev pact-foundation/pact-php
消费者端测试(Consumer Testing)
创建消费者测试
<?php
// tests/Consumer/ConsumerTest.php
use PHPUnit\Framework\TestCase;
use PhpPact\Consumer\InteractionBuilder;
use PhpPact\Consumer\Matcher\Matcher;
use PhpPact\Consumer\Model\ConsumerRequest;
use PhpPact\Consumer\Model\ProviderResponse;
use PhpPact\Standalone\MockService\MockServerConfig;
class UserServiceConsumerTest extends TestCase
{
private $httpClient;
private $mockServer;
protected function setUp(): void
{
// 配置Mock服务
$config = new MockServerConfig();
$config->setHost('localhost');
$config->setPort(7200);
$config->setConsumer('UserServiceConsumer');
$config->setProvider('UserApiProvider');
$config->setPactDir(__DIR__ . '/pacts');
// 创建交互构建器
$builder = new InteractionBuilder($config);
$this->mockServer = $config;
// 配置HTTP客户端
$this->httpClient = new \GuzzleHttp\Client([
'base_uri' => 'http://localhost:7200'
]);
}
/** @test */
public function testGetUserById()
{
$matcher = new Matcher();
// 定义消费者请求
$request = new ConsumerRequest();
$request
->setMethod('GET')
->setPath('/api/users/1')
->addHeader('Accept', 'application/json');
// 定义提供者响应
$response = new ProviderResponse();
$response
->setStatus(200)
->addHeader('Content-Type', 'application/json')
->setBody([
'id' => $matcher->integerType(1),
'name' => $matcher->stringType('John Doe'),
'email' => $matcher->stringType('john@example.com'),
'age' => $matcher->integerType(30)
]);
// 注册交互
$interaction = new \PhpPact\Consumer\Model\Interaction();
$interaction
->setDescription('Get user by ID')
->setProviderState('user with ID 1 exists')
->setRequest($request)
->setResponse($response);
$builder->addInteraction($interaction);
// 发送实际请求
$result = $this->httpClient->get('/api/users/1', [
'headers' => ['Accept' => 'application/json']
]);
// 验证响应
$this->assertEquals(200, $result->getStatusCode());
$data = json_decode($result->getBody(), true);
$this->assertEquals(1, $data['id']);
$this->assertNotEmpty($data['name']);
$this->assertTrue(filter_var($data['email'], FILTER_VALIDATE_EMAIL) !== false);
// 完成后会生成pact文件
}
}
使用Matcher更多功能
<?php
// tests/Consumer/AdvancedConsumerTest.php
class AdvancedConsumerTest extends TestCase
{
/** @test */
public function testComplexApiInteractions()
{
$matcher = new Matcher();
// 请求体匹配器
$request = new ConsumerRequest();
$request
->setMethod('POST')
->setPath('/api/users')
->addHeader('Content-Type', 'application/json')
->setBody([
'name' => $matcher->stringType('Test User'),
'email' => $matcher->stringType('test@example.com'),
'roles' => $matcher->arrayContaining(['admin']),
'metadata' => $matcher->eachLike([
'key' => $matcher->stringType('test'),
'value' => $matcher->stringType('value')
])
]);
// 响应匹配器
$response = new ProviderResponse();
$response
->setStatus(201)
->addHeader('Content-Type', 'application/json')
->setBody([
'id' => $matcher->integerType(1),
'name' => $matcher->like('Test User'),
'email' => $matcher->email('test@example.com'),
'created_at' => $matcher->dateTime(),
'links' => $matcher->eachLike([
'rel' => $matcher->stringType('self'),
'href' => $matcher->url('http://example.com/api/users/1'),
])
]);
// 更多的匹配器使用...
}
}
提供者端测试(Provider Testing)
创建提供者测试
<?php
// tests/Provider/ProviderTest.php
use PHPUnit\Framework\TestCase;
use PhpPact\Standalone\Provider\ProviderVerifier;
use PhpPact\Standalone\Provider\ProviderVerifierOptions;
class UserApiProviderTest extends TestCase
{
/** @test */
public function testUserApiProviderContract()
{
// 配置提供者验证器
$options = new ProviderVerifierOptions();
$options->setProviderName('UserApiProvider');
$options->setProviderBaseUrl('http://localhost:8080');
$options->setPactBrokerUri('http://localhost:9292'); // Pact Broker地址
$options->setPublishResults(true);
$options->setBrokerToken('your-broker-token');
// 创建验证器
$verifier = new ProviderVerifier($options);
// 运行验证
try {
$verifier->verify();
$this->assertTrue(true);
} catch (\Exception $e) {
$this->fail('契约验证失败: ' . $e->getMessage());
}
}
}
使用本地pact文件验证
<?php
// tests/Provider/LocalPactProviderTest.php
class LocalPactProviderTest extends TestCase
{
/** @test */
public function testWithLocalPactFiles()
{
$options = new ProviderVerifierOptions();
$options->setProviderName('UserApiProvider');
$options->setProviderBaseUrl('http://localhost:8080');
$options->setPactFiles([
__DIR__ . '/pacts/UserServiceConsumer-UserApiProvider.json'
]);
$verifier = new ProviderVerifier($options);
// 验证所有交互
$verifier->verify();
}
}
使用Pact Broker
配置Pact Broker
<?php
// config/pact_broker.php
return [
'broker_url' => 'http://localhost:9292',
'consumer_version' => '1.0.0',
'provider_version' => '1.0.0',
'publish_results' => true,
'broker_token' => null, // 如果需要认证
];
发布和管理契约
# 启动Pact Broker (使用Docker) docker run -d -p 9292:9292 pactfoundation/pact-broker # 查看Broker UI # http://localhost:9292
集成到CI/CD
Jenkins配置
pipeline {
agent any
stages {
stage('Consumer Tests') {
steps {
sh 'composer install'
sh 'php vendor/bin/phpunit --testsuite Consumer'
// 发布pact文件到Broker
sh 'curl -X PUT \
-H "Content-Type: application/json" \
-d @pacts/UserServiceConsumer-UserApiProvider.json \
http://localhost:9292/pacts/provider/UserApiProvider/consumer/UserServiceConsumer/version/1.0.0'
}
}
stage('Provider Tests') {
steps {
// 启动服务
sh 'php -S localhost:8080 -t public/ &'
// 运行提供者验证
sh 'php vendor/bin/phpunit --testsuite Provider'
}
}
}
}
完整的示例应用
消费者客户端
<?php
// src/UserServiceClient.php
class UserServiceClient
{
private $httpClient;
public function __construct($baseUrl)
{
$this->httpClient = new \GuzzleHttp\Client([
'base_uri' => $baseUrl
]);
}
public function getUser($id)
{
$response = $this->httpClient->get("/api/users/{$id}");
return json_decode($response->getBody(), true);
}
public function createUser($userData)
{
$response = $this->httpClient->post('/api/users', [
'json' => $userData
]);
return json_decode($response->getBody(), true);
}
}
提供者API
<?php
// public/users.php
class UserApiController
{
public function getUser($id)
{
// 数据库查询
$user = [
'id' => (int)$id,
'name' => 'John Doe',
'email' => 'john@example.com',
'age' => 30
];
return json_encode($user);
}
public function createUser()
{
$data = json_decode(file_get_contents('php://input'), true);
// 创建用户
return json_encode([
'id' => 1,
'name' => $data['name'],
'email' => $data['email']
], JSON_PRETTY_PRINT);
}
}
最佳实践
契约测试注意事项
// 1. 保持匹配器合理使用
class ContractTestBestPractices
{
public function testContractExamples()
{
$matcher = new Matcher();
// ✅ 好:使用合理范围
$response->setBody([
'id' => $matcher->integerType(100),
'name' => $matcher->stringType('Valid Name')
]);
// ❌ 避免:过度精确
// 'id' => 100 // 不要硬编码具体值
// ✅ 好:使用灵活匹配
'email' => $matcher->stringType('user@example.com')
// ❌ 避免:使用正则过于严格
// $matcher->regex('\\d{5}', '12345') // 可能过于限制
}
// 2. 处理可选字段
public function testOptionalFields()
{
$matcher = new Matcher();
$response->setBody([
'id' => $matcher->integerType(1),
'name' => $matcher->stringType('Test'),
'profile' => $matcher->eachLike([
'avatar' => $matcher->stringType('url')
]),
'contact' => $matcher->like([
'phone' => $matcher->stringType('123-456-7890')
])
]);
}
}
测试组织
// phpunit.xml
<phpunit>
<testsuites>
<testsuite name="Consumer">
<directory>tests/Consumer</directory>
</testsuite>
<testsuite name="Provider">
<directory>tests/Provider</directory>
</testsuite>
</testsuites>
</phpunit>
常见问题解决
// 处理回调/状态变化
class StateHandlerTest extends TestCase
{
public function testWithStateHandlers()
{
// 定义provider states
$stateHandler = function ($state) {
switch ($state) {
case 'user with ID 1 exists':
// 准备测试数据
$this->seedDatabase([...]);
break;
case 'user with ID 999 does not exist':
// 删除测试用户
break;
}
};
// 传递给验证器
$verifier = new ProviderVerifier($options);
$verifier->setStateHandler($stateHandler);
$verifier->verify();
}
}
通过遵循这些实践,你就能在PHP项目中有效地使用Pact进行契约测试了。