mcustiel/phiremock-server

A mocker for HTTP and REST services


Keywords
http, mock, server, tests, external, acceptance, acceptance-testing, http-mocking, http-server, phiremock, php7, rest-api, rest-mock
License
CNRI-Python-GPL-Compatible

Documentation

Phiremock Server

Phiremock is a mocker and stubber of HTTP services, it allows software developers to mock HTTP requests and setup responses to avoid calling real services during development, and is particulary useful during acceptance testing when expected http requests can be mocked and verified. Any HTTP service (i.e.: REST services) can be mocked and stubbed with Phiremock.

Phiremock is heavily inspired by WireMock, but does not force you to have a java installation in your PHP development environment. The full functionality of Phiremock is detailed in the following list:

  • Allows to mock http request based in method, headers, url, body content and form fields.
  • Allows to match expectations using several comparison functions.
  • REST interface to setup.
  • Stateful and stateless mocking through scenarios.
  • Network latency simulation.
  • Priorizable expectations for cases in which more than one matches the request. If more than one expectation matches the request and no priorities were set, the first match is returned.
  • Allows to verify the amount of times a request was done.
  • Allows to load default expectations from json files in a directory tree.
  • Proxy requests to another URL as they are received.
  • Fill the response body using data from the request.
  • Integration to codeception through phiremock-codeception-extension and phiremock-codeception-module.
  • Client with nice API supporting all functionalities: phiremock-client

Version Build Status Scrutinizer Code Quality Packagist Downloads

Installation

Default installation through composer

    "require-dev": {
        "mcustiel/phiremock-server": "^1.0",
        "guzzlehttp/guzzle": "^6.0"
    }

Phiremock Server requires guzzle client v6 to work. This dependency can be avoided and you can choose any psr18-compatible http client and overwrite Phiremock Server's factory to provide it.

Phar

You can also download the standalone server as a phar from here.

Running

Execute ./vendor/bin/phiremock.

Command line arguments

  • --ip|-i - Network interface where to listen for http connections. Default: 0.0.0.0
  • --port|-p - Port where to listen for http connections. Default: 8086
  • --expectations-dir|-e - Directory where to search for predefined static expectations. Default: [HOME_PATH]/.phiremock/expectations
  • --factory-class|f - Factory class to use to create the objects needed by phiremock server. Default: Default internal factory class.
  • --certificate|t - Certificate file to enable phiremock to listen for secure connections (https).
  • --certificate-key|k - Path to the certificate key.
  • --cert-passphrase|s - Passphrase to use if the certificate is encrypted.
  • --debug|-d - Flag to activate the debug mode.

Note: Static expectations saved in files are very useful if you use phiremock in your development environment. For testing, can be more useful to setup expectations on the fly.

Note: When a certificate is added, phiremock-server will only listen for secure connections.

Configuration

You can statically specify phiremock server's configuration in the .phiremock file. It looks as following:

<?php return [
    'port'             => 8086,
    'ip'               => '0.0.0.0',
    'expectations-dir' => $_SERVER['HOME'] . '/.phiremock/expectations',
    'debug'            => false,
    'factory-class'    => '\\My\\Namespace\\FactoryClass',
    'certificate'      => null,
    'certificate-key'  => null,
    'certificate-passphrase' => null,
];

