Chronicles of a build – the theme framework 09

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

  1. localize itself in the filesystem
  2. localize itself in the namespace
  3. 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

  1. PHPUnit will not allow to mock the __construct method
  2. I can’t inject a mock Controller instance in the Main 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.