Testing abstract classes with PHPUnit
January 22, 2014
Today I got into the task of testing this code
// file AbstractFactory.php
<?php
namespace TAD\Helpers;
abstract class AbstractFactory {
public function create( $arg ) {
$this->createInstance( $arg );
}
abstract protected function createInstance( $arg );
}
// file ControllersFactory.php
<?php
use jwage\SplClassLoader;
use TAD\MVC\Helpers\FilePathResolver;
use TAD\Helpers\EchoBot;
namespace TAD\MVC\Factories;
abstract class ControllerFactory extends \TAD\Helpers\AbstractFactory {
protected $controllersFolderPath = null;
protected $namespace = null;
protected $controllerClassName = null;
protected function createInstance( $arg ) {
if ( $this->controllerExists( $arg ) ) {
return $this->setAndReturn( $this->$controllerClassName );
}
\TAD\Helpers\EchoBot::_output( "View controller for $arg has not been implemented yet." );
return null;
}
public function __construct() {
// get the file the extending class was defined into
$className = get_called_class();
$classInfo = new \ReflectionClass( $className );
$classFile = $classInfo->getFileName();
$filePathResolver = new \TAD\MVC\Helpers\FilePathResolver( $classFile );
// set the views folder
$this->controllersFolderPath = $filePathResolver->getControllersFolderPath();
// get the extending class namespace
$ns = $classInfo->get_namespace_name();
$this->namespace = $ns;
// register the view components for autoloading
$classLoader = new SplClassLoader( $ns, dirname( $classFile ) );
$classLoader->register();
}
protected function controllerExists( $viewName ) {
$controllerFilePath = $this->controllersFolderPath . '/' . $viewName . 'ViewController.php';
$controllerClassName = $this->namespace . '\\Controllers\\' . $viewName . 'ViewController';
if ( file_exists( $controllerFilePath ) ) {
$this->$controllerClassName = $controllerClassName;
return true;
}
return false;
}
abstract protected function setAndReturn( $controllerClassName );
}
aside for the lack of any dependency injection possibility, it's the factory injecting dependencies after all, writing tests for abstract classes in PHPUnit is possible but not that easy. Until now I always did it running my tests against a concrete class, a dummy one created for testing purposes, extending the abstract
class I really wanted to test.
## Can I test an abstract class without extending it? Yes because I'm actually extending it mocking it.
No because I'm not really testing the class but another class extending it.
## Practical tests I wanted to test that calling the create
method on a class extending the ControllersFactory
class would have triggered the chain reaction resulting in a call to ControllersFactory::createInstance
method.
To make things clear the chain of extensions and method calling should be
ControllersFactoryExtendingClass::create()
AbstractFactory::create()
ControllersFactory::createInstance()
The test function I used to accomplish that is
public function testCreateWillCallInitAndCreateInstance() {
$sut = $this->getMockForAbstractClass( // I have to use a mock generator made for abstract classes
'\TAD\MVC\Factories\ControllerFactory', // the fully qualified name of the abstract class to mock
array(), // no parameters for the class constructor
'', // no new name for the class to mock
FALSE, // do not call the original constructor
TRUE, // call original clone
TRUE, // call autoload
array( 'createInstance' ) ); // I will mock the createInstance method
$sut->expects( $this->once() )->method( 'createInstance' )->will( $this->returnValue( TRUE ) );
$sut->create( 'SomeView' );
}
Another expectation I have is that, no matter how a class extending the ControllerFactory
implements the setAndReturn
method, calling create
with a non-existing parameter should output something polite that can be printed on a site without making the script come to a screeching halt.
public function testCreateNonExistingControllerEchoesToScreen() {
$stub = $this->getMockForAbstractClass( '\TAD\MVC\Factories\ControllerFactory', array(), '', FALSE, TRUE, TRUE, array( 'controllerExists' ) );
$stub->expects( $this->once() )->method( 'controllerExists' )->will( $this->returnValue( FALSE ) );
$this->expectOutputRegex( "/.*SomeView.*/" );
$stub->create( 'SomeView' );
}
The abstract classes are just classes at the end of the day
Simply using the getMockForAbstractClass
method will make partially or totally abstract classes testable generating, de facto, an extending class with its methods stubbed to null-returning ones. The syntax is a bit lengthy but not so different from the getMock
one.