The work on my little mVC framework goes on and, having decided to stick to TDD as much as possible I found myself a little “lost in the woods” while trying to test what seemed like a simple passage.
The premise
Starting from the end I would like to be able to use the framework like this
// file index.php
<?php
$theme = Theme::getInstance();
$theme->begin();
$theme->show('First'); // a controller
$theme->show('Second'); // a controller
$theme->show('Third'); // a controller
$theme->end();
And that’s all I’d like to have to write aside for wrapping the various parts into HTML tags.
The Theme
class is extending the Main
class and will
- localize itself in the filesystem
- localize itself in the namespace
- localize all controller files adhering the the assumed filesystem/namespace hierarchy
Each call to the show
method will trigger a check to see if the requested controller exists, a polite message will echo to the page if negative to allow the theme framework to be used for prototyping, and will then proceed to instance the requested controller. Code for the Main::show
function is
/**
* Shows a controller output on the page or echoes a polite error message
* @param string $controllerName The name of the element the controller models, like 'Header'
* @param array $args An array of arguments that will be passed to the controller
* @return none Echoes the controller output to the page
* @throws InvalidArgumentException If controller name is not a string or args is not an array
*/
public function show($controllerName, $args = null)
{
// check the parameters
if (!is_string($controllerName)) {
throw new \InvalidArgumentException("Controller name must be a string", 1);
}
if (!is_null($args) and !is_array($args)) {
throw new \InvalidArgumentException("Arguments must be an array or null", 2);
}
// if there are no infos for the controller then politely echo its non-existence
if (!in_array($controllerName, array_keys($this->controllersInfo))) {
$this->politelyEcho("Controller $controllerName is not a defined controller (yet).");
}
// get the controller fully qualified class name
$className = $this->controllersInfo[$controllerName]['className'];
// get an instance of the controller
$controller = new $className();
// generate a random 5 digit string to avoid duplicated keys in the `controllers` array
$rand = substr(md5(microtime()),rand(0,26),5);
// save a reference to the controller in the controllers
$this->controllers[$className . $rand] = $controller;
// call the controller show method passing it the args
$controller->show($args);
}
I have expectations
What if I want to set expectations on a controller? The most basic one could be to check if Main
will call, while running its own show
method, the controller show
method.
Long story short: I can’t do that because
- PHPUnit will not allow to mock the
__construct
method - I can’t inject a mock
Controller
instance in theMain
class
Some thought
Skipping point 1 of the list above, not solvable by me and probably not a problem, I will work on point 2.
The place to inject a mock Controller
instance is not in the show
method defined in the Main
class; I could change the method signature from
show($controllerName, $args)();
to
show($controllerName, $args, $mock = null);
which would not burden the call but would make me modify my production code, the one in the Main
class, to be able to test. A better way might be to refactor the show
, and the Bot
class along with it, to make it use the Bot
class method getControllerInstance
public function show($controllerName, $args = null)
{
// check the parameters
if (!is_string($controllerName)) {
throw new \InvalidArgumentException("Controller name must be a string", 1);
}
if (!is_null($args) and !is_array($args)) {
throw new \InvalidArgumentException("Arguments must be an array or null", 2);
}
try {
$controller = $this->bot->getControllerInstance($controllerName);
}
catch(Exception $e) {
$this->politelyEcho($e->getMessage());
}
// generate a random 5 digit string to avoid duplicated keys in the `controllers` array
$rand = substr(md5(microtime()) , rand(0, 26) , 5);
// store a reference to the controller instance in the controllers
$this->controllers[$controllerName . $rand] = $controller;
// call the controller show method passing it the args
$controller->show($args);
}
I’m now able to inject a mock Controller
instance to test for expectations like
// file MainTest.php
public function testShowWillCallShowOnController()
{
$sut = TestMain::getInstance();
$mockController = $this->getMockBuilder('\TAD\Test\controllers\FirstController')->setMethods(array(
'__construct',
'show'
))->disableOriginalConstructor()->getMock();
$mockBot = $this->getMockBuilder('\TAD\MVC\helpers\Bot')->setMethods(array(
'__construct',
'getControllerInstance'
))->disableOriginalConstructor()->getMock();
$mockBot->expects($this->once())->method('getControllerInstance')->will($this->returnValue($mockController));
$mockController->expects($this->once())->method('show');
// inject the mock Bot in place of the good one
$sut->bot = $mockBot;
$sut->show('First');
}
A word on mocking the constructor
I simply cannot mock the class constructor and really had an hard time understanding it. While it’s clear that __construct
is a class method, not an instance one, and will hence conflict with PHPUnit instance-oriented nature, the PHPUnit method disableOriginalConstructor
will do the trick allowing me to pretty much do anything.