This file will be searched as following, the first one found is the one to use:

  1. PROJECT_ROOT_DIR/.phiremock (if installed under /vendor)
  2. PROJECT_ROOT_DIR/.phiremock.dist (if installed under /vendor)
  3. PHIREMOCK_ROOT_DIR/.phiremock (if pulled standalone)
  4. PHIREMOCK_ROOT_DIR/.phiremock.dist (if pulled standalone)
  5. $CWD/.phiremock
  6. $CWD/.phiremock.dist
  7. $HOME/.phiremock/config
  8. .phiremock (uses php's include path)
  9. .phiremock.dist(uses php's include path)

Note: The command line arguments have higher priority over the options in the config file, so they will override them if provided.

Overwriting the factory class

If guzzle client v6 is provided as a dependency no extra configuration is needed. If you want to use a different http client you need to provide it to phiremock server as a psr18-compatible client. For instance, if you want to use guzzle client v7 you need to extend phiremock server's factory class:

<?php
namespace My\Namespace;

use Mcustiel\Phiremock\Client\Factory;
use GuzzleHttp;
use Psr\Http\Client\ClientInterface;

class FactoryWithGuzzle7 extends Factory
{
    public function createRemoteConnection(): ClientInterface
    {
        return new GuzzleHttp\Client();
    }
}

Then provide the fully qualified class name to phiremock-server using the command line options or the configuration file.

Note: This will only work if phiremock is instaled through composer, since it will use the same vendor folder and autoloader as your project. Also if you pull phiremock repo and extend the composer.json file.

How does it work?

Phiremock will allow you to create a stubbed version of some external service your application needs to communicate to. That can be used to avoid calling the real application during development or to setup responses to expected requests. To do this, you need to trick your application to request phiremock server when on development stage or testing stage.

Setup your application's configuration

First of all you need to setup the config for the different environments for your application. For instance:

    // config/production.json
    {
        "external_service": "https://service.example.com/v1/"
    }
    // config/development.json
    {
        "external_service": "http://localhost:8080/example_service_dev/"
    }
    // config/acceptance.json
    {
        "external_service": "http://localhost:8080/example_service_test/"
    }

Configure expectations

Then, using phiremock's REST interface, expectations can be configured, specifying the response to send for a given request. A REST expectation resource for phiremock looks like this:

{
    "version": "2",
    "scenarioName": null,
    "on": {
        "scenarioStateIs": null,
        "method": { "isSameString": "GET" },
        "url": { "matches": "~^/images/~"},
        "body": null,
        "headers" : null,
        "formData": null
    },
    "then": {
        "delayMillis": 100,
        "newScenarioState": null,
        "response": {
            "statusCode": 200,
            "body": "phiremock.base64:__BASE64_ENCODED_IMAGE__",
            "headers": { "Content-Type": "image/x-icon" }
        }
    },
    "priority": 0
}

The same format can be used in expectation files saved in the directory tree specified by the --expectations-dir argument of the CLI. For Phiremock Server to be able to load them, each file should have .json extension. For instance: match-all-images.json for the previous example.

Features

Create an expectation

To create previous response from code the following should be used:

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": { "isEqualTo" : "/example_service/some/resource" },
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "{\"id\": 1, \"description\": \"I am a resource\"}",
            "headers": {
                "Content-Type": "application/json"
            }
        }
    }
}

Clear expectations

After a test runs, all previously configured expectations can be deleted so they don't affect the execution of the next test:

DELETE /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host

List all expectations

If you want, for some reason, list all created expectations. A convenient endpoint is provided:

GET /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host

Verify amount of requests

To know how much times a request was sent to Phiremock Server, for instance to verify after a feature execution in a test, there is a helper method too:

POST /__phiremock/executions HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "request": {
        "method": "GET",
        "url": {
            "isEqualTo" : "/example_service/some/resource"
        }
    }
}

Search executed requests

To search for the list of requests to which Phiremock Server responded:

PUT /__phiremock/executions HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "request": {
        "method": "GET",
        "url": {
            "isEqualTo" : "/example_service/some/resource"
        }
    }
}

Reset requests log

To reset the requests counter to 0, Phiremock Server also provides an endpoint:

DELETE /__phiremock/executions HTTP/1.1
Host: your.phiremock.host

Reset Phiremock to its initial state

This call will clean the requests list, the scenarios, delete all configured expectations and reload the static ones defined in the expectations directory.

POST /__phiremock/reset HTTP/1.1
Host: your.phiremock.host

Cool stuff

Send binary body in response

