Getting the ServiceManager into the test environment and Dependency Injection

So far I’ve got the basic idea of how to test a ZF2 Controller with PHPUnit. However I had problems testing all my actions that used the AlbumTable class to load data from the database because I had no access to the ServiceManager which is used to provide AlbumTable to the controller. Obviously solving this was my next step…

So after getting some errors back from PHPUnit I new I needed to get the ServiceManager involved in the test environment. However 1 thing was bugging me, I’m sure at some point in the past I’d looked at the now infamous Akarbat tutorial and it was using dependency injection, however my current code wasn’t!

I contacted Rob Allen and asked him and sure enough he had use DI in a previous version of the tutorial. I asked why he had changed it and he said it was because the ServiceManager is much quicker, this I can understand because the Zend\Di stuff surely has to do quite a bit of work working out what needs to be injected where. I explained to Rob what I was trying to do and he helpfully pointed me to one of his test suites where I could see the ServiceManager being used in a test.

Getting the ServiceManager into the test suite

First up all I had to do was create an instance of the ServiceManager in the PHPUnit Bootstrap.php, the provide a method to access it in my tests. I could have just created a $serviceManager global variable but obviously that’s not what is done in this day & age, so I "borrowed" some more code from Rob and came up with this for the latest version of my Bootstap.php:

Next up I had to tell the contoller where it could find the ServiceManager object by adding this line to the bottom of my AlbumContollerTest::setUp() method:

And that’s it, now I could add tests for my actions that used AlbumTable:

The problems

Ok, so now I can get some tests going for my other actions but this really isn’t an ideal situation and here are the main reasons why:

  • My tests rely on far more than just the controller being implemented properly. My index action uses AlbumTable to pull data from the database to create Album objects which are then returned. I don’t even have tests for AlbumTable yet so if my controller tests fail the probably may not be with the controller at all so I could end up looking in completely the wrong place.
  • The results returned from the actions are based on data coming out of the database, yet the controller doesn’t directly talk to the database. This means I can’t test the results are correct without first making sure that there is some known test data in the database. Since this is a controller test it wouldn’t really be reasonable to write test data to the databse for this.

So what’s the solution? How about using a mock AlbumTable object to provide some test data for the controller to work with. This sounds good so I needed to work out how to achieve it. Essentially the ServiceManager is injected into the controller, which is then used to suck (is Dependency Suction a used term?) in an AlbumTable. So, I could reconfigure the ServiceManager to provide the mock object instead. This doesn’t seem right to me and the modern buzz phrase Dependency Injection which is all the rage right now; the Zend documentation goes on about it, all modern tutorials, books & papers go on about it and more importantly; a certain Grumpy Programmer had a moan at me for not using it 🙂

So here goes…

Switching to Dependency Injection

Getting ZF2 to use Dependency Injection

So first up I changed my AlbumController class to be ready to be injected by replacing the existing AlbumController::getAlbumTable() with these methods:

I probably could have got rid of getAlbumTable() altogether but all the methods refer to it so I left it in there.

Next up was to get the Controller loading through Zend\Di\Di, I had worked out that I needed to put some new details into my module.config.php but wasn’t really sure what. I had a little moment of panic but thanks to ocramius & Akrabat in #zftalk.2 I got it worked out, this is what my modules/Album/config/module.config.php looks like now:

So as I understand it the router now says it wants a controller called album to be created, the DI tries to look for album and finds it as an alias to Album\Controller\AlbumController. To instantiate this it then finds it needs to supply an Album\Model\AlbumTable to the controller which in turn needs a database Adapter.

I should really workout how to get the database details from the main config but I’ll look at that another time.

So that’s it, DI is working in the application so time to get it into the tests!

Using Dependency Injection in the tests

So I need a Zend\Di\Di object to use in my test cases and as it turns out (many thanks to ocramius for putting up with my persistent stupidity here) the Di needs some extra settings to get going with controllers. As I figured the Di might be used many times in my tests I thought I’d create a method in my Bootstrap.php. I figure it’s easier to post the full Bootstrap.php again so this is how it looks now:

I next got my controller test case to create the controller using Di, to do this I changed the setUp() to look like this:

And finally put together a test case:

The comments in the code explain what’s going on. Now the test is testing the index action of AlbumContoller rather than AlbumController, AlbumTable & Album all at once which is much better.

