Pact Php Save

PHP version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project

Project README

logo

Pact PHP

Code Analysis & Test Compatibility Suite Packagist

Downloads Downloads This Month

Fast, easy and reliable testing for your APIs and microservices.

Pact is the de-facto API contract testing tool. Replace expensive and brittle end-to-end integration tests with fast, reliable and easy to debug unit tests.

  • ⚑ Lightning fast
  • 🎈 Effortless full-stack integration testing - from the front-end to the back-end
  • πŸ”Œ Supports HTTP/REST and event-driven systems
  • πŸ› οΈ Configurable mock server
  • 😌 Powerful matching rules prevents brittle tests
  • 🀝 Integrates with Pact Broker / PactFlow for powerful CI/CD workflows
  • πŸ”‘ Supports 12+ languages

Why use Pact?

Contract testing with Pact lets you:

  • ⚑ Test locally
  • πŸš€ Deploy faster
  • ⬇️ Reduce the lead time for change
  • πŸ’° Reduce the cost of API integration testing
  • πŸ’₯ Prevent breaking changes
  • πŸ”Ž Understand your system usage
  • πŸ“ƒ Document your APIs for free
  • πŸ—„ Remove the need for complex data fixtures
  • πŸ€·β€β™‚οΈ Reduce the reliance on complex test environments

Watch our series on the problems with end-to-end integrated tests, and how contract testing can help.

----------

Table of contents

Versions

Version Status Spec Compatibility PHP Compatibility Install
10.x Alpha 1, 1.1, 2, 3, 4 ^8.1 See installation
9.x Stable 1, 1.1, 2, 3* ^8.0 9xx
8.x Deprecated 1, 1.1, 2, 3* ^7.4 ^8.0
7.x Deprecated 1, 1.1, 2, 3* ^7.3
6.x Deprecated 1, 1.1, 2, 3* ^7.2
5.x Deprecated 1, 1.1, 2, 3* ^7.1
4.x Deprecated 1, 1.1, 2 ^7.1
3.x Deprecated 1, 1.1, 2 ^7.0
2.x Deprecated 1, 1.1, 2 >=7
1.x Deprecated 1, 1.1 >=7

* v3 support is limited to the subset of functionality required to enable language inter-operable Message support.

Β Supported Platforms

OS Architecture Supported Pact-PHP Version
OSX x86_64 βœ… All
Linux x86_64 βœ… All
OSX arm64 βœ… 9.x +
Linux arm64 βœ… 9.x +
Windows x86_64 βœ… All
Windows x86 βœ… All

Installation

Install the latest version with:

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

Composer hosts older versions under mattersight/phppact, which is abandoned. Please convert to the new package name.

Basic Consumer Usage

All of the following code will be used exclusively for the Consumer.

Create Consumer Unit Test

Create a standard PHPUnit test case class and function.

Click here to see the full sample file.

Create Mock Request

This will define what the expected request coming from your http service will look like.

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

You can also create a body just like you will see in the provider example.

Create Mock Response

This will define what the response from the provider should look like.

$matcher = new Matcher();

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

In this example, we are using matchers. This allows us to add flexible rules when matching the expectation with the actual value. In the example, you will see regex is used to validate that the response is valid.

$matcher = new Matcher();

$response = new ProviderResponse();
$response
    ->setStatus(200)
    ->addHeader('Content-Type', 'application/json')
    ->setBody([
        'list' => $matcher->eachLike([
            'firstName' => 'Bob',
            'age' => 22
        ])
    ]);
