I've added a simple, trait-like way of adding new features to test cases and fixture plug-ins. This helps to write cleaner, easy-to-read tests.
You can mix and match tools for different domains in ways which are impossible with standard SimpleTest. Need to test a shell script which writes to a file? Just mix in the filesystem fixture tools with the shell test tools — and then add any number of custom fixtures, assertions or etc of your own on top. Everything can be re-used in other tests in any combination, as required.
Note that AperiTestCase exists a level down in the hierarchy. You probably won't want to use it directly. Extend AperiUnitTest to create a unit test. Web tests are derived from AperiWebTest. All the usual web test / unit test tools are available as well as a few extras.
The following table lists all the extra methods added to the standard SimpleTest set.
mixin($decorator) - Adds $decorator's methods to the test case (ie they are callable as if they were methods of the test case class). Note that decorators don't have to implement any particular interface. Mixin anything you like.
addFixture($fixture) - Fixture objects hook in to the setUp/ tearDown cycle. Optionally implement any of the methods: skip, setUp, or tearDown. They will be called at the appropriate time. Fixtures are also test case decorators ie all methods of the fixture object become callable as if they were methods of the test case. (This is actually an alias of mixin).
addNonDecoratingFixture($fixture) - Plug the object into the setUp/tearDown cycle but do not inject any methods to the test case.
dumpDecorators() - Debugging. The list of classes registered as test case decorators, in the order in which they were registered.
dumpFixtures() - Debugging. The list of classes registered as test case fixtures, in the order in which they were registered. Unlike plain-old-decorators, fixtures are plugged in to the setUp/tearDown cycle. Fixtures which also act as decorators will also show up in a dumpDecorators call.
dumpClassWith($method) - Debugging. Find the class which implements the given method (a class in the test case hierarchy or one of the test case decorators).
dumpInterface() - Debugging. Lists all test case methods (including those donated by decorators) and their declaring classes.
These can be useful when setting up skip conditions.
isLoadedExtension($name) - check for a required php extension
haveService($service_name) - check for running process mysqld, sshd, etc
hasService($service_name) - an alias of haveService
isWin()
isNix()
inCli()
beforeEverything() - Called once per test case before the first setUp. Must be chained to parent! (the others don't, unless you have added the method in a derived class and then subclass again)
beforeInvoking() - called once per test case before the first setUp
afterInvoking() - called once per test case after the last tearDown
afterEverything() - called once per test case after the last tearDown
Like the other framework hooks (skip, setUp, tearDown, etc), these methods are optional.
BeforeInvoking/afterInvoking are called outside the setUp/tearDown cycle so be careful what you put in them. For example, you wouldn't want to create an instance of the tested class here. You'll need a fresh instance for each new test — use setUp instead.
registry() - returns a (single) AperiRegistry instance
injector() - Returns a Phemto instance. Note that a single instance of Phemto is used for all test cases in a group test. The current test case, reporter and request will be pre-registered.
mirror() - returns a ReflectionObject($this) instance
Often you'll want to add extra capabilities to the base test case class — assertions, fixtures etc — to help test a new domain. Extending the basic SimpleTest framework with a derived class is probably the first and simplest choice. However, as the complexity of the extra code increases, you'll very quickly run into the usual problems with inheritance.
As an example, I've had to test a cli app (Rephactor) which interacts with a database, a subversion repository, and the file system. At the end of all that I had to assert stdout/stderr output from the script. A variety of tools and assertions are required for these different domains. Sometimes I'll need to use them separately, sometimes all together, but there's no sane way to do that using inheritance. You can't just stack a series of unrelated behaviours one on top of the other.
This sort of problem would normally be a signal to refactor, dumping inheritance and using aggregation instead. That's exactly what I've done here with a traits-like system of "method injection". It's very easy to plug-in extra features to a basic test case. Different toolsets can be combined in any way you need for a particular test.
You can also add discrete fixture objects to test cases using the same mechanism.
Decorators should be added in a beforeInvoking() method. All the decorator's methods will magically become methods of the test case:
<?php
class MyDecorator {
function foo() {
...
}
}
class MyTestCase extends AperiUnitTest {
function beforeInvoking() {
$this->mixin(new MyDecorator));
}
function testSomething() {
...
$this->foo(); // donated by MyDecorator
...
}
How many decorators can you add? As many as you like:
<?php
class MyTestCase extends AperiUnitTest {
function beforeInvoking() {
$this->mixin(new A);
$this->mixin(new B);
$this->mixin(new C);
...
...
}
Decorators can be mixed and matched freelly in almost any combinations. The sole restriction is that a given method cannot be implemented by more than one decorator (except for framework hooks skip, setUp, teardown etc — see later). If different decorators try to add the same method an exception will be thrown.
Note: this "method conflict" check looks for public methods of the same name in different decorators. It is assumed that public / non-public methods are distinguished by the use of underscores in the old php4 way. Php5 visibility keywords have not been tested. Personally, I've never found a use for these but I could update the code if there was a demand.
Note that method calls are only passed on to the decorator stack if they are not implemented by the test case. Hence, a test case foo() always overrides any decorator foo(). If you want to override a method in a fixture/decorator class either implement it in the test case or extend the fixture/decorator class itself.
Since a given method may be implemented by the test case, one of its antecedents, or one of the decorators (or by nothing at all) how can you find out which one it is?
<?php
class MyTestCase extends AperiUnitTest {
function test() {
...
$this->dumpClassWith('foo');
...
}
Or show the complete list of callable methods and their declaring classes:
<?php
class MyTestCase extends AperiUnitTest {
function test() {
$this->dumpInterface();
}
A test case can decorate its decorators. This isn't as daft as it might sound. Sometimes a decorator will provide a base functionality which you want to refine in the test.
<?php
class MyTestCase extends AperiUnitTest {
function foo() {
// ...
// do some additional thing
// ...
$this->decorators()->foo();
}
$this->decorators() is a composite object containing all decorators. The call will be picked up by the object which implements foo().
A new domain typically requires some new assertions. Sometimes these are useful simply to keep the code clean. At other times the standard set of assertions just don't know how to deal with the new domain. For example, you might want to assertPing() of a connection object or assertStdout() of a shell script.
Once you've written (and tested!) a new expectation class, the next step would normally be to add a corresponding assert~ method to the test case:
<?php
class MyTestCase extends AperiUnitTest {
function assertFoo($candidate, $message = '%s') {
$this->assert(
new FooExpectation;
$candidate,
$message);
}
Alternatively, if you think you'll need to use this again in other tests, you can add the method to a decorator (which can be mixed in with any number of other decorators and test cases). Pass the test case in to the decorator and hook in to the simpletest framework in the usual way:
<?php
class FooDecorator {
function __construct($test_case) {
$this->_test_case = $test_case;
}
function assertFoo($candidate, $message = '%s') {
$this->_test_case->assert(
new FooExpectation;
$candidate,
$message);
}
...
...
}
Add the FooDecorator to the test case:
<?php
class MyTestCase extends AperiUnitTest {
function beforeInvoking() {
$this->mixin(new FooDecorator($this));
}
function testSomething() {
...
...
$this->assertFoo(...);
}
The assertFoo() method is now available to the test case.
You can pass a class name to mixin() rather than an object instance and leave the messy instantiation details to Phemto. Phemto can often figure out dependencies without any special configuration but obviously it's up to you to sort that out.
Note that the the test case's injector instance, $this->injector(), will be used to assemble objects. The current test case class and the chosen SimpleReporter-derived reporter will be pre-registered.
As an example, the above can be simplified to:
<?php
class FooDecorator {
function __construct(AperiTestCase $test_case) { // type hint!
$this->_test_case = $test_case;
}
...
...
}
class MyTestCase extends AperiUnitTest {
function beforeInvoking() {
$this->mixin('FooDecorator'); // just the class name
}
This feature was added purely for the convenience of automatic instantiation. I wasn't thinking about injectable decorator behaviours. Neither should you, probably, but there might be some uses. Occasionally I've needed an unwritable file or dir in a filesystem fixture. On nix, "/root/" can be used but obviously this won't work on windows. An injectable fixture might be useful in similar situations. Test suites could automatically configure themselves for different OS's or users could inject a fixture of their own.
Note that it's best to type hint on "AperiTestCase" rather than the derived classes AperiUnitTest or AperiWebTest. If you hint higher up the decorator is less widely re-usable. You might think you'll never want to use a unit test decorator in a web test but what if you want to test a web script which writes a file? The decorator system was specifically designed to allow anything to be mixed in with anything else.
There is a second, specialised type of decorator: fixture objects. These hear the framework hooks: skip, setUp, teardown etc, and hence can be used to set up and tear down test fixtures.
There's no special requirement for writing a fixture class. Just implement one or more of the framework hooks — whatever you need.
By default, the fixture will also act as a test case decorator. Very often it makes sense to encapsulate fixture-creation code along with some related assertions.
Fixtures should be registered with the test case in a beforeInvoking() method, same as above.
<?php
class FooFixture {
function setUp() {
}
function tearDown() {
}
...
...
}
class MyTestCase extends AperiUnitTest {
function beforeInvoking() {
$this->addFixture('FooFixture');
}
...
...
The addFixture() method is provided as an alias of mixin() to help tests read smoothly — it explains the intent better. Both behave in exactly the same way. AperiTestCase will figure out what kind of object it is and if it should be plugged in to the setUp/tearDown cycle.
Note that, if you add many fixture objects, they will hear setUp() calls in the order in which they were registered and tearDown() in reverse order.
In case you were wondering, the exact call sequence of all the framework hooks is as follows:
before any tests are run:
while looping through test methods:
after all tests are run:
The main point to make is that fixture objects "wrap" the test methods ie fixture before/setUp are always called before both of the test case before/setUp methods. Fixture tearDown/after are always called after both of the test case tearDown/after methods.
The before & after methods aren't used often (and the above sequence is a lot simpler to grasp without them). However, they are very useful if you want to write an abstract test case or fixture. You can slip stuff into the setUp tearDown cycle without using the setUp and tearDown methods themselves and hence you don't need to worry about method chaining in the derived classes.
That's also the reason why I've added two hooks before the tests (beforeEverything & beforeInvoking) and two after (afterInvoking & afterEverything).
Note that if you add a beforeEverything() method to your test case, this must always be chained to its parent. AperiUnitTest uses this to add a couple of decorators to the base AperiTestCase class. None of the other framework hooks need to be chained in this way. Of course if you add a hierarchy of your own on top, you may need to chain methods in your own classes.
You can also add an object as a "non-decorating" fixture:
<?php
class MyTestCase extends AperiUnitTest {
function beforeInvoking() {
$this->addNonDecoratingFixture('FooFixture');
}
...
...
This will hear setUp/tearDown/etc calls as normal but public methods will not be injected into the test case.
Fixtures usually do need to donate a few methods to the test case. You'll only rarely want to use this method, if at all.
Fixture objects will often need to be plugged into the SimpleTest skip() mechanism. Just add a skip() method and call test case skipIf() and/or skipUnless() in the usual way. Of course the fixture will need a reference to the test case:
<?php
class FooFixture {
function __construct(AperiTestCase $test_case) {
$this->_test_case = $test_case;
}
function skip() {
$this->_test_case->skipIf(
$some_boolean_condition,
$message);
$this->_test_case->skipUnless(
$another_boolean_condition,
$message);
}
...
...
A ready-rolled class, RequiredService, does nothing else but skip. It's used to check for running services such as sshd, mysqld etc:
<?php
class MyTestCase extends AperiUnitTest {
function beforeInvoking() {
$this->mixin('RequiredService', 'sshd');
}
...
...
AperiTestCase has a simple way to run different classes past the same set of tests provided they all have a common ancestor (class or interface). Type-hint based dependency injection is used to wire everything up.
<?php
class TwoHandedAxe implements Axe {...}
class OneHandedAxe implements Axe {...}
class FiremansAxe implements Axe {...}
class CanChopStuffUp extends AperiUnitTest {
function shouldTest() {
return array(
'TwoHandedAxe',
'OneHandedAxe',
'FiremansAxe');
}
function setUp() {
$this->axe = $this->injector()->create('Axe');
}
function test() {
...
...
}
The test case will be run several times, once with each class named in shouldTest().
Pass constructor args by sticking them in an array:
<?php
class CanChopStuffUp extends AperiUnitTest {
...
function setUp() {
$this->axe = $this->injector()
->create('Axe', array($a, $b));
}
...
...
AperiUnitTest is a direct replacement for SimpleTest's UnitTestCase. Extend with your own class:
<?php
class CanDoSomething extends AperiUnitTest {
function test() {
...
...
}
Tests must be run with a special runner script.
As well as all the usual AperiTestCase and UnitTestCase stuff you have:
assertPattern( [$number_of_matches,] (optional argument) $pattern, (string) $haystack, (string) $message = '%s')
assertInList( $needle, $haystack, (an array) $message = '%s')
assertEqualArrayValues( $array, $comparison, $message = '%s')
assertArrayValuesNotEqual( $array, $comparison, $message = '%s')
assertIdenticalArrayValues( $array, $comparison, $message = '%s')
assertArrayValuesNotIdentical( $array, $comparison, $message = '%s')
The array comparison assertions can be useful if you want to assert a list of values but don't care what order they come in or what keys they're assigned to. A simple assertEqual() is too sensitive for that.
AperiWebTest is a direct replacement for SimpleTest's WebTestCase. It inherits all the AperiTestCase stuff plus all the standard WebTestCase behaviours.
I've tweaked assertPattern() to optionally allow the number of matches to be specified and added an assertion to verify the proclaimed charset.
assertPattern( [$number_of_matches,] (optional argument) $pattern, (string) $message = '%s')
assertHtmlCharset( - the charset proclaimed by $charset, <meta http-equiv="Content-Type... $message = '%s')
As usual, tests must be run with a special runner script.