This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; | |
} | |
} |
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Foo { | |
public function addAndSquare($bar, $baz) { | |
$tmp = Calculator::add($bar, $baz); | |
return Calculator::multiply($tmp, $tmp); | |
} | |
} |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
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.
ReplyDeleteIf 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 :)
Good catch. I've updated the code to suck a little bit less.
ReplyDelete