Welcome to Abdul Malik Ikhsan's Blog

Testing Zend Framework 2 application using phpspec

Posted in testing, Tutorial PHP, Zend Framework 2 by samsonasik on November 8, 2015

If you’re going to start working with new ZF2 application, it is be a good chance to use phpspec for testing tool. We can describe specification and generate code that we already describe. Ok, let’s start with clone ZF2 skeleton application:

$ composer create-project zendframework/skeleton-application:dev-master zfnew

We will start application with “test first”, so, we can remove current module/Application’s classes:

$ cd zfnew
$ rm -rf module/Application/Module.php
$ rm -rf module/Application/src/Application/Controller/IndexController.php

The next step is setup requiring phpspec dependency and its needed extensions:

$ composer config bin-dir bin
$ composer require phpspec/phpspec:~2.3.0 \
                   henrikbjorn/phpspec-code-coverage:~1.0.1 \
                   ciaranmcnulty/phpspec-typehintedmethods:~1.1 --dev

We will use henrikbjorn/phpspec-code-coverage for code coverage generation, and ciaranmcnulty/phpspec-typehintedmethods for typehint generation when running phpspec.

By default, our Application module follow PSR-0 autoloader, so we need to define it in composer.json:

// ...
   "autoload": {
        "psr-0": {
            "Application\\": "module/Application/src/"
        }
    },
// ...

To make it registered in composer’s autoload, we need to run dump-autoload:

$ composer dump-autoload

To point spec to describe Application namespace inside module/Application/src, we need to setup phpspec config under phpspec.yml, we can place it in root zfnew project:

# zfnew/phpspec.yml
suites:
  application_suite:
    namespace: Application
    src_path: module/Application/src/
    spec_path: module/Application

extensions:
  - PhpSpec\Extension\CodeCoverageExtension
  - Cjm\PhpSpec\Extension\TypeHintedMethodsExtension

code_coverage:
  format:
    - html
    - clover
  whitelist:
    - module/Application/src
  output:
    html: coverage
    clover: build/logs/clover.xml

Ok, let’s generate our first spec:

$ bin/phpspec desc Application/Module

We will get output like the following:

desc-1-module-class

We will get generated first spec like the following:

// module/Application/spec/Application/ModuleSpec.php
namespace spec\Application;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class ModuleSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('Application\Module');
    }
}

And when we run:

$ bin/phpspec run

We will get generated class like this if we choose ‘Y’ answering “Do you want me to create Application\Module for you?” question:

run-1-module-class

And we will get a Module class inside module/Application/src/Application directory:

namespace Application;

class Module
{
}

We need to have more examples to achieve standard Module class, that has getConfig() method, and especially for Application module, we need onBootstrap(MvcEvent $e) method, so we can write examples like the following:

// module/Application/spec/Application/ModuleSpec.php
namespace spec\Application;

use Application\Module;
use PhpSpec\ObjectBehavior;
use Zend\EventManager\EventManager;
use Zend\Mvc\Application;
use Zend\Mvc\MvcEvent;

class ModuleSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(Module::class);
    }

    function it_return_config()
    {
        $getConfig = $this->getConfig();

        $getConfig->shouldBeArray();
        $getConfig->shouldReturn(
            include __DIR__ . '/../../config/module.config.php'
        );
    }

    function its_bootstrap(MvcEvent $e, $application, $eventManager)
    {
        $application->beADoubleOf(Application::class);
        $eventManager->beADoubleOf(EventManager::class);

        $application->getEventManager()->willReturn($eventManager)->shouldBeCalled();
        $e->getApplication()->willReturn($application)->shouldBeCalled();

        $this->onBootstrap($e);
    }
}

And when run, we will get the following errors:

                                                                                
  Do you want me to create `Application\Module::getConfig()` for you? [Y/n] 
  Y
  Method Application\Module::getConfig() has been created.
                                                                                
  Do you want me to create `Application\Module::onBootstrap()` for you? [Y/n] 
  Y
  
  Method Application\Module::onBootstrap() has been created.
Application/Module                                                                
  18  - it return config
      is_array(null) expected to return true, but it did not.

Application/Module                                                                
  28  - its bootstrap
      some predictions failed:
        Double\Zend\Mvc\MvcEvent\P2:
          No calls have been made that match:
            Double\Zend\Mvc\MvcEvent\P2->getApplication()
          but expected at least one.  Double\Zend\Mvc\Application\P1:
          No calls have been made that match:
            Double\Zend\Mvc\Application\P1->getEventManager()
          but expected at least one.

            33%                                     66%                          3
1 specs
3 examples (1 passed, 2 failed)
708ms

Don’t worry about it, it is normal, we just need to fulfill what already described in code as we have generated code template:

namespace Application;

class Module
{

    public function getConfig()
    {
        // TODO: write logic here
    }

    public function onBootstrap(\Zend\Mvc\MvcEvent $mvcEvent)
    {
        // TODO: write logic here
    }
}

let’s fill it so it looks like:

namespace Application;

use Zend\Mvc\ModuleRouteListener;

