Because we can. For the Lulz. Whatever.
This is a memo, no implementation exists (yet). I might write a proof-of-concept if I ever get to it. Feel free to roll your own and contact me.
Use PHP code to define an API and behaviour, then render a JS file from that abstract definition and load it into an express.js app. Control the mock app from inside the PHP test case (start, stop).
Probably quite slow. Child process control from PHP is relatively poor. How to ensure forked child get's killed when PHP process dies unexpectedly?
Testcase:
$server = new MockServer();
$server->get('/')->responds(200)
->withHeader('Content-type', 'json')
->withJson(['message' => OK]);
$server->start();
// do HttpSocket/curl/file_get_contents
$server->kill();
Building an abstract model of the generated app. Mock server, handler, response, etc.:
class MockServer() {
// Build app model (code in $server->get()):
public function get($path) {
$handler = new Handler('/');
$handler->response = new Response();
$this->handlers[] = $handler;
return $handler;
}
// similar in $server->post(), etc.
// Generate the nodejs module containing the mocked API code
function generateApp() {
ob_start();
include('template.php');
$code = ob_get_contents();
ob_end_clean();
file_put_contents('__mock_express_app.js', $code);
}
}
class MockHandler() {
function responds($statusCode) {
$this->response = new Response($statusCode);
return $this->response();
}
}
class MockResponse() {
// Response has ::withHeader(), ::withJson(), etc.
}
App template (template.php):
// require express, etc.
foreach($this->handlers as $handler) {
echo "app.get({$handler->path}, function(req, res) {";
echo "res.status({$handler->response->status()})";
foreach($handler->headers as $header) {
echo "res.header({$header->name, $header->value})";
}
if ($handler->isJson()) {
echo "res.json(json_encode($handler->json()))";
} else {
echo "res.body($handler->body())";
}
echo "})";
}
Generated express app in __mock_express_app.js
should look something like below. Exports a router that can be
mounted into the main app.
var express = require('express');
var _myApp = module.exports = express.router();
// this is the expected result for above mocking code
_myApp.get('/', function(req, res, next) {
res.status(200);
res.header('Content-type', 'json');
res.json({message: 'OK'});
});
Setting up the server. Load the generated module and mount it into an simple express app, start it. Port is injected via env.
var express = require('express');
var mockedApp = require(process.env.APP_FILE);
var app = express();
app.use(bodyParser.json()); // default parser
app.use(morgan()); // for debugging, log requests
app.use(mockedApp);
app.use(notFound); // default 404 catchall
app.use(errorHandler); // should do something smart to report the error back to the wrapper script
app.listen(process.env.PORT || 12345, function() {
console.log('MOCK_SERVER_IS_UP');
});
Starting and stopping the server process from PHP looks roughly like below. Start node in the background via exec()
, use
pid to terminate it. Only problem is that the node.js doesn't get killed when the parent PHP process terminates, so it needs
to be shutdown explicitly.
// in class Server:
public function boot() {
$this->pidFile = "/tmp/{$this->sessId}.pid";
$this->outFile = "/tmp/{$this->sessId}.out";
$dir = dirname(__FILE__);
$cmds = array(
"cd $dir",
"npm install > /dev/null 2>&1",
"PORT={$this->port} node app.js > {$this->outFile} 2>&1 & echo $! > {$this->pidFile}"
);
exec(implode(' && ', $cmds));
// wait until we get the OK from the API server
for($i = 0; true; $i++) {
if (preg_match('/MOCK_SERVER_IS_UP/', $this->output())) {
break;
} else if ($i > $this->bootTimeOut) {
throw new Exception('API server timeout');
} else {
sleep(1);
}
}
}
public function kill() {
posix_kill($this->pid(), SIGKILL);
unlink($this->pidFile);
unlink($this->outFile);
}