waylandace/pact-php

启用消费者驱动的合约测试,遵循PACT基础原则。

7.2.5 2023-02-03 18:04 UTC

README

pact-php Packagist

Downloads Downloads This Month

Pact的PHP版本。Pact启用消费者驱动的合约测试。请阅读Pact.io以获取有关PACT的详细信息。

目录

版本

8.X更新了内部依赖和库。这导致不支持PHP 7.3。

7.x更新了内部依赖和库,主要是Guzzle 7.X。这导致不支持PHP 7.2。6.x更新了内部依赖,主要是围绕Amp库。这导致不支持PHP 7.1。

5.X添加了对异步消息和Pact规范3.X的初步支持。由于后端实现不完整,因此尚不支持完整的Pact规范3.X。但是,支持pact-messages。

4.X标签是PHPUnit 7.X更改的伴随,它需要PHP 7.1或更高版本。因此,4.X不支持PHP 7.0。

3.X标签是对2.X版本的重大变更。为了与Pact生态系统中的其他部分保持一致,Pact-PHP迁移到利用Ruby后端。这反映了.NET、JS、Python和Go实现。

如果您想继续使用2.X实现,可以继续从最新的2.X.X标签中提取。

规范

3.X版本是Pact-PHP的版本,而不是它支持的Pact规范版本。Pact-Php 3.X支持Pact-Specification 2.X

安装

使用以下命令安装最新版本:

$ composer require pact-foundation/pact-php --dev

Composer将旧版本托管在mattersight/phppact下,该版本已弃用。请将其转换为新的包名。

基本消费者使用

以下所有代码都仅用于消费者。

启动和停止模拟服务器

此库包含对Ruby独立模拟服务的包装。

配置此内容的最佳方法是使用PHPUnit监听器。此项目包含一个默认监听器,请参阅PactTestListener.php。这利用环境变量进行配置。这些环境变量可以添加到系统中,也可以添加到phpunit.xml配置文件中。以下是一个配置为使用默认设置的phpunit.xml文件示例。请注意,测试套件和参数数组必须具有相同的值。

或者,您可以根据以下示例以您喜欢的任何方式启动和停止

<?php
    use PhpPact\Standalone\MockService\MockServer;
    use PhpPact\Standalone\MockService\MockServerConfig;

    // Create your basic configuration. The host and port will need to match
    // whatever your Http Service will be using to access the providers data.
    $config = new MockServerConfig();
    $config->setHost('localhost');
    $config->setPort(7200);
    $config->setConsumer('someConsumer');
    $config->setProvider('someProvider');
    $config->setCors(true);

    // Instantiate the mock server object with the config. This can be any
    // instance of MockServerConfigInterface.
    $server = new MockServer($config);

    // Create the process.
    $server->start();

    // Stop the process.
    $server->stop();

创建消费者单元测试

创建一个标准的PHPUnit测试用例类和函数。

点击此处查看完整示例文件。

创建模拟请求

这将定义从您的http服务发送的预期请求的形状。

$request = new ConsumerRequest();
$request
    ->setMethod('GET')
    ->setPath('/hello/Bob')
    ->addHeader('Content-Type', 'application/json');

您也可以创建一个与提供者示例中看到的一样地体。

创建模拟响应

这将定义提供者响应的形状。

$matcher = new Matcher();

$response = new ProviderResponse();
$response
    ->setStatus(200)
    ->addHeader('Content-Type', 'application/json')
    ->setBody([
        'message' => $matcher->regex('Hello, Bob', '(Hello, )[A-Za-z]')
    ]);

在这个示例中,我们使用匹配器。这允许我们在匹配期望值与实际值时添加灵活的规则。在示例中,您将看到使用正则表达式来验证响应的有效性。

$matcher = new Matcher();

$response = new ProviderResponse();
$response
    ->setStatus(200)
    ->addHeader('Content-Type', 'application/json')
    ->setBody([
        'list' => $matcher->eachLike([
            'firstName' => 'Bob',
            'age' => 22
        ])
    ]);

构建交互

现在我们有了请求和响应,我们需要构建交互并将其发送到模拟服务器。

// Create a configuration that reflects the server that was started. You can
// create a custom MockServerConfigInterface if needed. This configuration
// is the same that is used via the PactTestListener and uses environment variables.
$config  = new MockServerEnvConfig();
$builder = new InteractionBuilder($config);
$builder
    ->given('a person exists')
    ->uponReceiving('a get request to /hello/{name}')
    ->with($request)
    ->willRespondWith($response); // This has to be last. This is what makes an API request to the Mock Server to set the interaction.

发送请求

$service = new HttpClientService($config->getBaseUri()); // Pass in the URL to the Mock Server.
$result  = $service->getHelloString('Bob'); // Make the real API request against the Mock Server.

验证交互

验证所有已注册的交互是否都已发生。这通常应该在每个测试中执行,以便正确标记未通过验证的测试。

$builder->verify();

进行断言

验证根据配置的响应预期数据是否正确。

$this->assertEquals('Hello, Bob', $result); // Make your assertions.

基本提供者使用

以下所有代码仅用于提供者。这将运行Pacts与真实提供者进行比较,并在Pact Broker上验证或失败验证。

创建单元测试

创建一个单独的单元测试函数。这将测试该服务的单个消费者。

启动API

启动API实例。有关提示,请点击此处

如果需要在发送每个请求之前设置API的状态,请参阅设置提供者状态

提供者验证

有三种方式验证Pact文件。请参阅下面的示例。

从Pact代理验证

这将从Pact Broker获取Pact文件并运行数据与已启动的API进行比较。

$config = new VerifierConfig();
$config
    ->setProviderName('someProvider') // Providers name to fetch.
    ->setProviderVersion('1.0.0') // Providers version.
    ->setProviderBranch('main') // Providers git branch name.
    ->setProviderBaseUrl(new Uri('https://:58000')) // URL of the Provider.
    ->setBrokerUri(new Uri('https://')) // URL of the Pact Broker to publish results.
    ->setPublishResults(true) // Flag the verifier service to publish the results to the Pact Broker.
    ->setProcessTimeout(60)      // Set process timeout (optional) - default 60
    ->setProcessIdleTimeout(10) // Set process idle timeout (optional) - default 10
    ->setEnablePending(true) // Flag to enable pending pacts feature (check pact docs for further info)
    ->setIncludeWipPactSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info)
    ->setRequestFilter(
        function (RequestInterface $r) {
            return $r->withHeader('MY_SPECIAL_HEADER', 'my special value');
        }
    );
// Verify that the Consumer 'someConsumer' that is tagged with 'master' is valid.
$verifier = new Verifier($config);
$verifier->verify('someConsumer', 'master'); // The tag is option. If no tag is set it will just grab the latest.

// This will not be reached if the PACT verifier throws an error, otherwise it was successful.
$this->assertTrue(true, 'Pact Verification has failed.');
从Pact代理验证所有

这将获取与给定提供者关联的所有Pact文件。

public function testPactVerifyAll()
{
    $config = new VerifierConfig();
    $config
        ->setProviderName('someProvider') // Providers name to fetch.
        ->setProviderVersion('1.0.0') // Providers version.
        ->setProviderBranch('main') // Providers git branch name.
        ->setProviderBaseUrl(new Uri('https://:58000')) // URL of the Provider.
        ->setBrokerUri(new Uri('https://')) // URL of the Pact Broker to publish results.
        ->setPublishResults(true) // Flag the verifier service to publish the results to the Pact Broker.
        ->setEnablePending(true) // Flag to enable pending pacts feature (check pact docs for further info)
        ->setIncludeWipPactSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info)

    // Verify that all consumers of 'someProvider' are valid.
    $verifier = new Verifier($config);
    $verifier->verifyAll();

    // This will not be reached if the PACT verifier throws an error, otherwise it was successful.
    $this->assertTrue(true, 'Pact Verification has failed.');
}
通过路径验证文件

这允许本地Pact文件测试。

public function testPactVerifyAll()
{
    $config = new VerifierConfig();
    $config
        ->setProviderName('someProvider') // Providers name to fetch.
        ->setProviderVersion('1.0.0') // Providers version.
        ->setProviderBranch('main') // Providers git branch name.
        ->setProviderBaseUrl(new Uri('https://:58000')) // URL of the Provider.
        ->setBrokerUri(new Uri('https://')) // URL of the Pact Broker to publish results.
        ->setPublishResults(true); // Flag the verifier service to publish the results to the Pact Broker.
        ->setEnablePending(true) // Flag to enable pending pacts feature (check pact docs for further info)
        ->setIncludeWipPactSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info)

    // Verify that the files in the array are valid.
    $verifier = new Verifier($config);
    $verifier->verifyFiles(['C:\SomePath\consumer-provider.json']);

    // This will not be reached if the PACT verifier throws an error, otherwise it was successful.
    $this->assertTrue(true, 'Pact Verification has failed.');
}