Binary contents can be sent as a response body too by encoding it as base64 in the expectation json.

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": { "isEqualTo" : "/example_service/photo.jpg" },
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "phiremock.base64:HERE_THE_BASE64_ENCODED_IMAGE",
            "headers": {
                "Content-Type": "image/jpeg"
            }
        }
    }
}

Priorities

Phiremock accepts multiple expectations that can match the same request. If no priorities are set, it will match the first expectation created but, if you need to give high priority to some request, you can do it easily.

Suppose you have the next two expectations configured:

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": { "isEqualTo": "/example_service/some/resource"}
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "<resource id=\"1\" description=\"I am a resource\"/>",
            "headers": [ "Content-Type": "text/xml" ]
        }
    }
}
POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": { "isEqualTo": "/example_service/some/resource"},
        "headers": {
            "Accept": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "{\"id\": 1, \"description\": \"I am a resource\"}",
            "headers": [ "Content-Type": "application/json" ]
        }
    },
    "priority": 10    
}

In the previous example, both expectations will match a request with url equal to: /example_service/some/resource and method GET. But Phiremock will give higher priority to the one with the Accept header equal to application/json. Default priority for an expectation is 0, the higher the number, the higher the priority.

Stateful behaviour

If you want to simulate a behaviour of the service in which a response depends of a state that was set in a previous request. You can use scenarios to create a stateful behaviour.

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "scenarioName": "saved",
    "on": {
        "scenarioStateIs": "Scenario.START",
        "method": { "isSameString": "POST" },
        "url": { "isEqualTo": "/example_service/some/resource"},
        "body": {"isEqualTo" : "{"\id": \"1\", \"name\" : \"resource\"}"},
        "headers": {
            "Accept": {"Content-Type": "application/json"}
        }
    },
    "then": {
        "newScenarioState": "RESOURCE_SAVED",
        "response": {
            "statusCode": 201,
            "body": "{"\id": \"1\", \"name\" : \"resource\"}",
            "headers": [ "Content-Type": "application/json" ]
        }
    }
}
POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "scenarioName": "saved",
    "on": {
        "scenarioStateIs": "RESOURCE_SAVED",
        "method": { "isSameString": "POST" },
        "url": { "isEqualTo": "/example_service/some/resource"},
        "body": {"isEqualTo" : "{"\id": \"1\", \"name\" : \"resource\"}"},
        "headers": {
            "Accept": {"Content-Type": "application/json"}
        }
    },
    "then": {
        "response": {
            "statusCode": 409,
            "body": "Resource with id = 1 was already created"
        }
    }
}

In this case, the first time that Phiremock Server receives a request matching the expectation, the first one will match and it will change the state of the saved scenario. From the second time the same request is executed, the second expectation will be matched. If you want after the second call, to go back to the initial state just add "newScenarioState": "Scenario.START" to the then section.

To reset all scenarios to the initial state (Scenario.START) use this simple method from the client:

DELETE /__phiremock/scenarios HTTP/1.1
Host: your.phiremock.host

To define a scenario state in any moment:

PUT /__phiremock/scenarios HTTP/1.1
Host: your.phiremock.host

{
    "scenarioName": "saved",
    "scenarioState": "Scenario.START"
}

Netwok latency simulation

If you want to test how your application behaves on, for instance, a timeout; you can make Phiremock to delay the response of your request as follows.

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": { "isEqualTo": "/example_service/some/resource"},
        "headers": {
            "Accept": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "delayMillis": 30000,
        "response": {
            "statusCode": 200
        }
    }
}

This will cause Phiremock Server to wait 30 seconds before sending the response.

Proxy

It could be the case that a mock is not needed for certain call. For this specific case, Phiremock provides a proxy feature that will pass the received request unmodified to a configured URI and return the real response from it. It can be configured as folows:

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "POST" },
        "url": { "isEqualTo": "/example_service/some/resource"},
        "headers": {
            "Accept": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "proxyTo": "http://your.real.service/some/path/script.php"
    }
}

