Vortos
Logger

Testing

How to use NullLogger in unit tests and capture log output in integration tests.

Testing

Unit Tests — NullLogger

For unit tests where you don't care about log output, inject a NullLogger. It satisfies LoggerInterface and discards everything silently:

use Psr\Log\NullLogger;

final class RegisterUserHandlerTest extends TestCase
{
    public function test_registers_user(): void
    {
        $handler = new RegisterUserHandler(
            repository: new InMemoryUserRepository(),
            logger:     new NullLogger(),
        );

        $handler(new RegisterUser(email: 'alice@example.com'));

        // assert domain state — log output doesn't matter
    }
}

NullLogger is part of psr/log — no additional package needed.

Integration Tests — Capturing Logs

For tests that need to assert a specific log entry was produced, use Monolog's TestHandler:

use Monolog\Handler\TestHandler;
use Monolog\Level;
use Monolog\Logger;

final class DeadLetterWriterTest extends TestCase
{
    public function test_logs_critical_on_dead_letter(): void
    {
        $handler = new TestHandler();
        $logger  = new Logger('test', [$handler]);

        $writer = new DeadLetterWriter(
            connection: $this->createMock(Connection::class),
            logger:     $logger,
        );

        $writer->write(
            transportName: 'user.events',
            eventClass:    UserRegisteredEvent::class,
            handlerId:     'user.registered',
            payload:       '{}',
            headers:       [],
            failureReason: 'Connection refused',
            exceptionClass: \RuntimeException::class,
            attemptCount:  4,
        );

        $this->assertTrue($handler->hasCriticalThatContains('dead-lettered'));
    }
}

TestHandler Assertions

MethodWhat it checks
hasRecords(Level)Any record at this level
hasDebug(string) / hasInfo(string) / hasWarning(string) / hasError(string) / hasCritical(string)Exact message at level
hasDebugThatContains(string) / hasErrorThatContains(string) / etc.Message containing substring
hasRecordThatMatches(pattern, Level)Regex match on message
getRecords()All captured records as arrays

Swapping the Driver in Tests

If your test suite registers services using LoggerInterface, configure the test environment to use a NullLogger globally. Add config/test/logging.php:

config/test/logging.php
use Monolog\Level;
use Vortos\Logger\Config\LogChannel;
use Vortos\Logger\DependencyInjection\VortosLoggingConfig;

return static function (VortosLoggingConfig $config): void {
    // Silence all framework channels in tests
    $config->disableChannel(
        LogChannel::Http,
        LogChannel::Cqrs,
        LogChannel::Messaging,
        LogChannel::Cache,
        LogChannel::Query,
    );

    // Raise the minimum level for the app channel so only CRITICAL leaks through
    $config->channel(LogChannel::App)->level(Level::Critical);
};

This keeps test output clean without requiring every test to inject a NullLogger manually.

Buffer Flushing in Tests

Batched sinks flush via FlushScheduler at the end of the HTTP/console lifecycle and on a periodic timer — neither of which fires mid-test in PHPUnit. Use TestHandler (which bypasses buffering entirely) for assertions, or make a sink write-through in the test config:

$config->sink(LogChannel::App->value)->writeThrough();

Asserting No Unexpected Logs

Sometimes the important assertion is that a code path did not log an error:

$handler = new TestHandler();
$logger  = new Logger('test', [$handler]);

// run the code under test

$this->assertFalse(
    $handler->hasErrorRecords(),
    'Expected no errors but got: ' . json_encode($handler->getRecords())
);

On this page