PHP 怎么契约测试Pact

wen PHP项目 3

本文目录导读:

PHP 怎么契约测试Pact

  1. Pact基础概念
  2. 环境准备
  3. 消费者端测试(Consumer Testing)
  4. 提供者端测试(Provider Testing)
  5. 使用Pact Broker
  6. 集成到CI/CD
  7. 完整的示例应用
  8. 最佳实践
  9. 常见问题解决

我来详细介绍如何在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进行契约测试了。

抱歉,评论功能暂时关闭!