2012-03-12

Unit testing code that uses static methods

Static calls are death to testability. There are ways of mocking out static calls made inside a class, but if you have some code that makes static calls outside a class, it's much more difficult. The best way is to refactor the static class to an instance class so you can mock things out.

/**
* Static calculator class.
*
* There's no way to dependency inject this for
* mocking it out while testing.
*/
class Calculator {
private function __construct() {}
public static function add($a, $b) {
return $a + $b;
}
public static function multiply($a, $b) {
return $a * $b;
}
}
/**
* Instance calculator class.
*/
class Calculator {
public function add($a, $b) {
return $a + $b;
}
public function multiply($a, $b) {
return $a * $b;
}
}
view raw static.php hosted with ❤ by GitHub

But if you're working in a legacy code base with large and ugly static classes, refactoring them may be overly difficult or hard to justify from a business perspective. But you still want to test the new code that you're writing that uses those static classes. A coworker showed me one way to do it. It's still hacky and not as good as fixing the static class properly.

Basically instead of refactoring the crufty static code, you do the refactoring in the class you're writing. Here's an example class that uses the static Calculator class from above:

class Foo {
public function addAndSquare($bar, $baz) {
$tmp = Calculator::add($bar, $baz);
return Calculator::multiply($tmp, $tmp);
}
}
view raw static-user.php hosted with ❤ by GitHub

You can't mock out the Calculator class in this case. But you can make changes to your Foo class that will let you control the output from the static class. You can add protected methods to your class that call the static methods, and then mock those methods for testing and test the mock of the class you're testing.

class Foo {
public function addAndSquare($bar, $baz) {
$tmp = $this->add($bar, $baz);
return $this->multiply($tmp, $tmp);
}
protected function add($foo, $bar) {
return Calculator::add($foo, $bar);
}
protected function multiply($foo, $bar) {
return Calculator::multiply($foo, $bar);
}
}
class FooTest
extends PHPUnit_Framework_TestCase {
public function testAddAndSquare() {
$mockFoo = $this->getMock('Foo',
array('add', 'multiply'));
$mockFoo->expects($this->any())
->method('add')
->will($this->returnValue(42));
$mockFoo->expects($this->any())
->method('multiply')
->with($this->equalTo(42),
$this->equalTo(42))
->will($this->returnValue(99));
$this->assertEquals(99,
$mockFoo->addAndSquare(1, 1));
}
}

Obviously this example is very simplistic, but in a more complicated class you could have the static class throw exceptions or different output or whatever.

Now you have two problems

This technique is far from perfect. If you were to change the behavior of the protected methods of your class, the unit tests would still pass, but the code would no longer work the same in production.

2 comments:

  1. The example seems to highlight one of the possible flaws with the solution, which is when it calls $this->add( $bar, $bar ). Shouldn't the second $bar be $baz? This would probably be caught by additional test cases, of course.

    If only we all had unlimited time and money to rebuild horror code, right? This at least gets part of the way there. Thanks for sharing the idea :)

    ReplyDelete
  2. Good catch. I've updated the code to suck a little bit less.

    ReplyDelete