This is a pre-release. Public interfaces are not guaranteed to be backwards compatible between builds, and test coverage is currently limited.
This project provides a lightweight HTTP server for designed to support black-box and integration testing of code that needs to communicate with HTTP APIs.
The emulator runs as a docker container. By default, it answers on port 80 - customise this with the PORT
environment
variable if required.
All http endpoints that accept data (handlers and emulator management endpoints) automatically support an incoming Content-Type of either application/json or application/x-www-form-urlencoded.
The emulator includes a simple regex-based routing layer. This dispatches incoming requests to handlers.
By default, the emulator defines handlers for:
HTTP method | URL | Description |
---|---|---|
* | /ping-200(/{anything}) | Answers every request with a 200 status and a text/plain response "OK". Useful if your code doesn't care about the response content. |
POST | /ses/v2/email/outbound-emails | Stubs the Amazon SES v2 SendEmail API to return a valid response with a random message ID. |
POST | /sendgrid/v3/mail/send | Stubs the Sendgrid v3 send API to return a valid response with a random message ID. |
You can add custom handlers by mounting (or building) them into the docker container and registering them with the emulator. A handler is just a simple callable registered against a regex that matches the HTTP method & URL of the incoming request. The simplest setup is to define this all in one config file, like so:
# my-project-handlers.php
return [
// If you provide a custom handlers file this will replace the handlers that ship with the emulator. If you want them
// to be available as well, just merge the emulator's own config file into yours like so:
...require '/api_emulator/default_handlers/handlers.php',
// Then define your own handler here. The pattern will be matched against the complete URL, including any querystring.
'#^POST /hello-world$#' => function (
\Psr\Http\Message\ServerRequestInterface $request,
Ingenerator\ApiEmulator\HandlerRequestContext $context
) : \Psr\Http\Message\ResponseInterface {
// try to keep handlers as simple as possible. Note that the Guzzle\Psr7\Response class here is provided by
// the emulator itself.
return new \Guzzle\Psr7\Response(
200,
['Content-Type' => 'text/plain'],
'Hi '.$request->getParsedBody()['username']
);
},
];
Then run the container like so:
docker run \
-e HANDLERS_FILE=/my-project/my-project-handlers.php \
-v "$PWD/api_emulator":/my-project \
-p 8080:80
ghcr.io/ingenerator/api_emulator:main
And then you can speak to it like:
> curl -XPOST -d "{"username": "Brian"} -H "Content-Type:application/json" http://127.0.0.1:8080/hello-world
The location of the mounted handlers is entirely arbitrary, so long as your HANDLERS_FILE environment variable points to the correct path.
The "all-in-one-file" approach obviously doesn't scale very well. For all but the simplest projects we recommend
extracting the handlers to individual files, or grouping them for each API, and using normal PHP require
and ...
operations to merge them all into your config file at runtime. For example, the way that the script above merges in
the emulator's default handlers.
You may want to extract helpers and shared code to keep your handlers simple. So long as you mount / build them into
the emulator alongside your handlers you can require
them in like any other PHP code. Note that there is no way to
add paths to the emulator's own autoloader : just require files manually where you need them.
The emulator automatically captures the full details (method, uri, headers, parsed body) for all incoming requests
(except any to the management interface under /_emulator-meta
).
To retrieve the request details, call GET /_emulator-meta/requests
. This will return JSON like:
[
{
"id": "2023-08-24-10-57-11-680239",
"handler_pattern": "#^\\w+ /ping-200$#",
"uri": "http://api-emulator-http:9000/ping-200?customer_id=219204",
"method": "POST",
"headers": {
"Host": [
"api-emulator-http:9000"
],
"User-Agent": [
"Guzzle"
],
"Content-Length": [
"242"
]
},
"parsed_body": {
"email": "brian@foo.test",
"name": "Brian",
"categories": [
"Customer",
"User"
]
}
}
]
Note that - as with the PSR Request objects - each key in the headers
object is an array of header lines. This
will usually only contain a single entry, but that is not guaranteed as HTTP Header lines are not required to be unique.
Handlers should be as simple as possible, ideally requiring predictable stub responses without any need for runtime setup.
However, sometimes you won't be able to avoid making them stateful. For example:
- You might be calling an API with multiple methods that have to be called in sequence, where the second response depends on values that came in the first request.
- Your test code might need to populate non-default status or content of an entity as part of test setup.
The emulator provides a simple data repository to facilitate this.
Use the data repository passed in to your handler as part of the HandlerRequestContext argument:
use Guzzle\Psr7\Response;
use Ingenerator\ApiEmulator\HandlerRequestContext;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
return [
'#^POST /users$#' => function(ServerRequestInterface $request, HandlerRequestContext $context): ResponseInterface {
$id = uniqid();
$context->data_repository->save('/users/'.$id, ['name' => $request->getParsedBody()['name'] ]);
return new \GuzzleHttp\Psr7\Response(200, ['Content-Type'=> 'application/json'], json_encode(['id' => $id]));
},
'#^GET /users/.+$#' => function(ServerRequestInterface $request, HandlerRequestContext $context): ResponseInterface {
$id = basename($request->getUri());
if ($context->data_repository->hasPath('users/'.$id)) {
$data = $context->data_repository->load('/users/'.$id);
return new Response(200, ['Content-Type'=> 'application/json'], json_encode(['name' => $data['name']]));
} else {
return new Response(404, );
}
},
]
The emulator also provides an external HTTP interface for managing test data:
- POST to
/_emulator-meta/handler-data/{path}
to store data. - DELETE to
/_emulator-meta/handler-data/{path}
to remove it.
For example, to seed the data for the handlers shown above, you could run:
# Set the data using the emulator's built-in endpoint
curl -X POST \
-d '{"name": "Brian"}' \
http://127.0.0.1:8080/_emulator-meta/handler-data/users/81237
# The custom handler can now read it - this will print `{"name": "Brian"}`
curl http://127.0.0.1:8080/users/81237
You will usually want to reset the emulator's state for each new testcase. Send DELETE /_emulator-meta/global-state
to reset both the handler data repository and the list of captured requests.
There is a healthcheck endpoint at GET /_emulator-meta/health
- you can use this e.g. for a k8s healthcheck, or
to have your test suite wait until the emulator is ready before starting tests.