12

I'm using Monolog inside Symfony2, using the default MonologBundle. I'm trying to assert inside my tests, that a line is logged. I've configured this in my config_test.yml:

monolog:
    handlers:
        main:
            type:   test
            level:  debug

How do I get to the results of Monolog's TestHandler in my tests (that inherit from Symfony2's WebTestCase)?

Aerendir
  • 6,152
  • 9
  • 55
  • 108
hvtilborg
  • 1,397
  • 11
  • 21

3 Answers3

6

As solution:

Get all handlers from monolog service and search test handler.

foreach ($this->container->get('monolog')->getHandlers() as $handler) {
  if ($handler instanceof TestHandler) {
    $testHandler = $handler;
    break;
  }
}

if (!$testHandler) {
  throw new \RuntimeException('Oops, not exist "test" handler in monolog.');
}

$this->assertFalse($testHandler->hasCritical()); // Or another assertions
ZhukV
  • 2,892
  • 6
  • 25
  • 36
  • 1
    Note that : This method is deprecated in Symfony 3.4 and will break in Symfony 4 – aneth101 Mar 19 '18 at 17:14
  • Not sure about modern Symfony, but it works for Laravel 9 which uses Monolog 2 under the hood. – kdmitry Feb 17 '23 at 12:27
  • In new version of Symfony TestHandler can be retrieve with `self::getContainer()->get('monolog.handler.test')` – Mcsky May 25 '23 at 12:19
4

In your command class, you have to simply set the handler with pushHandler():

namespace AppBundle\Command;

use Symfony\Bridge\Monolog\Handler\ConsoleHandler;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class YourCommand extends ContainerAwareCommand
{
    // ...

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $logger = $this->getContainer()->get('logger');

        // PUSH THE OutputInterface OBJECT INTO MONOLOG
        $logger->pushHandler(new ConsoleHandler($output));

        // Your command logic here...
    }

In your test, using CommandTester:

namespace AppBundle\Tests\Command;

use AppBundle\Command\YourCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;

class YourCommandTest extends KernelTestCase
{
    public function testExecute()
    {
        $kernel = $this->createKernel();
        $kernel->boot();

        // mock the Kernel or create one depending on your needs
        $application = new Application($kernel);
        $application->add(new YourCommand());

        $command = $application->find('acme:your:command');

        $commandTester = new CommandTester($command);
        $commandTester->execute(
            array('command'   => $command->getName()),
            /**
             * Here set the verbosity
             */
            array('verbosity' => OutputInterface::VERBOSITY_DEBUG)
        );

        // die(print_r($commandTester->getDisplay()));

        $this->assertRegExp('/.../', $commandTester->getDisplay());
    }
}

Keep attention to array('verbosity' => OutputInterface::VERBOSITY_DEBUG).

This way you'll can obtain all the logs (a INFO in this case, set with $logger->info('Starting <info>acme:your:command</info>');):

[2015-08-13 23:39:22] app.INFO: Starting acme:your:command: 

Now you can use $this->assertRegExp()to check if a particular line is logged or not.

You can also transform the string in an array with

explode('\n', $commandTester->getDisplay())

This solution were found here and is explained in the documentation of Monolog here.

More about Monolog and Symfony (Symfony Docu).

More about Monolog Handlers (Monolog Docu).

Symfony 5 (autowiring), PHP 7.4

namespace App\Command;

use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Monolog\Handler\ConsoleHandler;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class YourCommand extends Command
{
    protected static $defaultName = 'acme:your:command';
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // PUSH THE OutputInterface OBJECT INTO MONOLOG
        if ($this->logger instanceof Logger) {
            $this->logger->pushHandler(new ConsoleHandler($output));
        }

        // Your command logic here...

        return self::SUCCESS;
    }
}

In your test, using CommandTester:

namespace AppBundle\Tests\Command;

use AppBundle\Command\YourCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;

class YourCommandTest extends KernelTestCase
{
    public function testExecute()
    {
        $kernel = static::createKernel();
        $application = new Application($kernel);

        $command = $application->find('acme:your:command');
        $commandTester = new CommandTester($command);
        $commandTester->execute(
            ['command'   => $command->getName()],
            /**
             * Here set the verbosity
             */
            ['verbosity' => OutputInterface::VERBOSITY_DEBUG]
        );

        $output = $commandTester->getDisplay();
        // die(print_r($commandTester->getDisplay()));

        self::assertStringContainsString('/.../', $$output);
    }
}
Aerendir
  • 6,152
  • 9
  • 55
  • 108
  • I wasn't trying to assert log lines in a command. A more generic, overall solution would be appreciated. – hvtilborg Aug 14 '15 at 09:29
  • I think I don't understand what you would like to achieve... Can you update your question with some more details? Post the code of the test, so we can see how you implemented it and give you a solution. – Aerendir Aug 14 '15 at 11:58
0

Quite old question but giving my 2 cents.

Handlers are all registered as services, thus you can easily use dependency injection.

In packages/test/monolog.yaml

monolog:
  handlers:
    testing:
      type: test
      level: info

Then, in your test:

use Monolog\Handler\TestHandler;

// ...

private TestHandler $testHandler;

public function setUp(): void
{
    parent::setUp();

    $this->testHandler = $this->get('monolog.handler.testing');
}

public function testSomething()
{
    // ...

    // Get records
    $this->testHandler->getRecords();
}

If you have a base class for your tests, you can create a helper:

protected function assertIsLogged(string $partOfMessage, int $level = Logger::DEBUG): void
{
    /** @var TestHandler $handler */
    $handler = $this->get('monolog.handler.testing');

    foreach ($handler->getRecords() as $record) {
        // Ignore logs below expected level
        if ($record['level'] < $level) {
            continue;
        }

        if (\str_contains($record['message'], $partOfMessage)) {
            $this->addToAssertionCount(1);

            return;
        }
    }

    $this->fail(sprintf('Failed to assert that a log contains "%s" for minimum level %s.', $partOfMessage, Logger::getLevelName($level)));
}
Alain Tiemblo
  • 36,099
  • 17
  • 121
  • 153