Skip to content

Instantly share code, notes, and snippets.

@lesichkovm
Forked from mindplay-dk/TestServer.php
Created September 16, 2019 05:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lesichkovm/32f6c7fb117ba380ced7c994eb4a6a37 to your computer and use it in GitHub Desktop.
Save lesichkovm/32f6c7fb117ba380ced7c994eb4a6a37 to your computer and use it in GitHub Desktop.
Minimal (copy and paste) unit testing

Quick and simple unit-testing without a test-framework.

I wanted a simple, single-file unit-test that I can package and distribute with components - and I agree with Go language philosophy of testing and their non-framework approach.

There's nothing to deploy or integrate here - it's just a few global functions, and you should feel free to modify these and tailor them specifically to suit the test-requirements for your solution.

Runs in a browser or on the command-line.

See this project for real-world use and integration with Travis CI.

Command-line output from example.php looks something like this:

> php.exe /path/to/my/test.php

=== Describe your test here ===

- PASS: OK
- PASS: because it's true
- PASS: TRUE
- PASS: just because (TRUE)
- PASS: because the bar() method will throw (RuntimeException)

Process finished with exit code 0

Code Coverage

To implement code coverage reporting, import the php-code-coverage package from your composer.json:

...
    "require-dev": {
        "phpunit/php-code-coverage": "2.0.*@dev"
    }
...

Be sure to include the Composer autoloader in your test-script:

/** @var \Composer\Autoload\ClassLoader $autoloader */
$autoloader = require __DIR__ . '/vendor/autoload.php';

You may also wish to configure the autoloader with your namespace.

Before doing your tests, initialize the code coverage run-time:

if (coverage()) {
    $filter = coverage()->filter();
    
    // whitelist the files to cover:
    
    $filter->addFileToWhitelist(__DIR__ . '/src/Foo.php');
    $filter->addFileToWhitelist(__DIR__ . '/src/Bar.php');
    
    coverage()->setProcessUncoveredFilesFromWhitelist(true);
    
    // start code coverage:
    
    coverage()->start('test');
}

Now do your tests, and then finally output the results:

if (coverage()) {
    // stop code coverage:
    
    coverage()->stop();

    // output code coverage report to console:
    
    $report = new PHP_CodeCoverage_Report_Text(10, 90, false, false);

    echo $report->process(coverage(), false);
    
    // output code coverage report for integration with CI tools:
    
    $report = new PHP_CodeCoverage_Report_Clover();

    $report->process(coverage(), __DIR__ . '/build/logs/clover.xml');
}

Be sure to copy/paste the coverage() function from below.

Strict Error Handling

If you wish to relay all PHP errors/warnings/notices to exceptions, add the following:

