Not sure why I didn't start doing this sooner. We have a basic Feature toggle system that is maintained in the global scope to make it easily accessible to any part of the code:
Feature::isEnabled("something_awesome");
I needed to force a particular feature on in a PHPUnit integration test, but in order to tidy up after myself, I would need to ensure that the test reset the Feature system after it had finished. There are a few ways of doing this.
The first is to consider using PHPUnit's built in features for preserving global state . I've never really used them, but I have seen plenty of problems caused by them, so I didn't even bother looking.
Another option is to wrap the test execution in a try/catch/finally block and ensure the global state is reset regardless of what the test code does:
/** @test */
public function single_nasty_test_with_global_scope_changes()
{
GlobalScope::$thing = true;
try {
// exercise system that uses GlobalScope::$thing
$this->assertTrue(GlobalScope::$thing);
} finally{
GlobalScope::$thing = false;
}
}
This isn't too bad, but I'd prefer to avoid the indentation and would look a lot messier if there were several lines of test code.
Another option, you could add the reset code to your tearDown
method or add a totally new @after
method.
/** @after */
public function reset_global_state()
{
GlobalScope::$thing = false;
}
/** @test */
public function single_nasty_test_with_global_scope_changes()
{
GlobalScope::$thing = true;
// exercise system that uses GlobalScope::$thing
$this->assertTrue(GlobalScope::$thing);
}
This isn't too bad again, but it gets a bit lost for me, it applies to every test method in the class (which would be a dozen or so in my instance), rather than the one test method that needs it. It's also separated from the test method, so it's not immediately clear if the global scope is being reset accordingly.
None of these really appealed to me, so I had a quick look inside PHPUnit to see if it had anything that would allow me to set it up contextually, right next to that particular test methods setup. There wasn't that I could see, but it didn't take two minutes to write this little trait:
<?php
trait AfterHooks
{
private $afterHooks = [];
public function after(callable $callback)
{
$this->afterHooks[] = $callback;
}
/**
* @after
*/
public function runAfterHooks()
{
$afterHooks = $this->afterHooks;
$this->afterHooks = [];
foreach ($afterHooks as $afterHook) {
$afterHook();
}
}
}
And here is how you use it:
use AfterHooks;
/** @test */
public function some_nasty_test_with_global_scope_changes()
{
$this->after(function () { GlobalScope::$thing = false; });
GlobalScope::$thing = true;
// exercise system that uses GlobalScope::$thing
$this->assertTrue(GlobalScope::$thing);
}
/** @test */
public function another_test()
{
// exercise system that uses GlobalScope::$thing
$this->assertFalse(GlobalScope::$thing);
}
Doesn't have to be global scope, you can use it to tear down anything that's particularly relevant to a specific test. You are welcome.