mock service in DI container

jz
Member | 1
+
0
-

Hi,

I'm writing integration tests for Nette app and I'd like to have an easy way to inject a mocked service/method into the DI container for specific test. I'm able to do it as you can see the code bellow. It is working for simple cases or only if I'm running just one test.

The problem is, that some services (created by previous tests) could hold the service as a private property (coming from the constructor). I'd need to inject the mock in all services dependent on the service I'd like to mock (or at least to force to recreate it again with mock as dependency).

Do you know how could I list all services which are dependent on specified service?

Regards

Jan

abstract class IntegrationTestCaseBase {
    protected Nette\DI\Container $diContainer;

// ....

    /**
     * @param class-string $diServiceType
     * @param mixed        $return
     *
     * EXPERIMENTAL! other services dependent on this service are holding the original service!
     */
    public function doubleDIServiceMethod(string $diServiceType, string $methodName, $return): void
    {
        $mock = $this->getMockForDIService($diServiceType);
        /** @var \Mockery\ExpectationInterface $exp */
        $exp = $mock->shouldReceive($methodName);
        $exp->andReturn($return);
    }

    /**
     * @param class-string $diServiceType
     * @return \Mockery\MockInterface|\Mockery\LegacyMockInterface
     */
    public function getMockForDIService(string $diServiceType)
    {
        $wiredClassName = $this->getWiredClassName($diServiceType);
        if (!isset($this->originalServiceList[$wiredClassName])) {
            $this->originalServiceList[$wiredClassName] = $this->diContainer->getService($wiredClassName);
        }
        if (!isset($this->mockedServiceList[$diServiceType])) {
            $this->mockedServiceList[$diServiceType] = Mockery::mock($this->originalServiceList[$wiredClassName]);
        }
        $this->diContainer->removeService($wiredClassName);
        $this->diContainer->addService($wiredClassName, $this->mockedServiceList[$diServiceType]);
        return $this->mockedServiceList[$diServiceType];
    }


    /**
     * @param class-string $diServiceType
     */
    private function getWiredClassName(string $diServiceType): string
    {
        if (!isset($this->wiredClassNameList[$diServiceType])) {
            $this->wiredClassNameList[$diServiceType] = $this->diContainer->findAutowired($diServiceType)[0];
        }
        return $this->wiredClassNameList[$diServiceType];
    }

    private function restoreOriginalServicesInDIContainer(): void
    {
        foreach ($this->originalServiceList as $wiredClassName => $originalService) {
            $this->diContainer->removeService($wiredClassName);
            $this->diContainer->addService($wiredClassName, $originalService);
        }
    }
}
David Grudl
Nette Core | 7824
+
0
-

Nette DI can't return this.

mystik
Member | 109
+
0
-

Best way is to recreate new container for each test. But for performance reasons in some tests we sometimes use one ugly hack. We have utility class (see bellow) to get all services in container by reflection and then we just remove all of them by caling ‘removeService’ that forces their recreation. Be aware that this could break any time if Nette changes anyhing in DI internals so use with caution and only when container recreation is performance killer.

<?php

namespace Metis\DI;

use Nette\DI\Container;
use Nette\Reflection\ClassType;
use Nette\Reflection\Method;
use ReflectionNamedType;

class ContainerUtils {

  /**
   * @param Container $container Container
   * @return array service name => service type
   */
  public static function getAllServicesFrom(Container $container) {
    $services = array();
    /** @var Method $method */
    foreach(ClassType::from($container)->getMethods() as $method) {
      if(preg_match('#^createService_*(.+)\z#', $method->getName(), $match)) {
        $index = str_replace('__', '.', strtolower(substr($match[1], 0, 1)) . substr($match[1], 1));
        $returnType = $method->getReturnType();
        if($returnType instanceof ReflectionNamedType) {
          $services[$index] = $returnType->getName();
        }
      }
    }
    return $services;
  }

}

mystik
Member | 109
+
0
-

Another option if you can easily modify config for tests would be crating own Container implementation (extending basic Nette DI Container) and add method that erase protected $instances property. Then use this implementation by setting configuration value di>parentClass. Generated containers would then be offsprings of your extended class.