fr3d/psr3-message-assertions
PHPUnit helper to assert your application’s PSR-3 log messages follow the Logger spec. Use the included TestLogger as a logger dependency during tests to validate message formatting and context placeholders. Composer-installable, BSD-2-Clause.
Install the Package
composer require fr3d/psr3-message-assertions
Replace Your Logger in Tests
In your test class, inject TestLogger (from fr3d\Psr3MessageAssertions\PhpUnit\TestLogger) instead of your production logger. Example:
use fr3d\Psr3MessageAssertions\PhpUnit\TestLogger;
public function testLoggerCompliance()
{
$logger = new TestLogger();
$this->app->instance(\Psr\Log\LoggerInterface::class, $logger);
// Trigger logging (e.g., via a service or facade)
Log::info('Test message');
}
First Use Case Run a test where you log messages. If any message violates PSR-3 (e.g., non-string context values, invalid log levels), the test will fail with a descriptive assertion message.
Unit Testing Log Compliance
Use TestLogger in unit tests to enforce PSR-3 compliance for critical logging paths (e.g., error handling, auditing). Example:
public function testErrorLogging()
{
$logger = new TestLogger();
Log::setLogger($logger); // If using a facade
// Simulate an error
Log::error('Failed to process', ['user_id' => 123, 'invalid_key' => new stdClass()]);
// Fails: `invalid_key` is not a string, scalar, or array of scalars.
}
Integration Testing Replace the logger in integration tests where external services (e.g., Sentry, Monolog) are involved. Verify logs are structured correctly before they reach downstream systems.
Custom Assertions
Extend TestLogger to add custom validation (e.g., regex patterns for message formats):
use fr3d\Psr3MessageAssertions\PhpUnit\TestLogger;
class CustomTestLogger extends TestLogger
{
protected function assertMessageFormat(string $message): void
{
if (!preg_match('/^\[ERROR\] .*/', $message)) {
$this->fail("Error message must start with '[ERROR]'");
}
}
}
Monolog Integration
Wrap Monolog’s Logger with TestLogger to validate logs before they’re processed:
$monolog = new Monolog\Logger('name');
$testLogger = new TestLogger();
$testLogger->pushHandler($monolog->getHandlers()[0]);
Log::setLogger($testLogger);
Facade Usage
If using Laravel’s Log facade, bind TestLogger in tests:
$this->app->bind(\Psr\Log\LoggerInterface::class, function () {
return new TestLogger();
});
Mocking vs. Assertions
Prefer TestLogger over mocks when you need to verify PSR-3 compliance (not just check if logging was called). Mocks won’t catch invalid message formats.
False Positives in Context
TestLogger rejects non-string/non-scalar context values (e.g., objects, resources). If your app intentionally logs objects (e.g., for debugging), either:
TestLogger to allow specific classes:
protected function assertContext(array $context): void
{
foreach ($context as $key => $value) {
if (!is_scalar($value) && !$value instanceof AllowedClass) {
$this->fail("Context value for '$key' must be scalar or AllowedClass");
}
}
}
Level Validation
The package enforces PSR-3 log levels (DEBUG, INFO, etc.). If you use custom levels (e.g., CUSTOM), extend TestLogger to whitelist them:
protected function assertLevel($level): void
{
$validLevels = array_merge(parent::getValidLevels(), ['CUSTOM']);
if (!in_array($level, $validLevels)) {
$this->fail("Invalid log level: $level");
}
}
Performance in CI
TestLogger adds overhead. Disable it in non-test environments or use it selectively:
if (app()->environment('testing')) {
$this->app->bind(\Psr\Log\LoggerInterface::class, TestLogger::class);
}
Facade Caching Laravel’s service container caches bindings. Clear the cache after tests if reusing the same container:
$this->app->forgetInstance(\Psr\Log\LoggerInterface::class);
Inspect Failed Assertions
TestLogger provides detailed failure messages. Example output:
Failed asserting that log message 'User loaded' has a valid context.
Context key 'user' has value [object](App\Models\User).
Expected scalar or array of scalars.
Log Level Mismatches
If tests fail with "Invalid log level," check for typos (e.g., log::emergency() vs. Log::emergency()).
Context Arrays Nested arrays in context are allowed, but all leaf values must be scalar. Flatten them if needed:
Log::info('Data', ['nested' => ['id' => 123, 'name' => 'Test']]); // Passes
Log::info('Data', ['nested' => ['obj' => new stdClass()]]); // Fails
Custom Assertions Override methods like:
assertMessage(): Validate message format (e.g., required prefixes).assertContext(): Relax scalar requirements for specific keys.assertLevel(): Add custom log levels.Handler Integration
Extend TestLogger to support PSR-3 handlers:
class HandlerTestLogger extends TestLogger
{
public function pushHandler(HandlerInterface $handler)
{
$this->handlers[] = $handler;
}
}
Laravel-Specific Extensions
For Laravel, create a test trait to auto-configure TestLogger:
trait LogsPsr3Compliant
{
protected function setUp(): void
{
$this->app->bind(\Psr\Log\LoggerInterface::class, TestLogger::class);
}
}
How can I help you explore Laravel packages today?