Matcher Explanation Parameters Example
term Match a value against a regex pattern. Value, Regex Pattern $matcher->term('Hello, Bob', '(Hello, )[A-Za-z]')
regex Alias to term matcher. Value, Regex Pattern $matcher->regex('Hello, Bob', '(Hello, )[A-Za-z]')
dateISO8601 Regex match a date using the ISO8601 format. Value (Defaults to 2010-01-01) $matcher->dateISO8601('2010-01-01')
timeISO8601 Regex match a time using the ISO8601 format. Value (Defaults to T22:44:30.652Z) $matcher->timeISO8601('T22:44:30.652Z')
dateTimeISO8601 Regex match a datetime using the ISO8601 format. Value (Defaults to 2015-08-06T16:53:10+01:00) $matcher->dateTimeISO8601('2015-08-06T16:53:10+01:00')
dateTimeWithMillisISO8601 Regex match a datetime with millis using the ISO8601 format. Value (Defaults to 2015-08-06T16:53:10.123+01:00) $matcher->dateTimeWithMillisISO8601('2015-08-06T16:53:10.123+01:00')
timestampRFC3339 Regex match a timestamp using the RFC3339 format. Value (Defaults to Mon, 31 Oct 2016 15:21:41 -0400) $matcher->timestampRFC3339('Mon, 31 Oct 2016 15:21:41 -0400')
like Match a value against its data type. Value $matcher->like(12)
somethingLike Alias to like matcher. Value $matcher->somethingLike(12)
eachLike Match on an object like the example. Value, Min (Defaults to 1) $matcher->eachLike(12)
constrainedArrayLike Behaves like the eachLike matcher, but also applies a minimum and maximum length validation on the length of the array. The optional count parameter controls the number of examples generated. Value, Min, Max, count (Defaults to null) $matcher->constrainedArrayLike('test', 1, 5, 3)
boolean Match against boolean true. none $matcher->boolean()
integer Match a value against integer. Value (Defaults to 13) $matcher->integer()
decimal Match a value against float. Value (Defaults to 13.01) $matcher->decimal()
hexadecimal Regex to match a hexadecimal number. Example: 3F Value (Defaults to 3F) $matcher->hexadecimal('FF')
uuid Regex to match a uuid. Value (Defaults to ce118b6e-d8e1-11e7-9296-cec278b6b50a) $matcher->uuid('ce118b6e-d8e1-11e7-9296-cec278b6b50a')
ipv4Address Regex to match a ipv4 address. Value (Defaults to 127.0.0.13) $matcher->ipv4Address('127.0.0.1')
ipv6Address Regex to match a ipv6 address. Value (Defaults to ::ffff:192.0.2.128) $matcher->ipv6Address('::ffff:192.0.2.1')
email Regex to match an address. Value ([email protected]) $matcher->email('[email protected]')

Build the Interaction

Now that we have the request and response, we need to build the interaction and ship it over to the mock server.

// 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', ['name' => 'Bob'])
    ->uponReceiving('a get request to /hello/{name}')
    ->with($request)
    ->willRespondWith($response); // This has to be last. This is what makes FFI calls to register the interaction and start the mock server.

Make the Request

$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.

Verify Interactions

Verify that all interactions took place that were registered. This typically should be in each test, that way the test that failed to verify is marked correctly.

$verifyResult = $builder->verify();
$this->assertTrue($verifyResult);

Make Assertions

Verify that the data you would expect given the response configured is correct.

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

Delete Old Pact

If the value of PACT_FILE_WRITE_MODE is merge, before running the test, we need to delete the old pact manually:

rm /path/to/pacts/consumer-provider.json

Publish Contracts To Pact Broker

When all tests in test suite are passed, you may want to publish generated contract files to pact broker.

CLI

Run this command using CLI tool:

pact-broker publish /path/to/pacts/consumer-provider.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-token SomeToken

See more at https://docs.pact.io/pact_broker/publishing_and_retrieving_pacts#publish-using-cli-tools

Github Actions

See how to use at https://github.com/pactflow/actions/tree/main/publish-pact-files

Basic Provider Usage

All of the following code will be used exclusively for Providers. This will run the Pacts against the real Provider and either verify or fail validation on the Pact Broker.

Create Unit Test

Create a single unit test function. This will test all defined consumers of the service.

protected function setUp(): void
{
    // Start API
}

protected function tearDown(): void
{
    // Stop API
}

public function testPactVerifyConsumers(): void
{
    $config = new VerifierConfig();
    $config->getProviderInfo()
        ->setName('someProvider')
        ->setHost('localhost')
        ->setPort(8000);
    $config->getProviderState()
        ->setStateChangeUrl(new Uri('http://localhost:8000/pact-change-state'))
        ->setStateChangeTeardown(true)
        ->setStateChangeAsBody(true);

    // If your provider dispatch messages
    $config->addProviderTransport(
        (new ProviderTransport())
            ->setProtocol(ProviderTransport::MESSAGE_PROTOCOL)
            ->setPort(8000)
            ->setPath('/pact-messages')
            ->setScheme('http')
    );

    // If you want to publish verification results to Pact Broker.
    if ($isCi = getenv('CI')) {
        $publishOptions = new PublishOptions();
        $publishOptions
            ->setProviderVersion(exec('git rev-parse --short HEAD'))
            ->setProviderBranch(exec('git rev-parse --abbrev-ref HEAD'));
        $config->setPublishOptions($publishOptions);
    }

    // If you want to display more/less verification logs.
    if ($logLevel = \getenv('PACT_LOGLEVEL')) {
        $config->setLogLevel($logLevel);
    }

    // Add sources ...

    $verifyResult = $verifier->verify();

    $this->assertTrue($verifyResult);
}