set_error_handler(function ($errno, $errstr, $errfile, $errline) {
    $error = new ErrorException($errstr, 0, $errno, $errfile, $errline);

    if ($error->getSeverity() & error_reporting()) {
        throw $error;
    }
});
<?php
require __DIR__ . '/test.php';
class Foo
{
public function bar()
{
throw new RuntimeException('blam!');
}
public $baz = true;
}
header('Content-type: text/plain');
test(
'Describe your test here',
function () {
$foo = new Foo();
ok($foo->baz === true);
ok($foo->baz != false, "because it's true");
eq($foo->baz, true);
eq($foo->baz, true, "just because");
expect(
'RuntimeException',
"because the bar() method will throw",
function () use ($foo) {
$foo->bar(); // will throw
}
);
}
);
exit(status()); // exits with errorlevel (for CI tools etc.)
<?php
// https://gist.github.com/mindplay-dk/4260582
/**
* Describe and run a test
*
* @param string $title test title (short, concise description)
* @param callable $function test implementation
*
* @return void
*/
function test($title, $function)
{
echo "\n=== $title ===\n\n";
try {
call_user_func($function);
} catch (Exception $e) {
ok(false, "UNEXPECTED EXCEPTION", $e);
}
}
/**
* Check the result of an expression.
*
* @param bool $result result of assertion (must === TRUE)
* @param string $why description of assertion
* @param mixed $value optional value (displays on failure)
*
* @return void
*/
function ok($result, $why = null, $value = null)
{
$trace = trace();
if ($trace) {
$trace = "[{$trace}] ";
}
if ($result === true) {
echo "- PASS: {$trace}" . ($why ?: 'OK') . ($value === null ? '' : ' (' . format($value) . ')') . "\n";
} else {
echo "# FAIL: {$trace}" . ($why ?: 'ERROR') . ($value === null ? '' : ' - ' . format($value, true)) . "\n";
status(false); // mark test as failed
}
}
/**
* Compare an actual value against an expected value.
*
* @param mixed $value actual value
* @param mixed $expected expected value (must === $value)
* @param string $why description of assertion
*
* @return void
*/
function eq($value, $expected, $why = null)
{
$result = $value === $expected;
$info = $result
? format($value)
: "expected: " . format($expected, true) . ", got: " . format($value, true);
ok($result, ($why === null ? $info : "$why ($info)"));
}
/**
* Check for an expected exception, which must be thrown.
*
* @param string $exception_type Exception type name (use `ClassName::class` syntax where possible)
* @param string $why description of assertion
* @param callable $function function expected to cause the exception
*
* @void
*/
function expect($exception_type, $why, $function)
{
try {
call_user_func($function);
} catch (Exception $e) {
if ($e instanceof $exception_type) {
ok(true, $why, $e);
} else {
$actual_type = get_class($e);
ok(false, "$why (expected $exception_type but $actual_type was thrown)");
}
return;
}
ok(false, "$why (expected exception $exception_type was NOT thrown)");
}
/**
* Format a value for display (for use in diagnostic messages)
*
* @param mixed $value
* @param bool $verbose
*
* @return string formatted value
*/
function format($value, $verbose = false)
{
if ($value instanceof Exception) {
return get_class($value) . ($verbose ? ": \"" . $value->getMessage() . "\"" : '');
}
if (!$verbose && is_array($value)) {
return 'array[' . count($value) . ']';
}
if (is_bool($value)) {
return $value ? 'TRUE' : 'FALSE';
}
if (is_object($value) && !$verbose) {
return get_class($value);
}
return print_r($value, true);
}
/**
* Track or report the status of a test.
*
* The end of your test should exit with a status code for continuous integration, e.g.:
*
* exit(status());
*
* @param bool|null $status test status
*
* @return int number of failures
*/
function status($status = null)
{
static $failures = 0;
if ($status === false) {
$failures += 1;
}
return $failures;
}
/**
* Return a shared instance of a code coverage tracking/reporting service.
*
* @link https://packagist.org/packages/phpunit/php-code-coverage
*
* @return PHP_CodeCoverage|null code coverage service, if available
*/
function coverage()
{
static $coverage = null;
if ($coverage === false) {
return null; // code coverage unavailable
}
if ($coverage === null) {
if (!class_exists('PHP_CodeCoverage')) {
echo "# Notice: php-code-coverage not installed\n";
$coverage = false;
return null;
}
try {
$coverage = new PHP_CodeCoverage;
} catch (PHP_CodeCoverage_Exception $e) {
echo "# Notice: no code coverage run-time available\n";
$coverage = false;
return null;
}
}
return $coverage;
}
/**
* Invoke a protected or private method (by means of reflection)
*
* @param object $object the object on which to invoke a method
* @param string $method_name the name of the method
* @param array $arguments arguments to pass to the function
*
* @return mixed the return value from the function call
*/
function invoke($object, $method_name, $arguments = array())
{
$class = new ReflectionClass(get_class($object));
$method = $class->getMethod($method_name);
$method->setAccessible(true);
return $method->invokeArgs($object, $arguments);
}
/**
* Inspect a protected or private property (by means of reflection)
*
* @param object $object the object from which to retrieve a property
* @param string $property_name the property name
*
* @return mixed the property value
*/
function inspect($object, $property_name)
{
$property = new ReflectionProperty(get_class($object), $property_name);
$property->setAccessible(true);
return $property->getValue($object);
}
/**
* Obtain a filename and line number index of a call made in a test-closure
*
* @return string|null formatted file/line index (or NULL if unable to trace)
*/
function trace() {
$traces = debug_backtrace();
$skip = 0;
$found = false;
while (count($traces)) {
$trace = array_pop($traces);
if ($skip > 0) {
$skip -= 1;
continue; // skip closure
}
if ($trace['function'] === 'test') {
$skip = 1;
$found = true;
continue; // skip call to test()
}
if ($found && isset($trace['file'])) {
return basename($trace['file']) . '#' . $trace['line'];
}
}
return null;
}
<?php
/**
* This class will launch the built-in server in PHP 5.4+ in the background
* and clean it up after use.
*/
class TestServer
{
/**
* @var resource PHP server process handle
*/
protected $proc;
/**
* @var resource[] indexed array of file-pointers for the open PHP server process
*/
protected $pipes;
/**
* Launch the built-in PHP server as a child process.
*
* @param string|null $host local host name or IP (defaults to "127.0.0.1")
* @param int $port local port number (defaults to 8000)
* @param string|null $path root folder from which the PHP server child process should serve scripts
* (defaults to current working directory)
*/
public function __construct($host = null, $port = 8000, $path = null)
{
if ($host === null) {
$host = '127.0.0.1';
}
if ($path === null) {
$path = getcwd();
}
$descriptorspec = array(
0 => array('pipe', 'r'), // stdin
1 => array('pipe', 'w'), // stdout
2 => array('pipe', 'a') // stderr
);
$cmd = "php -S {$host}:{$port} -t {$path}";
$this->proc = proc_open($cmd, $descriptorspec, $this->pipes);
}
/**
* Shut down the PHP server child process
*/
public function __destruct()
{
fclose($this->pipes[0]);
fclose($this->pipes[1]);
if (stripos(php_uname('s'), 'win') > -1) {
$status = proc_get_status($this->proc);
exec("taskkill /F /T /PID {$status['pid']}");
} else {
proc_terminate($this->proc);
}
proc_close($this->proc);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment