Unit Testing Log Messages in Laravel 5.6

When writing unit tests for your Laravel application, you may find yourself needing to assert that certain messages are logged based on some condition. Under the hood, Laravel’s logging service uses Monolog, a PSR-3 logging package which uses a handler stack that allows us to log to different mediums such as files, email, and much more. When writing tests around logging, we want to use the TestHandler, which simply stores every log message into an array that can then be accessed to assert that the given message was in fact logged. By configuring Monolog with the TestHandler, we can assert a given message is logged as follows:

/** @test */
public function it_logs_a_message_on_failure()
{
// Call a method that should create a log message
(new Service())->methodThatFails();
    // Retrieve the records from the Monolog TestHandler
$records = app('log')
->getHandlers()[0]
->getRecords();
    $this->assertCount(1, $records);
$this->assertEquals(
'The service failed',
$records[0]['message']
);
}

For this to work, however, we need to change the handler that Laravel configures Monolog with. By default it uses the StreamHandler which writes to a log file, but we want it to use the TestHandler so that we can check the messages in our tests. Prior to Laravel 5.6, we could drop the following code in our base test case’s setUp() method:

$this->app->configureMonologUsing(function ($monolog) {
$monolog->pushHandler(new \Monolog\Handler\TestHandler());
});

Sweet! Now when Laravel bootstraps an application instance before each test method, we will push the TestHandler as the only handler and all logs will be stored in its array. Our unit tests can now assert against it.

This is all fine and dandy if you are still on Laravel 5.5 or lower, but changes to the logging service in 5.6 require a different approach.

Configuring Monolog for Unit Tests in Laravel 5.6

Some vast improvements to Laravel’s logging service were introduced in the most recent update that give you greater control over logging in your application. Despite these improvements, some breaking changes were introduced that are covered in the upgrade guide. Among these changes is the removal of the configureMonologUsing() method we utilized to set the proper TestHandler . We now need to take a different approach.

In Laravel 5.6, rather than configuring Monolog using a callback in a service provider (or, in our case, the setUp() method of our test class), we need to define an invokeable custom log channel that returns the full Monolog instance we want to use.

If you take a look at that documentation link, you’ll see that we define the class, then register the channel in the config/logging.php configuration file. Cool, so let’s create a test logger and register it!

app/Logging/CreateTestLogger.php

<?php

namespace App\Logging;

use Monolog\Logger;
use Monolog\Handler\TestHandler;
class CreateTestLogger
{
/**
* Create a custom Monolog instance.
*
* @param array $config
* @return \Monolog\Logger
*/
public function __invoke(array $config)
{
$monolog = new Logger('test');
$monolog->pushHandler(new TestHandler());

return $monolog;
}
}

config/logging.php

'channels' => [
'test' => [
'driver' => 'custom',
'via' => App\Logging\CreateTestLogger::class,
],
],

Now, we can set the LOG_CHANNEL environment variable within phpunit.xml to the value of test, the name given to our test logger:

<env name="LOG_CHANNEL" value="test"/>

Beautiful! If we run the same test method from the beginning of this article, everything should work as expected.

However, what if you are developing a Laravel package that does not have access to the logging configuration? How would you set the proper TestHandler in such a case? Our solution only works if you are unit testing the actual application. One solution is to “hack” the configuration in our test class’ setUp() method:

/**
* {@inheritdoc}
*/
public function setUp()
{
parent::setUp();
    config([
'logging.channels' => [
'test' => [
'driver' => 'custom',
'via' => function () {
$monolog = new Logger('test');
$monolog->pushHandler(new TestHandler());
                    return $monolog;
},
],
],
]);
}

It’s a bit messy but as it turns out, you can pass a closure to the via option rather than a class name. All we’re doing here is setting the logging.channels configuration value at run time of our tests and defining the test driver there, rather than in an actual configuration file. This also replaces the need for our CreateTestLogger class, as the Monolog instance is returned via a closure. (Note that you still need to set the environment variable in your PHPUnit configuration file)

This is a useful approach for package developers who need to test that log messages are sent.

Conclusion

Laravel 5.6 introduced some breaking changes to how logging is handled. While more robust, a new approach has to be used to properly test against the logging service in your Laravel application or package. By configuring a custom log channel and setting the environment variable to use said channel, you can rest easy knowing that your log messages are working as expected!

Like what you read? Give Sam Rapaport a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.