Verification Sources

There are four ways to verify Pact files. See the examples below.

Verify From Pact Broker

This will grab the Pact files from a Pact Broker.

$selectors = (new ConsumerVersionSelectors())
    ->addSelector(new Selector(mainBranch: true))
    ->addSelector(new Selector(deployedOrReleased: true));

$broker = new Broker();
$broker
    ->setUrl(new Uri('http://localhost'))
    ->setUsername('user')
    ->setPassword('pass')
    ->setToken('token')
    ->setEnablePending(true)
    ->setIncludeWipPactSince('2020-01-30')
    ->setProviderTags(['prod'])
    ->setProviderBranch('main')
    ->setConsumerVersionSelectors($selectors)
    ->setConsumerVersionTags(['dev']);

$verifier->addBroker($broker);

Verify From Url

This will grab the Pact file from a url.

$url = new Url();
$url
    ->setUrl(new Uri('http://localhost:9292/pacts/provider/personProvider/consumer/personConsumer/latest'))
    ->setUsername('user')
    ->setPassword('pass')
    ->setToken('token');

$verifier->addUrl($url);

Verify Files in Directory

This will grab local Pact files in directory. Results will not be published.

$verifier->addDirectory('C:\SomePath');

Verify Files by Path

This will grab local Pact file. Results will not be published.

$verifier->addFile('C:\SomePath\consumer-provider.json');

Start API

Get an instance of the API up and running. Click here for some tips.

If you need to set up the state of your API before making each request please see Set Up Provider State.

Tips

Starting API Asynchronously

You can use the built in PHP server to accomplish this during your tests setUp function. The Symfony Process library can be used to run the process asynchronous.

PHP Server

Symfony Process

Set Up Provider State

The PACT verifier is a wrapper of the Ruby Standalone Verifier. See API with Provider States for more information on how this works. Since most PHP rest APIs are stateless, this required some thought.

Here are some options:

  1. Write the posted state to a file and use a factory to decide which mock repository class to use based on the state.
  2. Set up your database to meet the expectations of the request. At the start of each request, you should first reset the database to its original state.

No matter which direction you go, you will have to modify something outside of the PHP process because each request to your server will be stateless and independent.

Message support

The goal is not to test the transmission of an object over a bus but instead vet the contents of the message. While examples included focus on a Rabbit MQ, the exact message queue is irrelevant. Initial comparisons require a certain object type to be created by the Publisher/Producer and the Consumer of the message. This includes a metadata set where you can store the key, queue, exchange, etc that the Publisher and Consumer agree on. The content format needs to be JSON.

To take advantage of the existing pact-verification tools, the provider side of the equation stands up an http proxy to callback to processing class. Aside from changing default ports, this should be transparent to the users of the libary.

Both the provider and consumer side make heavy use of lambda functions.

Consumer Side Message Processing

The examples provided are pretty basic. See example.

  1. Create the content and metadata (array)
  2. Annotate the MessageBuilder appropriate content and states
    1. Given = Provider State
    2. expectsToReceive = Description
  3. Set the callback you want to run when a message is provided
    1. The callback must accept a JSON string as a parameter
  4. Run Verify. If nothing blows up, #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);

$verifyResult = $builder->verify();

$this->assertTrue($verifyResult);

Provider Side Message Validation

Handle these requests on your provider:

  1. POST /pact-change-state
    1. Set up your database to meet the expectations of the request
    2. Reset the database to its original state.
  2. POST /pact-messages
    1. Return message's content in body
    2. Return message's metadata in header PACT-MESSAGE-METADATA

Click here to see the full sample file.

Usage for the optional pact-stub-service

If you would like to test with fixtures, you can use the pact-stub-service like this:

$files    = [__DIR__ . '/someconsumer-someprovider.json'];
$port     = 7201;
$endpoint = 'test';

$config = (new StubServerConfig())
            ->setFiles($files)
            ->setPort($port);

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

$client = new \GuzzleHttp\Client();

$response = $client->get($this->config->getBaseUri() . '/' . $endpoint);

echo $response->getBody(); // output: {"results":[{"name":"Games"}]}
Open Source Agenda is not affiliated with "Pact Php" Project. README Source: pact-foundation/pact-php
Stars
254
Open Issues
35
Last Commit
1 week ago
License

Open Source Agenda Badge

Open Source Agenda Rating