halfpastfour.am
halfpastfour.am

Reputation: 5933

Object Mocking - How to replace a factory with a service in the service manager?

I'm having some trouble getting my unit test to work. I'm testing a controller that uses a service that is created by a factory. What I want to achieve is to replace a factory with a mocked service so I can perform tests without using an active database connection.

The setup

In my service manager's configuration file I point to a factory. The factory requires an active database connection that I don't want to use during my unit test.

Namespace MyModule;

return [
    'factories' => [
        MyService::class => Factory\Service\MyServiceFactory::class,
    ],
];

Note: I have changed class names and simplified configuration for illustration purposes.

The service uses a mapper that I won't be going into now because that is not relevant to the situation. The mappers are tested in their own testcases. The service itself has it's own testcase as well but needs to be present for the controller's actions to work.

The controller action simply receives information from the service.

Namespace MyModule\Controller;

use MyModule\Service\MyService;
use Zend\Mvc\Controller\AbstractActionController;

class MyController extends AbstractActionController
{
    /**
     * @var MyService
     */
    private $service;

    /**
     * @param MyService $service
     */
    public function __construct(MyService $service)
    {
        $this->service = $service;
    }


    /**
     * Provides information to the user
     */
    public function infoAction()
    {
        return [
            'result' => $this->service->findAll(),
        ];
    }
}

Note: Again, I have changed class names and simplified the example for illustration purposes.

What I've tried

In my testcase I've tried to override the desired factory like this:

/**
 * @return \Prophecy\Prophecy\ObjectProphecy|MyService
 */
private function mockService()
{
    $service = $this->prophesize(MyService::class);
    $service->findAll()->willReturn(['foo', 'bar']);

    return $service;
}

/**
 * @param \Zend\ServiceManager\ServiceManager $services
 */
private function configureServiceManager(ServiceManager $services)
{
    $services->setAllowOverride(true);
    $services->setService(MyService::class, $this->mockService()->reveal());
    $services->setAllowOverride(false);
}

At first sight this looks great, but it doesn't work. It just seems to append the service to the service manager's list of services, not overriding the factory.

Changing $services->setService to $services->setFactory requires me to build another factory. What I could do is create a factory that injects a mock-mapper into the service but that feels wrong. I'm testing the controller, not the service or mapper so I am trying to avoid complex solutions like that to keep my test cases simple and clear.

Are there any options regarding my situation? Is it possible to override a factory with a service in the service manager or am I looking at it wrong?

Upvotes: 1

Views: 598

Answers (2)

halfpastfour.am
halfpastfour.am

Reputation: 5933

There is no need to build new factories for this. Just use a simple closure instead:

/**
 * @param \Zend\ServiceManager\ServiceManager $services
 */
private function configureServiceManager(ServiceManager $services)
{
    $services->setAllowOverride(true);

    $mockedService = $this->mockService();
    $services->setFactory(MyService::class, function() use ($mockedService) {
        $mockedService->reveal();
    });

    $services->setAllowOverride(false);
}

Now you can still mock only the required service. Adding expectations in the test case is still as flexible as it should be:

public function testMyCase()
{
    $expected = ['foo', 'bar'];
    $this->mockService()->findAll()->willReturn($expected);

    $result = $this->service->findAll();
    $this->assertSame($expected, $result);
}

Upvotes: 0

akond
akond

Reputation: 16035

I think you need a separate config file for unit testing.

phpunit.xml

<?xml version="1.0"?>
<phpunit bootstrap="./Bootstrap.php">

Bootstrap.php

require 'vendor/autoload.php';
$configuration = include 'config/phpunit.config.php';

Zend\Mvc\Application::init ($configuration);

config/phpunit.config.php is a config file created for unit testing only:

config/phpunit.config.php

$configuration = include (__DIR__ . '/application.config.php');
$configuration ['module_listener_options'] ['config_glob_paths'] [] = 'config/phpunit/{,*.}local.php';

config/phpunit/yourfile.local.php

return [
    'service_manager' => array (
        'factories' => [
            MyService::class => ...
        ]
    )
];

In config/phpunit/yourfile.local.php you can let MyService::class be whatever you want, even a closure.

Upvotes: 1

Related Questions