3

I'm used to the habit of writing like this:

$results = SomeModelQuery::create()->filterByFoo('bar')->find();

However this does not scale for unit testing because I can't inject a mock object, i.e. I can't affect what data is returned. I'd like to use fixture data, but I can't.

Nor does it seem great to inject an object:

class Foo
{
    public __construct($someModelQuery)
    {
        $this->someModelQuery = $someMOdelQuery;
    }

    public function doSthing()
    {
         $results = $this->someModelQuery->filterByFoo('bar')->find();
    }
}

DI feels horrible. I have tens of query objects to mock and throw. Setting through constructor is ugly and painful. Setting using method is wrong because it can be forgotten when calling. And it feels painful to always for every single lib and action to create these query objects manually.

How would I elegantly do DI with PropelORM query classes? I don't want to call a method like:

$oneQuery = OneQuery::create();
$anotherQuery = AnotherQuery::create();
// ... 10 more ...
$foo = new Foo($oneQuery, $anotherQuery, ...);
$foo->callSomeFunctionThatNeedsThose();
Bo Persson
  • 90,663
  • 31
  • 146
  • 203
Tower
  • 98,741
  • 129
  • 357
  • 507

1 Answers1

3

In my opinion (and Martin Folowers's) there is a step between calling everything statically and using Dependency Injection and it may be what you are looking for.

Where I can't do full DI (Zend Framework MVC for example) I will use a Service Locator. A Service Layer will be the place that all your classes go to get there dependencies from. Think of it as a one layer deep abstraction for your classes dependencies. There are many benefits to using a Service Locator but I will focus on testability in this case.

Let's get into some code, here is are model query class

class SomeModelQuery
{
    public function __call($method, $params)
    {
        if ($method == 'find') {
            return 'Real Data';
        }
        return $this;
    }
}

All it does is return itself unless the method 'find' is called. Then is will return the hard-coded string "Real Data".

Now our service locator:

class ServiceLocator
{
    protected static $instance;

    protected $someModelQuery;

    public static function resetInstance()
    {
        static::$instance = null;
    }

    public static function instance()
    {
        if (self::$instance === null) {
            static::$instance = new static();
        }
        return static::$instance;
    }

    public function getSomeModelQuery()
    {
        if ($this->someModelQuery === null) {
            $this->someModelQuery = new SomeModelQuery();
        }
        return $this->someModelQuery;
    }

    public function setSomeModelQuery($someModelQuery)
    {
        $this->someModelQuery = $someModelQuery;
    }
}

This does two things. Provides a global scope method instance so you can always get at it. Along with allowing it to be reset. Then providing get and set methods for the model query object. With lazy loading if it has not already been set.

Now the code that does the real work:

class Foo
{
    public function doSomething()
    {
        return ServiceLocator::instance()
            ->getSomeModelQuery()->filterByFoo('bar')->find();
    }
}

Foo calls the service locator, it then gets an instance of the query object from it and does the call it needs to on that query object.

So now we need to write some unit tests for all of this. Here it is:

class FooTest extends PHPUnit_Framework_TestCase
{
    protected function setUp()
    {
        ServiceLocator::resetInstance();
    }

    public function testNoMocking()
    {
        $foo = new Foo();
        $this->assertEquals('Real Data', $foo->doSomething());
    }

    public function testWithMock()
    {
        // Create our mock with a random value
        $rand = mt_rand();
        $mock = $this->getMock('SomeModelQuery');
        $mock->expects($this->any())
            ->method('__call')
            ->will($this->onConsecutiveCalls($mock, $rand));
        // Place the mock in the service locator
        ServiceLocator::instance()->setSomeModelQuery($mock);

        // Do we get our random value back?
        $foo = new Foo();
        $this->assertEquals($rand, $foo->doSomething());
    }
}

I've given an example where the real query code is called and where the query code is mocked.

So this gives you the ability to inject mocks with out needing to inject every dependency into the classes you want to unit test.

There are many ways to write the above code. Use it as a proof of concept and adapt it to your need.

SamHennessy
  • 4,288
  • 2
  • 18
  • 17
  • Otherwise great, but I am not a fan of a singleton. Is there a way to do this without one? There's also the option that I actually create a test database that is for each test dropped and recreated with test data, but that is a bit slow. – Tower Mar 15 '12 at 06:50
  • As I said there are lots of ways of doing this. If you are tring to avoid a Singleton you can inject the Service Locator into where you need it. Also if we are just going to good testability you could do optional injection. That is, if a dependency is set then use it, else create your own or get the global instance of the object. This allows simple use in production code and the ability to inject mocks in test code. You could setup the Service Locator so that the consuming class just creates an instance of its own. But without true DI somethings will need to be a singleton, like DB connections – SamHennessy Mar 15 '12 at 14:56