elazar/phantestic

A small PHP testing framework

0.3.0 2015-10-21 23:30 UTC

This package is not auto-updated.

Last update: 2020-09-18 20:31:57 UTC


README

A small PHP testing framework that aims to be simple, fast, modern, and flexible.

Currently in a very alpha state. Feel free to mess around with it, but expect things to break.

Build Status Code Climate

Installation

Use Composer.

{
    "require-dev": {
        "elazar/phantestic": "^0.2"
    }
}

Components

A test loader loads the tests to be run. It can be anything that implements \Traversable (i.e. an instance of a class that implements \IteratorAggregate or \Iterator, such as \Generator) to allow loaded tests to be iterable.

The test runner uses the test loader to load tests, run them, and in the process emit multiple events that test handlers can intercept and act upon.

As an example, LocalRunner runs tests in the same PHP process as the test runner. Its constructor accepts two arguments: the test loader to use and an array of test handler objects that implement HandlerInterface.

When its run() method is called, LocalRunner handles injecting an event emitter into the test handler objects, which enables those objects to register callbacks with the emitter for any events that may be relevant to them.

An example of a test handler is CliOutputHandler, which outputs the results of executing tests to stdout as they are received and a failure summary once all tests have been run.

Configuring a Runner

There is no stock test runner; one must be configured based on the needs of your project.

Here's a sample runner configuration.

$classmap_path = '../vendor/composer/autoload_classmap.php';
$loader = new \Phantestic\Loader\ClassmapFileObjectLoader($classmap_path);
$handlers = [ new \Phantestic\Handler\CliOutputHandler ];
$runner = new \Phantestic\Runner\LocalRunner($loader, $handlers);
$runner->run();

ClassmapFileObjectLoader locates tests based on the contents of a classmap file, such as the one generated by the -o flag of several Composer commands. By default, it looks for class files with names ending in Test.php, instantiates the classes, and invokes methods with names prefixed with test. Callbacks used to filter test methods based on file, class, and method names and to generate test case objects can be changed using the constructor parameters of ClassmapFileObjectLoader.

Writing Tests

Theoretically, tests can be anything callable. The test loader may restrict this to specific types of callables (e.g. ClassmapFileObjectLoader only supports instance methods). The test loader wraps test callbacks in an instance of a class implementing TestInterface, such as the default Test implementation.

Failures can be indicated by throwing an exception. Other statuses can be indicated by throwing an instance of a subclass of Result. Test converts errors to exceptions and considers any uncaught exception to indicate failure. Likewise, no exception being thrown indicates success.

// src/Adder.php
class Adder
{
    public function add($x, $y)
    {
        return $x + $y;
    }
}

// tests/AdderTest.php
class AdderTest
{
    public function testAdd()
    {
        $adder = new Adder;
        $result = $adder->add(2, 2);
        if ($result != 4) {
            throw new \RangeException('2 + 2 should equal 4');
        }
    }
}

Writing Test Handlers

Test handlers implement HandlerInterface, which has a single method: setEventEmitter(). This method receives an instance of EventEmitterInterface as its only argument. Within its implementation of setEventEmitter(), a test handler can use this argument to register event callbacks. An example of this is below, taken from CliOutputHandler, which registers its own methods as callbacks for several events.

public function setEventEmitter(EventEmitterInterface $emitter)
{
    $emitter->on('phantestic.test.failresult', [$this, 'handleFail']);
    $emitter->on('phantestic.test.passresult', [$this, 'handlePass']);
    $emitter->on('phantestic.tests.before', [$this, 'beforeTests']);
    $emitter->on('phantestic.tests.after', [$this, 'afterTests']);
}

Supported events may vary depending on the test loader and runner in use.

LocalRunner

  • phantestic.tests.before: Before any tests are run, with the test runner as an argument
  • phantestic.tests.after: After all tests are run, with the test runner as an argument
  • phantestic.test.before: Before each test, with the test case and runner as arguments
  • phantestic.test.after: After each test, with the test case and runner as arguments
  • phantestic.test.failresult: When a test fails, with the test case and runner as arguments
  • phantestic.test.passresult: When a test passes, with the test case and runner as arguments
  • phantestic.test.RESULT: When a test has a result other than passing or failing where RESULT is the short name of the class extending Result, with the test case and runner as arguments

ClassmapFileObjectLoader

  • phantestic.loader.loaded: When a test case is loaded, with the test case and associated test class name and test method name as arguments

Differences from PHPUnit

Tests may be instance methods of classes, but they don't have to be since individual tests can be anything callable. If you do choose to use instance methods for tests:

  • There is no equivalent to PHPUnit_Framework_TestCase. You may create your own base class if you wish, but it is not required.
  • There are no equivalents to setUp() or tearDown(). Consider using either __construct() and __destruct() or test handlers in conjunction with a loader that supports phantestic.test.before and phantestic.test.after.
  • If you wish for tests to be located using the same criteria as PHPUnit and you're using Composer, just use ClassmapFileObjectLoader and specify only the classmap file path when instantiating it.
  • Assertions are merely methods that throw exceptions if expected conditions are not met. Consider supplementary libraries like those recommended for use with Peridot.
  • Mocking is not supported natively. Consider supplementary libraries like Phake or Mockery.
  • Database seeding and assertions are not supported natively. Consider supplementary libraries like Phactory or Faker.