Final Thoughts

Some things which I don’t are great about it so far and any advice anyone can offer would be greatly appreciated are:

  • Using an array of StdClass instead of a Zend\Db\RecordSet of Album mocks, I’m sure this is fine at the moment but if Album & the controller gets more complicated then maybe the Album interface will need to be mocked too.
  • The dependence on Zend\View\ViewModel which is returned. now this is part of ZF2 so I assume it should be fine to assume it is working but maybe that type should be injected too…thoughs?
  • Very lazy testing of the data at the end 🙂

I’d really love some feedback on this so if any of you have the time and some thoughts that would be great!

16 thoughts on “Getting the ServiceManager into the test environment and Dependency Injection

  1. Pingback: ServiceManager in ZF2 Testumgebung verwenden - Zend Framework Magazin

  2. I guess I should have been more descriptive, sorry. All the pages halt executing JS because of the following error:
    Uncaught exception: TypeError: ‘main.style’ is not a function

    It makes using the code you publish really hard.

    Thanks!

  3. Thank you for your help trying to get Unit Testing to work with Zend Framework 2. You’re one the the very few posts that deals with this topic and just beginning ZF2 myself, it has helped me greatly.

    I’m in the process of making a VERY VERY simple skeleton to build a few applications upon my way and I was having trouble implementing Unit Tests the way you have here. I’m not interested in using Di and would rather keep the ServiceManager and deal with the troubled controller tests due to tightly coupled models.

    My problem is I keep getting a class not found error for ‘Zend\Http\Request’ and keep racking my head to no avail. I really want to begin my learning of Zend Framework 2 using TDD for best practices.

    Would you mind to take a look at my repo and help me understand where I’m failing? Is the bootstrap even being executed?

    Note: I prefer my Unit Tests to be in tests folder in the root directory and not scattered around throughout the modules; rather have them all in one spot. I also prefer to have the controller classes to be tested to extend from my own abstract class so I can add controller independent functions like test404, assertAction, assertControler, etc.

    My repo can be found at:
    https://bitbucket.org/DaveTheAve/nc_skeleton

    Thank you in advance.

  4. have you tried to test an action with a redirect ?
    i’m getting Zend\Mvc\Exception\DomainException: Redirect plugin requires event compose a router

      • This is what I did to test a redirect:

        public function testForwardCorrectCredentials()
        {
        // Add a mock Router object to just do a test forward to anywhere
        $router = $this->getMockBuilder(‘Zend\Mvc\Router\RouteStackInterface’)
        ->disableOriginalConstructor()
        ->getMock();
        $router->expects($this->once())
        ->method(‘assemble’)
        ->will($this->returnValue(‘/forward’));

        $this->event->setRouter($router);

        $this->routeMatch->setParam(‘action’, ‘index’);
        $result = $this->controller->dispatch($this->request);
        $response = $this->controller->getResponse();

        // Status code 302 for a redirect
        $this->assertEquals(302, $response->getStatusCode());

  5. Hey, thanks for the post.

    In the newer Zend2 versions they changed the
    ServiceManagerConfiguration to ServiceManagerConfig. Well, basically the seem to have changed all names from Configuration to Config.

  6. Hi Tom,

    thank you for a very good post. Just wanted to add a quick note. If anyone have followed latest Akrabat’s tutorial then must have setup ‘Album\Model\AlbumTable’ injection through Module’s getServiceConfig method. At that point if you skip whole DI setup leaving ServiceManager bootstrapped from your post straight to a final test case ‘testIndexAction’ (obviously earlier removing line: 23) then it should still return the Album’s data after a dispatch process. My point is that it is much easier to achieve that with ServiceManager having cleaner code and keeping Controller tidy.

  7. Hi nice article,
    but there is an error in your code
    Class ‘Zend\Mvc\Service\ServiceManagerConfiguration’ not found
    the class name is wrong it is just
    ‘Zend\Mvc\Service\ServiceManagerConfig’

    And i should have the idea of reading the other comments before solving the problem myself 🙂

  8. Hi, nice read.

    Maybe you should just fix the JS issue.

    Did you try to get the final render of your view ? I try to use $response->getContent() but it remains empty :/

    Best regards

Leave a Reply

Your email address will not be published. Required fields are marked *