技巧

异步启动API

您可以在测试的setUp函数期间使用内置的PHP服务器来完成此操作。可以使用Symfony Process库异步运行进程。

PHP服务器

Symfony Process

设置提供者状态

PACT验证器是Ruby独立验证器的包装器。有关如何工作的更多信息,请参阅带有提供者状态的API。由于大多数PHP REST API是无状态的,这需要一些思考。

这里有几种选择

  1. 将发布的state写入文件,并使用工厂根据state决定使用哪个mock repository类。
  2. 设置数据库以满足请求的预期。在每个请求的开始,您应该首先将数据库重置到其原始状态。

无论您选择哪种方式,您都必须修改PHP进程之外的内容,因为每个对您服务器的请求都将是无状态的且独立。

其他示例

有一个独立的存储库,包含2.X和3.X实现的端到端示例。

消息支持

这个特性是初步的,因为整个 Pact 社区正在完善它。目标不是测试对象在总线上的传输,而是审查消息的内容。虽然示例侧重于 Rabbit MQ,但确切的消息队列无关紧要。初始比较需要消息的发布者/生产者和消费者创建一定类型的对象。这包括一个元数据集,您可以存储发布者和消费者同意的键、队列、交换等。内容格式必须是 JSON。

为了利用现有的 pact-verification 工具,等式的一侧设置一个 http 代理来回调处理类。除了更改默认端口外,这应该对库用户来说是透明的。

提供者和消费者双方都大量使用 lambda 函数。

消费者端消息处理

提供的示例相当基础。请参阅 examples\tests\MessageConsumer。

  1. 创建内容和元数据(数组)
  2. 为 MessageBuilder 注释适当的内容和状态
    1. 给定 = 提供者状态
    2. expectsToReceive = 描述
  3. 设置当提供消息时要运行的回调
    1. 回调必须接受一个 JSON 字符串作为参数
  4. 运行 Verify。如果没有出错,#winning。
$builder    = new MessageBuilder(self::$config);

$contents       = new \stdClass();
$contents->song = 'And the wind whispers Mary';

$metadata = ['queue'=>'And the clowns have all gone to bed', 'routing_key'=>'And the clowns have all gone to bed'];

$builder
    ->given('You can hear happiness staggering on down the street')
    ->expectsToReceive('footprints dressed in red')
    ->withMetadata($metadata)
    ->withContent($contents);

// established mechanism to this via callbacks
$consumerMessage = new ExampleMessageConsumer();
$callback        = [$consumerMessage, 'ProcessSong'];
$builder->setCallback($callback);

$builder->verify();

提供者端消息验证

随着我们对这个实现的推进,这可能会演变。提供者严重依赖于回调。一些复杂性在于消费者和提供者在单个 pact 中有很多消息和状态。

对于每条消息,需要提供一个单独的提供者状态。这个提供者状态的名字必须是运行特定消息回调在提供者侧的键。请参阅 example\tests\MessageProvider。

  1. 创建你的回调和状态,封装在可调用对象中
    1. 数组键是消费者侧的提供者状态 / 给定(given)
    2. 如果您需要自定义要传递的参数,将整个内容封装在 lambda 中是有帮助的
  2. 选择你的验证方法
  3. 如果没有出错,#winning
        $callbacks = array();

        // a hello message is a provider state / given() on the consumer side
        $callbacks["a hello message"] = function() {
            $content = new \stdClass();
            $content->text ="Hello Mary";

            $metadata = array();
            $metadata['queue'] = "myKey";

            $provider = (new ExampleMessageProvider())
                ->setContents($content)
                ->setMetadata($metadata);

            return $provider->Build();
        };

        $verifier = (new MessageVerifier($config))
            ->setCallbacks($callbacks)
            ->verifyFiles([__DIR__ . '/../../output/test_consumer-test_provider.json']);

pact-stub-service 的用法

如果您想使用 fixtures 进行测试,可以使用 pact-stub-service 如此

$pactLocation             = __DIR__ . '/someconsumer-someprovider.json';
$host                     = 'localhost';
$port                     = 7201;
$endpoint                 = 'test';

$config = (new StubServerConfig())
            ->setPactLocation($pactLocation)
            ->setHost($host)
            ->setPort($port)
            ->setEndpoint($endpoint);

$stubServer = new StubServer($config);
$stubServer->start();

$service = new StubServerHttpService(new GuzzleClient(), $config);

echo $service->getJson(); // output: {"results":[{"name":"Games"}]}