Integration Testing. Or, how i learned to stop overriding and love the 'filter'
Yesterday i was tasked with throwing together a base Integration Test
class for Lithium. If you aren't aware, (for various reasons) Lithium
uses it's own custom Unit test class in it's core test suite. I won't
go into all the reasons for that here (/me has an inherent fear of the
German Bergmann :P), suffice it to say: this keeps everything
lightweight and, thanks to lithium's filter system, is lightening
fast.
Unit test that would halt execution upon a failed test (as opposed to
the default unit test behavior, which continues running). After briefly reviewing the implementation of Unit (see
lithium\test\Unit) it was clear that overriding Unit::run() would be
inefficient because there were too many other things going on that
would need to be replicated in Integration::run(). Perfect, this sounded like my first chance to write my own filter
(technically it would be my second, but my filter on Dispatcher::run() i
wrote in the test bootstrapper was little more than a copy and paste
of @gwoo's Asset filter (see: some random pastium somewhere. It's
about time Lithium had a proper developer's blog :P)). Writing a Filter Well the first step (when writing a filter with the intention of
modifying existing behavior) is understanding the existing behavior
you will be effecting with your filter. For me, this was a loop in Unit::run():
foreach ($methods as $method) {
$this->_runTestMethod($method, $options);
}
Basically, i needed this loop to break when a test failed. First, i had to tinker slightly with Unit::run(), updating this loop to break
as follows:
foreach ($methods as $method) {
if ($this->_runTestMethod($method, $options) === false) {
break;
}
}
For the existing Unit test, this would run as expected (a void return would simply continue the loop. Now, however, i had an execution point
at which in insert my filter's logic. My Integration Test class would wrap a filter around
Integration::_runTestMethod() which did the following:
1. Grabbed the existing aggregated results
2. Ran the next Test case
3. Checked the newly aggregated results and, on fail returned false So, next i had a look at Unit::_runTestMethod() (the method that i
would be filtering). Keep in mind that these test classes are meant to
be instantiated. Filters work slightly different when dealing with
Static/Instantiated classes, for obvious reasons. So here's _runTestMethod():
protected function _runTestMethod($method, $options) {
try {
$this->setUp();
} catch (Exception $e) {
$this->_handleException($e, __LINE__ - 2);
return $this->_results;
}
$params = compact('options', 'method');
$passed = $this->_filter(__CLASS__ . '::run', $params, function($self, $params, $chain) {
try {
$method = $params['method'];
$lineFlag = __LINE__ + 1;
$self->$method();
} catch (Exception $e) {
$self->invokeMethod('_handleException', array($e));
}
});
$this->tearDown();
return $passed;
}
What's happening here is _runTestMethod does some setup, then runs the part of the method which is filterable (everything passed into the
anonymous function that is $this->_filter's third argument). _filter
is a method all Lithium classes inherit by extending the core Object
(or StaticObject) class. _runTestMethod is saying:
Here, run all the filters that anyone has applied to "run", then run
this. Actually, it's even better than that. because of the recursive
nature of the filtering system, we can write a filter with context
before and after this closure is executed (utilizing the chain, which
i will show in a moment). So now it was just a matter of applying my filter in the base
Integration Test class' _init(). Note: Lithium has a standard for
subclasses. Instead of using __construct(), use _init() for
initializations which is automatically called in
lithium\core\Object::__construct(). This is done for various reason's
i won't get into here, but it makes for some clean standards. And finally, here's my Integration Test class:
class Integration extends \lithium\test\Unit {
/**
* Auto init for applying Integration filter
*
* @return void
*/
protected function _init() {
parent::_init();
$this->applyFilter('run', function($self, $params, $chain) {
$before = $self->results();
$chain->next($self, $params, $chain);
$after = $self->results();
while (count($after) > count($before)) {
$result = array_pop($after);
if ($result['result'] == 'fail') {
return false;
}
}
});
}
}
After calling parent::_init() (convention), I apply a filter to this
object's 'run' method (remember _runTestMethod named itself "run" when
calling _filter). Passing in my closure with the standard three
parameters $self, $params, and $chain. All you need to worry about for
those is $params, which would be an array of any data you wanted to
pass into your filter.
$before = $self->results();In this context $self is a copy of this Integration instance. The line:
$chain->next($self, $params, $chain);passes on execution to the next method in this filter's chain (read up
on monad's if you're bored). And finally, now that we've executed this next test, the rest of the
filter checks our new results for failures and returns false if found. Conclusions "Why not just override _runTestMethod?" While this would work, it would violate D.R.Y. (don't repeat
yourself), as really we want to still perform everything in
Unit::_runTestMethod. What we actually want to do here is best described in an aspect
oriented approach. Which is now provided for us by Lithium's filtering
system. Hopefully this is of some use to someone at some point. There isn't a
lot of literature yet on pragmatically utilizing Lithium's nicer
features and, while code examples are valuable, some narrative context
always seems to help me. The more I find myself realizing the
capabilities of Lithium's filtering and the elegant solutions it
allows, the more excited I get about the project.
For More information on the project, check out: lithify.me