I really want to implement this for PHP. However, ...
# pact-php
p
I really want to implement this for PHP. However, I try to start to implement Pact-PHP with Laravel. I get these errors when I run tests
Copy code
Server error: `GET <http://localhost:38189/users/1>` resulted in a `500 Internal Server Error` response:
{"message":"Multiple interaction found for GET /users/1","matching_interactions":[{"description":"A get request to /user (truncated...)

  at vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:113
    109▕         if ($summary !== null) {
    110▕             $message .= ":\n{$summary}\n";
    111▕         }
    112▕ 
  ➜ 113▕         return new $className($message, $request, $response, $previous, $handlerContext);
    114▕     }
    115▕ 
    116▕     /**
    117▕      * Obfuscates URI if there is a username and a password present

      +10 vendor frames 
  11  tests/Consumer/Service/ConsumerServiceHelloTest.php:64
      GuzzleHttp\Client::request()
This is my test code
Copy code
<?php

namespace Tests\Consumer\Service;

use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use PhpPact\Consumer\InteractionBuilder;
use PhpPact\Consumer\Matcher\Matcher;
use PhpPact\Consumer\Model\ConsumerRequest;
use PhpPact\Consumer\Model\ProviderResponse;
use PhpPact\Standalone\MockService\MockServerEnvConfig;
use PHPUnit\Framework\TestCase;

class ConsumerServiceHelloTest extends TestCase
{
    /**
     * Example PACT test.
     *
     * @throws Exception
     * @throws GuzzleException
     */
    public function testGetUserWithGivenId()
    {
        $matcher = new Matcher();

        // Create your expected request from the consumer.
        $request = new ConsumerRequest();
        $request
            ->setMethod('GET')
            ->setPath('/users/1')
            ->addHeader('Content-Type', 'application/json');

        // Create your expected response from the provider.
        $response = new ProviderResponse();
        $response
            ->setStatus(200)
            ->addHeader('Content-Type', 'application/json')
            ->setBody([
                'id' => 1
            ]);

        // Create a configuration that reflects the server that was started. You can create a custom MockServerConfigInterface if needed.
        $config  = new MockServerEnvConfig();
        $builder = new InteractionBuilder($config);
        $builder
            ->uponReceiving('A get request to /users/{id}')
            ->with($request)
            ->willRespondWith($response); // This has to be last. This is what makes an API request to the Mock Server to set the interaction.

        $client = new Client([
            'base_uri' => $config->getBaseUri(),
        ]);

        $result = $client->request('GET', '/users/1', [
            'headers' => [
                'Content-Type' => 'application/json',
            ],
        ]);

        $builder->verify(); // This will verify that the interactions took place.

        $this->assertEquals('', $result); // Make your assertions.
    }
}
Debug
Copy code
[2022-02-25T00:53:14.684488 #35930] DEBUG -- : {
  "description": "A get request to /users/{id}",
  "request": {
    "method": "GET",
    "path": "/users/1",
    "headers": {
      "Content-Type": "application/json"
    }
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "body": {
      "id": 1
    }
  },
  "metadata": null
}
Please suggest to me 🙂
m
That error usually comes up when there is a conflicting scenario name (
uponReceiving
) and request details without a state to differentiate the requests. I can’t see it in your test, so this could be that there is a different test somewhere else with the same details. Alternatively, the pact write mode could be
merge
and the pact already has that request in it. In that case, you should clear out the pact file before running your tests
p
@Matt (pactflow.io / pact-js / pact-go) With the pact-php do not require to install pact-ruby-standalone, right? It can running from phpunit.xml. Am I understand, right?
👍 1
Maybe I still don't understand about the state in pact I see this and not clear why I need to setup
m
is the above your entire setup? It seems like you might be missing something. Are there other tests?
Alternatively, you might not be calling the appropriate lifecycle methods to start/stop the mock server (hence why it thinks it’s seeing a duplicate)
p
I am still confused about how to run a pact with PHP. I try to compare setup from go-workshop, but it differently.
Last time, I run a pack-mock-server with a standalone outside PHP project and see the result. So I need to add this
$builder->finalize()
to clear interactions when test end see updated code:
Copy code
$request = new ConsumerRequest();
$request->setMethod("GET")
    ->setPath("/users/1")
    ->addHeader("Accept", "application/json");

$response = new ProviderResponse();
$response->setStatus(200)
    ->addHeader("Content-Type", "application/json")
    ->setBody([
        "id" => 1,
        "name" => "John Doe",
        "email" => "<mailto:john@example.com|john@example.com>"
    ]);

$config = new MockServerEnvConfig();

$config->setLog(true)
    ->setLogLevel('debug');

$builder = new InteractionBuilder($config);
$builder->given("User with id 1 exists")
    ->uponReceiving("GET user for id: 1")
    ->with($request)
    ->willRespondWith($response);

$client = new Client(["base_uri" => $config->getBaseUri()]);
$userApi = new UserApi($client);

$result = $userApi->getUserById(1);

$this->assertTrue($builder->verify());
$this->assertEquals(1, $result->id);
$this->assertEquals('John Doe', $result->name);
$this->assertEquals('<mailto:john@example.com|john@example.com>', $result->email);

$builder->finalize();
Now it can generate pact file, but need to configure running pact-mock-server with phpunit.xml
phpunit.xaml
Copy code
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <listeners>
        <listener class="PhpPact\Consumer\Listener\PactTestListener">
            <arguments>
                <array>
                    <element key="0">
                        <string>PhpPact Example Tests</string>
                    </element>
                </array>
            </arguments>
        </listener>
    </listeners>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
        <testsuite name="PhpPact Example Tests">
            <directory>./tests/Consumer</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="MAIL_MAILER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="TELESCOPE_ENABLED" value="false"/>
        <env name="PACT_MOCK_SERVER_HOST" value="localhost"/>
        <env name="PACT_MOCK_SERVER_PORT" value="7200"/>
        <env name="PACT_CONSUMER_NAME" value="Frontend"/>
        <env name="PACT_CONSUMER_VERSION" value="1.0.0"/>
        <env name="PACT_CONSUMER_TAG" value="master"/>
        <env name="PACT_PROVIDER_NAME" value="UserApi"/>
        <env name="PACT_OUTPUT_DIR" value=".\example\output"/>
        <env name="PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT" value="10"/>
        <!-- <env name="PACT_BROKER_URI" value="<http://localhost>"/> -->
    </php>
</phpunit>
m
Ah, sorry I’m not sure. I don’t know Pact PHP very well
🙌 1
p
Hmm, Look like PHPUnit can run pact-mock-server now. Not sure I missing something.
No worry about that. You always help me. Thanks 🙂
🙏 1
Maybe I need to learn about the concept of Pact too much.
m
I read https://github.com/pact-foundation/pact-php/#start-and-stop-the-mock-server but don’t really know about PHP listeners
I don’t think you should be starting/stopping the Ruby standalone yourself. It looks like there are PHP methods to do it
you can either a) do a
$server->start();
before your tests and
$server->stop();
after them, or b) use the listeners (I think you’re using the listeners)
p
Yes, you're right. Yesterday I spend more time running it along with Phpunit from the link you share with me. I try to make sure no need to install Ruby and use the standalone like go-workshop
Now it can work now. Not sure why?
🤔 1
Yesterday, I got an error that failed to connect localhost:7200. pact-mock-server not running.
m
mmm maybe the server hadn’t started in time?
p
Not sure. Maybe related to the order of phpunit.xml. Today I just change the line of it before testsuite and it works.
@Matt (pactflow.io / pact-js / pact-go) Could you please explain about state in pact? I see it need database or file
m
A state is a general concept. In order to test complex scenarios with pact, your provide may need to be in a particular “state” to work. e.g. your provider might need to have “user A exist” in the database, for a request to
/users/A
to return a
200
it might have a state “user is unauthorized” to return a
403
it might expect the state to be “backend is unavailable” for a request to return a
500
A state could require you to mock api calls, seed a database, stub internal components etc.
You can use it to test the same calls with multiple responses (e.g. for users, it could be a
200
or a
404
if it doesn’t exist)
p
Normally, It probably works with a file, right?
m
what do you mean, sorry?
p
Ahh sorry for the unclear text. I mean state is the same with pact file, right?
I think it just only texts to tell me what is the test case.
Copy code
Given("User sally exists").
UponReceiving("A request to login with user 'sally'").
This is part of state, right?
☝️ 1
m
yes. your consumer test says what state each test should be in, that’s written to the pact file.
yep!
The combination of
Given
and
UponReceiving
is the scenario you’re testing
you can have the same
UponReceiving
but with different
Given
to test different sub-scenarios
the state is just a string (or an object, in later versions of the spec) that the provider test can used to determine how to get into that state. How you do that, is very specific to your code
p
Oh a bit clear
So finally it will generate to a json file, right?
Maybe I just need to explore and learn more. Thanks so much for your explanation. 🙏
m
Pact consumer test generate a pact file (the contract) Contract get’s published to broker (usually, but you can use the pact file) Provider verifies the contract
see the CI/CD workshop in 👇 howtolearn
s
Here are a number of useful hands-on labs that teach all of the key concepts: https://docs.pactflow.io/docs/workshops and https://docs.pact.io/implementation_guides/workshops
p
@Matt (pactflow.io / pact-js / pact-go) Thank you so much.
👍 1