Why you can't (or shouldn't) unserialize exceptions
Posted by John Kleijn • Friday, May 7. 2010 • Category: TDDSorry for not posting much, I'm insanely busy, but I can squeeze in a quick post. I already lost lots of time on the topic of this post, so I might as well take 10 minutes more and explain.
I develop using TDD, using PHPUnit. I (usually) use Zend Framework in combination with Doctrine. To be a 100% sure tests are completely isolated, PHPUnit has a nice feature called "Process Isolation", which means as much as that every test is run in its own environment. To get the result of the test into the main environment, PHPUnit serializes and unserializes the "test result" object. Which is fine, I couldn't think of another way to do it.
But, this test result object may include failures, in which case the exception(s) are attached. Which is a problem. A few lines of code is worth a thousand words, so here you go:
jkleijn@goliath:~$ php -a
Interactive shell
php > class Argument { function __wakeup() { throw new LogicException; }}
php > class Subject { function fail($argument){ throw new InvalidArgumentException; }}
php > try { $s = new Subject; $s->fail(new Argument); } catch(Exception $e) { unserialize(serialize($e)); }
PHP Warning: Uncaught exception 'LogicException' in php shell code:1
Stack trace:
#0 [internal function]: Argument->__wakeup()
#1 php shell code(1): unserialize('O:24:"InvalidAr...')
#2 {main}
thrown in php shell code on line 1
php >
As you can see, unserializing the exception triggers the __wakeup() method in the argument that was passed to the failing method. The problem with that is that an Exception object in PHP includes a full backtrace, which, amongst other things, includes arguments to function or method calls. No issue if the arguments are scalar values or a simple array, but an object may not like being serialized, per se.
Such is the case with Doctrine_Record and Doctrine_Collection objects. They can be unserialized, but require that a database connection has been made first, or they will throw: Uncaught exception 'Doctrine_Connection_Exception' with message 'There is no open connection'. Which obviously is not a precondition one would want to have for running test cases. It kinda defeats having test isolation in the first place.
The good news is that this is only an issue if a test fails, so as long as your test passes, you're fine. The bad news is that in order to find out WHY your test failed, you have to hack a file dump of the serialized test result, to find the exception message and origin. Which is a nuisance at best.