class Module
{
    public function getConfig()
    {
        return include __DIR__ . '/../../config/module.config.php';
    }

    public function onBootstrap(\Zend\Mvc\MvcEvent $mvcEvent)
    {
        $eventManager        = $mvcEvent->getApplication()->getEventManager();
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);
    }
}

To prove, you can re-run bin/phpspec run and everything will be green ;).

Now, let’s create spec for Application\Controller\IndexController:

$ bin/phpspec desc Application/Controller/IndexController

And we can define the IndexControllerSpec:

// module/Application/spec/Application/Controller/IndexControllerSpec.php
namespace spec\Application\Controller;

use Application\Controller\IndexController;
use PhpSpec\ObjectBehavior;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class IndexControllerSpec extends ObjectBehavior
{
    function let(ViewModel $viewModel)
    {
        $this->beConstructedWith($viewModel);
    }

    function it_is_initializable()
    {
        $this->shouldHaveType(IndexController::class);
    }

    function it_is_extends_abstract_action_controller()
    {
        $this->shouldBeAnInstanceOf(AbstractActionController::class);
    }

    function its_index_action_return_view_model(ViewModel $viewModel)
    {
        $this->indexAction()->shouldReturn($viewModel);
    }
}

We use beConstructedWith(), so, we need to inject ViewModel into controller’s construction. We can run bin/phpspec run and we will get the following code:

// module/Application/src/Application/Controller/IndexController.php
namespace Application\Controller;

class IndexController
{

    public function __construct(\Zend\View\Model\ViewModel $viewModel)
    {
        // TODO: write logic here
    }

    public function indexAction()
    {
        // TODO: write logic here
    }
}

Let’s fulfill the examples as described in spec:

// module/Application/src/Application/Controller/IndexController.php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
    private $viewModel;

    public function __construct(\Zend\View\Model\ViewModel $viewModel)
    {
        $this->viewModel = $viewModel;
    }

    public function indexAction()
    {
        return $this->viewModel;
    }
}

We need to build the Controller with Factory, so we can describe the factory:

$ bin/phpspec desc Application/Factory/Controller/IndexControllerFactory

We can write spec examples:

// module/Application/spec/Application/Factory/Controller/IndexControllerFactorySpec.php
namespace spec\Application\Factory\Controller;

use Application\Controller\IndexController;
use Application\Factory\Controller\IndexControllerFactory;
use PhpSpec\ObjectBehavior;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class IndexControllerFactorySpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(IndexControllerFactory::class);
    }

    function it_is_implements_factory_interface()
    {
        $this->shouldImplement(FactoryInterface::class);
    }

    function it_is_create_indexcontroller(ServiceLocatorInterface $serviceLocator)
    {
        $this->createService($serviceLocator)
             ->shouldReturnAnInstanceOf(IndexController::class);
    }
}

When run bin/phpspec run, we will get generated code:

// module/Application/src/Application/Factory/Controller/IndexControllerFactory.php
namespace Application\Factory\Controller;

class IndexControllerFactory
{

    public function createService(\Zend\ServiceManager\ServiceLocatorInterface $serviceLocatorInterface)
    {
        // TODO: write logic here
    }
}

Let’s modify to fulfill the spec examples:

// module/Application/src/Application/Factory/Controller/IndexControllerFactory.php
namespace Application\Factory\Controller;

use Application\Controller\IndexController;
use Zend\ServiceManager\FactoryInterface;
use Zend\View\Model\ViewModel;

class IndexControllerFactory implements FactoryInterface
{
    public function createService(\Zend\ServiceManager\ServiceLocatorInterface $serviceLocatorInterface)
    {
        $viewModel = new ViewModel();
        return new IndexController($viewModel);
    }
}

So, everything looks good, we can run bin/phpspec run again, and we can get green result again:

run-2-phpspec

We can see the coverage result in coverage/index.html:

cov-phpspec-zf2

Now, to make our ZF2 application still works when call ‘/’ in browser, we can update our module/Application/config/module.config.php:

// ...
    'controllers' => array(
        'factories' => array(
            'Application\Controller\Index' => 'Application\Factory\Controller\IndexControllerFactory'
        ),
    ),
// ...

That’s it ­čśë

Advertisements

2 Responses

Subscribe to comments with RSS.

  1. gbell12 said, on January 25, 2017 at 2:25 pm

    Hi Abdul – I know this is an old post but it’s useful to me TODAY!
    I think it’s really ballsy how you didn’t even start the module with boilerplate code but rather test-drove it. That’s a COMPLETE application of TDD with ZF. Very interesting mindset.

    Are you strict with your unit testing being isolated units? If so, what’s a unit to you? A ZF controller? A ZF module? Or, do you sometimes use real collaborators (ie. Doctrine2)?

    • samsonasik said, on January 25, 2017 at 2:44 pm

      I’m glad it useful for you.

      I use unit first, functional or integration later to make sure everything is hit and no errors. ZF Controller can be both unit and functional or integration “via” test (eg: with real DB with data fixture). ZF Module class can be a unit only test.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: