Getting around the limits of magic method mocking.
The problem
Using PHP magic methods is a practice frowned upon but it’s something that might sometimes be necessary.
A use case might be one where a client class is calling undefined methods on one of its dependencies like this:
class Page_Semaphore {
protected $pages;
public function __construct( Pages $pages ) {
$this->pages = $pages;
}
public function get_color_for_page( $page ) {
if( $this->pages->{"{$page}_can_run"}( ) ) {
return 'green';
}
return 'red';
}
}
And its Pages
dependency
class Pages {
public function __call($name, $args) {
$matches = array();
if ( !preg_match( '/([a-zA-Z]+)_can_run/', $name, $matches ) ) {
throw new BadMethodCallException( 'Only *_can_run is supported' );
}
$page = $matches[1];
$page_post = get_page_by_path( $page );
if ( empty($page_post) ) {
return false;
}
$run_condition = get_post_meta( $page_post->ID, '_run_condition', true );
return $run_condition === 'ok' ? true : false;
}
}
While testing the Page_Semaphore
class I could write two tests like these
class Page_Semaphore_Test extends PHPUnit_Framework_TestCase {
/**
* @test
* it should return green if page is ok
*/
public function it_should_return_green_if_page_is_ok() {
$pages = $this->prophesize( 'Pages' );
$pages->some_page_can_run()->willReturn( true );
$semaphore = new Page_Semaphore( $pages->reveal() );
$out = $semaphore->get_color_for_page( 'some_page' );
$this->assertEquals( 'green', $out );
}
/**
* @test
* it should return red if page is other than ok
*/
public function it_should_return_red_if_page_is_other_than_ok() {
$pages = $this->prophesize( 'Pages' );
$pages->some_page_can_run()->willReturn( false );
$semaphore = new Page_Semaphore( $pages->reveal() );
$out = $semaphore->get_color_for_page( 'some_page' );
$this->assertEquals( 'red', $out );
}
}
Running the tests will result in the mocking engine complaining about the undefined some_page_can_run
method.
I’m using prophecy to mock objects here but the same would apply to PhpUnit.
A solution
The best solution would be not to rely on magic methods at all but ruled that out here is another one that’s based around inheritance.
While the $page
parameter used by the Page_Semaphore::get_color_for_page
might vary I can fix what page slugs will be used in my tests in a certain way.
The class below extends the Pages
class providing a concrete empty implementation of the method.
class Test_Pages extends Pages {
public function some_page_can_run () {
}
}
and I will make the method behave the way I want mocking this class in my tests
class Page_Semaphore_Test extends PHPUnit_Framework_TestCase {
/**
* @test
* it should return green if page is ok
*/
public function it_should_return_green_if_page_is_ok() {
$pages = $this->prophesize( 'Test_Pages' );
$pages->some_page_can_run()->willReturn( true );
$semaphore = new Page_Semaphore( $pages->reveal() );
$out = $semaphore->get_color_for_page( 'some_page' );
$this->assertEquals( 'green', $out );
}
/**
* @test
* it should return red if page is other than ok
*/
public function it_should_return_red_if_page_is_other_than_ok() {
$pages = $this->prophesize( 'Test_Pages' );
$pages->some_page_can_run()->willReturn( false );
$semaphore = new Page_Semaphore( $pages->reveal() );
$out = $semaphore->get_color_for_page( 'some_page' );
$this->assertEquals( 'red', $out );
}
}
This will solve the problem while also not breaking the type hinting in place.