1

In a symfony 4 project, many services/controllers need log. Trying to use the advantage of traits & autowire options given by symfony, I created a loggerTrait that will be passed to the different services.

namespace App\Helper;

use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;

trait LoggerTrait
{
    /** @var LoggerInterface */
    private $logger;

    /** @var array */
    private $context = [];

    /**
     * @return LoggerInterface
     */
    public function getLogger(): LoggerInterface
    {
        return $this->logger;
    }

    /**
     * @required
     *
     * @param LoggerInterface|null $logger
     */
    public function setLogger(?LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }

    public function logDebug(string $message, array $context = []): void
    {
        $this->log(LogLevel::DEBUG, $message, $context);
    }
...
}

(inspired by symfonycasts.com)

A service will be using this trait

namespace App\Service;

use App\Helper\LoggerTrait;

class BaseService
{
    use LoggerTrait;

    /** @var string */
    private $name;

    public function __construct(string $serviceName)
    {
        $this->name = $serviceName;
    }

    public function logName()
    {
        $this->logInfo('Name of the service', ['name' => $this->name]);
    }
}

It works perfectly but I couldn't succeed to test it.

I tried to extend KernelTestCase in my test to mock an loggerInterface but I receive Symfony\Component\DependencyInjection\Exception\InvalidArgumentException: The "Psr\Log\LoggerInterface" service is private, you cannot replace it which make perfect sens.

Here my test:

namespace App\Tests\Service;

use App\Service\BaseService;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class BaseServiceTest extends KernelTestCase
{
    private function loggerMock()
    {
        return $this->createMock(LoggerInterface::class);
    }

    protected function setUp()
    {
        self::bootKernel();
    }

    /**
     * @test
     * @covers ::logName
     */
    public function itShouldLogName()
    {
        // returns the real and unchanged service container
        $container = self::$kernel->getContainer();

        // gets the special container that allows fetching private services
        $container = self::$container;

        $loggerMock = $this->loggerMock();
        $loggerMock->expect(self::once())
            ->method('log')
            ->with('info', 'Name of the service', ['name' => 'service_test']);

        $this->logger = $container->set(LoggerInterface::class, $loggerMock);


        $baseService = new BaseService('service_test');
        var_dump($baseService->getLogger());
    }
}

Is there a solution to test such a logger inside the service ?

4
  • $baseService->setLogger($loggerMock) should do the trick. No need to fool around with the container or to test the @required functionality.
    – Cerad
    Commented Aug 23, 2019 at 20:10
  • Is it okay to use it in the test even if do not use it in the service ? Commented Aug 26, 2019 at 7:54
  • 1
    setLogger is used in the service. It is just automatically called for you (thanks to @required) by the container. As a general rule, avoid the container for regular unit tests.
    – Cerad
    Commented Aug 26, 2019 at 12:55
  • ok, thank you for confirming this! Commented Aug 26, 2019 at 14:44

1 Answer 1

0

You can override the service to be public (only for the test environment) in your config_test.yml as follows:

services:
  Psr\Log\LoggerInterface:
    public: true

This is commonly done for testing private services.

1
  • 3
    I tried this, but it did not change the service.. I still have "Symfony\Component\DependencyInjection\Exception\InvalidArgumentException: The "Psr\Log\LoggerInterface" service is private, you cannot replace it." error. Commented Aug 26, 2019 at 8:03

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.