Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a mock object with default properties

Tags:

php

phpunit

I need to create a mock object with default set of properties so it can be used elseware in the codebase upon instantiation.

$mock = $this->getMock('MyClass', array(), array(), 'MyClass_Mock');
$mock->prop = 'foobar';

$myclassMock = new get_class($mock);
var_dump($myclassMock->prop); // NULL
// How can I make this dump 'foobar' ?

I'm testing part of the framework that determines, locates, and instantiates these classes so injecting the mocked object would defeat the purpose of the test.

I don't need to mock any methods.. just dynamically create a mocked class like so:

class MyClass_Mock extends MyClass {
  public $prop = 'foobar';
}

Edit: Simplified example

like image 524
Mike B Avatar asked Feb 17 '12 15:02

Mike B


3 Answers

How do you feel about using Reflection?

$r = new ReflectionClass('MyClass');

$props = $r->getDefaultProperties();

$mock = new stdClass;

foreach ($props as $prop => $value) {
    $mock->$prop = $value;
}

I've not used Reflection a whole lot myself, only for basic introspection. I'm not sure if you'll be able to fully mimic visibility etc. using it, but I don't see why not if you continue down the route of writing to a string and evaling.

Edit:

Scanned through the Reflection functions out of curiosity, it is totally possible to fully mimic the class with dummy methods, implementing full visibility constraints, constants, and static elements where appropriate if you dynamically build the class in a string and eval it.

However it looks like it will be a complete mission to really fully support every possibility when it comes down to getting data types correct (you'll need code to rebuild an array constructor from an array for example)

Best of luck if you go down this route, it requires more brain power than I'm willing to spare right now :)

Here's a little bit of code, you can do the same thing with constants, and create empty methods in a similar way.

class Test
{
    private static $privates = 'priv';
    protected $protected = 'prot';
    public $public = 'pub';
}

$r = new ReflectionClass('Test');

$props = $r->getDefaultProperties();

$mock = 'class MockTest {';

foreach ($props as $prop => $value) {
    $rProp = $r->getProperty($prop);


    if ($rProp->isPrivate()) {
        $mock .= 'private ';
    }
    elseif ($rProp->isProtected()) {
        $mock .= 'protected ';
    }
    elseif ($rProp->isPublic()) {
        $mock .= 'public ';
    }

    if ($rProp->isStatic()) {
        $mock .= 'static ';
    }

    $mock .= "$$prop = ";

    switch (gettype($value)) {
        case "boolean":
        case "integer":
        case "double":
            $mock .= $value;
            break;
        case "string":
            $mock .= "'$value'";
            break;
/*
"array"
"object"
"resource"
*/
    case "NULL":
            $mock .= 'null';
            break;
    }

    $mock .= ';';
}

$mock .= '}';

eval($mock);

var_dump(new MockTest);
like image 126
Leigh Avatar answered Sep 18 '22 11:09

Leigh


I'm not sure you even need to do this for testing purposes.

Usually when testing code that involves model access you use fixtures instead of mocking the actual models because models are "dumb" data structures that don't expose any capabilities that need to be mocked.

Your example bears this out: if you don't need to mock behavior (methods), you don't need a mock object. You need a data fixture instead that the model uses as its data source. This is especially true if, as you say, "dependency injection isn't an option".

Of course, if you decide you want to mock the model anyway, I'd suggest @Leigh's reflection solution.

I just answered a question about database testing yesterday that you might check out for a bit more detail: PHPUnit: How to test database interactions on remote Postgres server?

like image 33
rdlowrey Avatar answered Sep 20 '22 11:09

rdlowrey


I think the issue is that you must have the system-under-test (your framework) be able to use new to instantiate model objects directly, and each test needs to set the default values for its properties differently.

If this is the case, you can create a simple base class to populate a predefined set of properties upon construction. The solution below uses late static binding from PHP 5.3, but you could easily achieve the same results without it with a slight tweak.

class MockModel
{
    public static $properties;

    public function __construct() {
        if (isset(static::$properties) && is_array(static::$properties)) {
            foreach (static::$properties as $key => $value) {
                $this->$key = $value;
            }
        }
    }
}

class MockBook extends MockModel { /* nothing needed */ }

function testBookWithTitle() {
    MockBook::$properties = array(
        'title' => 'To Kill a Mockingbird'
    );
    $book = new MockBook;
    self::assertEquals('To Kill a Mockingbird', $book->title);
}

As long as you can provide the class name to use with new to your framework, this should work. If you need to be able to create more than one instance of the same mock class in during a single call to your framework, you'll need to enhance the above with some sort of indexing mechanism.

like image 25
David Harkness Avatar answered Sep 20 '22 11:09

David Harkness