In this case, Phiremock will POST http://your.real.service/some/path/script.php with the configured body and header and return it's response.

Compare JSON objects

Phiremock supports comparing strict equality of json objects, in case it's used in the API. The comparison is object-wise, so it does not matter that indentation or spacing is different.

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "body": { "isSameJsonObject": "{\"some\": \"json\", \"here\": [1, 2, 3]}"},
        "headers": {
            "Content-Type": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "response": {
            "statusCode": 201,
            "body": "{\"id\": 1}"
        }
    }
}

Generate response based in request data

It could happen that you want to make your response dependent on data you receive in your request. For this cases you can use regexp matching for request url and/or body, and access the subpatterns matches from your response body specification using ${body.matchIndex} or ${url.matchIndex} notation.

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": {"matches": "~^/example_service/(\w+)/?id=(\d+)~"}
        "body": { "matches": "~\{\"name\" : \"([^\"]+)\"\}~" },
        "headers": {
            "Content-Type": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "The resource is ${url.1}, the id is ${url.2} and the name is ${body.1}",
            "headers": {"X-id": "id is ${url.2}"}
        }
    }
}

Also retrieving data from multiple matches is supported:

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": {"matches": "~/peoples-brothers-list/json~"}
        "body": { "matches": "%\"name\"\s*:\s*\"([^\"]*)",\s*\"brothers\"\s*:\s*(\d+)%" },
        "headers": {
            "Content-Type": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "${body.1} has ${body.2} brothers, ${body.1.2} has ${body.2.2} brothers, ${body.1.3} has ${body.2.3} brothers"
        }
    }
}

This is also supported to generate the proxy url as shown in the following example:

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "isSameString": "GET" },
        "url": {"matches": "~^/example_service/(\w+)~"}
        "headers": {
            "Content-Type": {"isEqualTo": "application/json"}
        }
    },
    "then": {
        "proxyTo": "https://some.other.service/path/${url.1}"
    }
}

Conditions based in form data

For requests encoded with application/x-www-form-urlencoded and specifying this Content Type in the headers. Phiremock Server is capable of execute conditions on the values of the form fields.

POST /__phiremock/expectations HTTP/1.1
Host: your.phiremock.host
Content-Type: application/json

{
    "version": "2",
    "on": {
        "method": { "matches": "~POST|PUT~" },
        "url": {"isEqualTo": "/login-form-handler"}
        "formData": {
            "username": {"isEqualTo": "the_username"},
            "password": {"isEqualTo": "the_password"},
        }
    },
    "then": {
        "response": {
            "statusCode": 200,
            "body": "Login successful"
        }
    }
}

Backwards compatibility

Phiremock Server still supports expectations in the format of Phiremock V1. This should make your migration from Phiremock v1 to Phiremock v1 (phiremock-server + phiremock-client) easier.

{
    "scenarioName": "potato",
    "scenarioStateIs": "Scenario.START",
    "newScenarioState": "tomato",
    "request": {
        "method": "GET",
        "url": {
            "isEqualTo": "/some/thing"
        },
        "headers": {
            "Content-Type": {
                "isEqualTo": "text/plain"
            }
        }
    },
    "response": {
        "statusCode": 200,
        "body": "Hello world!",
        "headers": {
            "Content-Type": "text/plain"
        }
    },
    "priority": 1
}

Appendix

List of condition matchers:

  • contains: Checks that the given section of the http request contains the specified string.
  • isEqualTo: Checks that the given section of the http request is equal to the one specified, case sensitive.
  • isSameString: Checks that the given section of the http request is equal to the one specified, case insensitive.
  • matches: Checks that the given section of the http request matches the regular expression specified.
  • isSameJsonObject: Checks that json received in the request is the same as a given JSON.

See also

Contributing:

Just submit a pull request. Don't forget to run tests and php-cs-fixer first, and write documentation.

Thanks to:

And everyone who submitted their Pull Requests.