Skip to content

Instantly share code, notes, and snippets.

@al-the-x
Last active August 29, 2015 14:04
Show Gist options
  • Save al-the-x/691f440c0edfe6a0485f to your computer and use it in GitHub Desktop.
Save al-the-x/691f440c0edfe6a0485f to your computer and use it in GitHub Desktop.
I get tired of writing this simple `FunctionalTestCase` over and over again. I should submit to @sbergmann for PHPUnit...
<?php
/**
* A "functional" test is typically one that runs the full application through it's
* paces: constructing a request, triggering a route, generating a response, and
* testing the fully rendered output. This abstract class provides some assertions
* appropriate for testing requests and responses and abstract methods for actually
* fetching requests and responses and routing the application. The developer should
* extend this class for his or her specific framework or application.
*/
abstract class FunctionalTestCase extends PHPUnit_Framework_TestCase
{
static $HTTP_METHODS = [ 'get', 'post', 'put', 'delete', 'head', 'options', 'patch' ];
/**
* Override in framework-specific classes to check the status of the response,
* as the API for doing so may differ between frameworks.
*
* @param integer $expected status code
* @param string|null $message to display on `fail()`
*/
function assertStatus($expected, $message = null){
$this->assertEquals($expected, $this->response()->getStatus(), $message);
}
/**
* Override in framework-specific classes to check a redirection in the response,
* as the API for doing so may differ between frameworks.
*
* @param string $expected URL for the redirect response
* @param integer $status code of the redirect, `302` by default
* @param string|null $message to display on `fail()`
*/
function assertRedirect($expected, $status = 302, $message = null){
$this->assertStatus($status, $message);
$this->assertEquals($expected, $this->responseHeaders('location'), $message);
}
/**
* @param string $expected XPath query
* @param integer $count to test against query
* @param string|null $message to display on `fail()`
*/
function assertXPath($expected, $count = 1, $message = null){
$query = $this->getDOMXPath()->query($expected);
$this->assertEquals($count, $query->length, $message);
}
/**
* @param string $expected XPath query
* @param string|null $message to display on `fail()`
*/
function assertNotXpath($expected, $message = null){
$this->assertXPath($expected, 0, $message);
}
/**
* Capture and return any output of `$callback` via output buffering...
*
* @param Closure $callback to invoke
* @return string captured output
*/
function capture(Closure $callback){
ob_start(); $callback(); return ob_get_clean();
}
/** -- Framework-specific Utility Functions -- **/
/**
* Return an appropriate "application instance" for each framework, e.g. `\Slim\Slim` for Slim
* or `Zend_Controller_Front` for ZF1. If there's a need to `$refresh` this instance, perhaps
* between tests, this method should support it. If the `$refresh` parameter is NOT passed, the
* instance should be cached for future invocations.
*
* @param boolean $refresh whether to (re)instantiate the application instance
* @return object representing the application instance
*/
abstract function app($refresh = false);
/**
* Return an appropriate "request instance" for each framework, e.g. `\Slim\Http\Request` or
* `Zend_Http_Request` (ZF1). If there's a need to `$refresh` this instance, perhaps between
* tests, this method should support it. If the `$refresh` parameter is NOT passed, the instance
* should be cached for future invocations, probably on the `app()` instance.
*
* @param boolean $refresh whether to clear / rebuild the request instance
* @return object representing the request instance
*/
abstract function request($refresh = false);
/**
* Get / set the request headers on the request instance appropriate to the application
*
* @param array $headers to set on the request
* @return mixed representation of the request headers
*/
abstract function requestHeaders(array $headers = null);
/**
* Return an appropriate "response instance" for each framework, e.g. `\Slim\Http\Response` or
* `Zend_Http_Response` (ZF1). If there's a need to `$refresh` this instance, perhaps between
* tests, this method should support it. If the `$refresh` parameter is NOT passed, the instance
* should be cached for future invocations, probably on the `app()` instance.
*
* @param boolean $refresh whether to clear / rebuild the response instance
* @return object representing the response instance
*/
abstract function response($refresh = false);
/**
* Get the headers from the response in whatever format makes sense for the application. Note
* that there's no setter for response headers, as these should be a side-effect of routing the
* application. Optionally, providing the name of a response header should return that specific
* header value.
*
* @param string $header name to retrieve
* @return mixed representation of the response headers appropriate to the application or a specific header value
*/
abstract function responseHeaders($header = null);
/**
* Get the body of the response as a STRING for examination and use in XPath queries.
*
* @return string body of the response
*/
abstract function responseBody();
/**
* Invoke the application route with a mock / constructed request with the provided `$query` params
* and `$headers`. This method is a command and should only return `$this` for method chaining.
*
* @param string $method to route, e.g. GET, POST, PUT, DELETE
* @param string $path to route, e.g. "/path/to/resource", "/answer/42"
* @param array $query params to provide via the request
* @param array $headers to set on the request
* @return \FunctionalTestCase instance for method chaining, if appropriate
*/
abstract function route($method, $path, array $query = [ ], array $headers = [ ]);
/**
* Shortcuts for `route()` based on recognized HTTP_METHODS, e.g. `$this->post('/path/to/resource', $data)`.
*
* @param string $method called against $this instance
* @array $arguments provided to $method, if any
* @throw \BadMethodCallException if $method isn't in HTTP_METHODS
*/
function __call($method, array $arguments = [ ]){
if ( ! in_array($method, static::$HTTP_METHODS) ){
throw new \BadMethodCallException('Requested method does not exist: ' . $method);
}
list($path, $params, $headers) = array_pad($arguments, 3, [ ]);
return $this->route($method, $path, $params, $headers);
}
/** -- XPath Utility Methods -- **/
/**
* @param boolean $refresh whether to refresh the DOMDocument instance
* @return \DOMDocument instance constructed from $this->response()
*/
function getDOMDocument($refresh = false){
static $instance;
if ( $refresh or !$instance ){
$instance = $instance ?: new DOMDocument;
// DOMDocument::loadHTML complains if passed an empty string...
$this->responseBody() and $instance->loadHTML($this->responseBody());
}
return $instance;
}
/**
* @param boolean $refresh whether to refresh the DOMXPath instance
* @return \DOMXPath instance constructed from $this->response()
*/
function getDOMXPath($refresh = false){
static $instance;
if ( $refresh or !$instance){
$instance = new DOMXPath($this->getDOMDocument($refresh));
}
return $instance;
}
} // END FunctionalTestCase
<?php
abstract class SlimFunctionalTestCase extends FunctionalTestCase
{
/**
* @param boolean $refresh
* @return \Slim\Slim
*/
function app($refresh = false){
static $app;
$refresh and $app = app();
return $app ?: app();
}
/**
* `\Slim\Environment::mock()` is used to signal the `\Slim\Router` to
* dispatch a particular `\Slim\Route`.
*
* @param array $overrides to apply to the defaults
* @return \Slim\Environment
*/
function env(array $overrides = [ ]){
return \Slim\Environment::mock($overrides);
}
/**
* @param boolean $refresh
* @return \Slim\Http\Request
*/
function request($refresh = false){
return $this->app()->request();
}
/**
* @param array $headers to set
* @return \Slim\Http\Headers for the request
*/
function requestHeaders(array $headers = null){
if ( ! is_null($headers) ){
$this->request()->headers()->replace($headers);
}
return $this->request()->headers();
}
/**
* @param boolean $refresh
* @return \Slim\Http\Response
*/
function response($refresh = false){
return $this->app()->response();
}
/**
* @param string $header to return instead of ALL headers
* @return mixed \Slim\Http\Headers for the response or string $header value
*/
function responseHeaders($header = null){
$headers = $this->response()->headers();
if ( is_null($header) ){
return $headers;
}
// Technically this works, too, but only in later PHPs:
// return $this->response()->headers()[$header];
return $headers[$header];
}
/**
* @param string $method to route, e.g. POST, GET
* @param string $path to route, e.g. '/path/to/resource'
* @param array $query params to provide via the request
* @param array $hedaers to set on the request
* @return \FunctionalTestCase
*/
function route($method, $path, array $query = [ ], array $headers = [ ]){
// Slim extracts almost everything from the `\Slim\Environment`...
$this->env($options = array_merge([
'REQUEST_METHOD' => strtoupper($method),
'PATH_INFO' => $path,
'SERVER_NAME' => 'testing',
'slim.input' => http_build_query($params)
], $headers));
$app = $this->app(true);
// Because `\Slim\Slim::run()` produces output...
$this->capture(function() use ($app){
$app->run();
});
$this->getDOMXPath(true);
return $this;
}
} // END SlimFunctionalTestCase
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment