Testing
Contents
When testing application code, you often want to assert that it logged the right thing: the expected message, level, payload, or metric. Semantic Logger logs through a global, asynchronous pipeline, so you cannot simply read it back from a normal appender. Instead it ships a test helper that captures log events in memory during a block, so you can make assertions on them.
The events are captured as raw SemanticLogger::Log objects, before any appender or formatter runs,
so your assertions are not affected by how logging happens to be configured.
Helpers are provided for both Minitest and RSpec. Other frameworks are covered further down.
Minitest
Step 1: install the helpers
Add the helper methods to your tests, once, in test_helper.rb:
Minitest::Test.include SemanticLogger::Test::Minitest
Step 2: capture events
Wrap the code under test in semantic_logger_events. It returns every log event created during the
block:
messages = semantic_logger_events do
User.new.enable!
end
By default it captures events from every class. To capture only the events from one class, pass that class:
messages = semantic_logger_events(ApiClient) do
# Only ApiClient log events created during this block are captured.
end
Step 3: assert on the events
Check how many events were produced, then assert on each one with assert_semantic_logger_event:
require_relative "test_helper"
class UserTest < ActiveSupport::TestCase
describe User do
it "logs message" do
messages = semantic_logger_events do
User.new.enable!
end
# How many events were logged
assert_equal 2, messages.count, messages
# The first event
assert_semantic_logger_event(
messages[0],
level: :info,
message: "User enabled"
)
# The second event
assert_semantic_logger_event(
messages[1],
level: :debug,
message: "Completed"
)
end
end
end
Every argument other than the event itself is optional, so you assert only on what matters to the test. The available checks are:
| Argument | Checks |
|---|---|
message |
Exact match of the text message. |
message_includes |
Partial (substring) match of the message. |
level |
Log level: :trace, :debug, :info, :warn, :error, :fatal. |
payload |
Exact match of the payload Hash. |
payload_includes |
Partial match: the given keys/values, ignoring any others. |
tags |
Tags active on the thread when logged. |
named_tags |
Named tags active on the thread when logged. |
name |
Class name of the logger. |
exception |
The Ruby exception that was logged. |
metric |
The metric name. |
metric_amount |
The metric amount. |
dimensions |
The metric dimensions. |
context |
Named contexts captured with the entry. |
time |
When the entry was created. |
duration |
Duration of a measure call, in milliseconds. |
backtrace |
The captured backtrace, if any. |
thread_name |
Name of the thread that logged the entry. |
Match part of a message or payload
For assertions that should not break on incidental detail, match partially.
Use message_includes for a substring of the message:
assert_semantic_logger_event(
messages[0],
level: :info,
message_includes: "enabled"
)
Use payload_includes to assert specific payload keys while ignoring any extras. Compare with
payload, which must match the whole Hash exactly:
# Exact: the payload must be exactly these keys and values
assert_semantic_logger_event(
messages[0],
level: :info,
message: "User enabled",
payload: {first_name: "Jack", last_name: "Jones"}
)
# Partial: first_name must be present, other keys are ignored
assert_semantic_logger_event(
messages[0],
level: :info,
message: "User enabled",
payload_includes: {first_name: "Jack"}
)
For more examples, see the Rails Semantic Logger tests.
RSpec
The RSpec helpers mirror the Minitest ones: a capture helper plus matchers for asserting on the captured events.
Step 1: install the helpers
Require the helpers and include them, once, in spec_helper.rb:
require "semantic_logger/test/rspec"
RSpec.configure do |config|
config.include SemanticLogger::Test::RSpec
end
Step 2: capture events
Wrap the code under test in capture_semantic_logger_events. It returns every log event created
during the block, regardless of the global default log level:
events = capture_semantic_logger_events do
User.new.enable!
end
By default it captures events from every class. To capture only the events from one class, pass that class:
events = capture_semantic_logger_events(ApiClient) do
# Only ApiClient log events created during this block are captured.
end
Step 3: assert on the events
Use be_a_semantic_logger_event to assert on a single captured event:
RSpec.describe User do
it "logs message" do
events = capture_semantic_logger_events do
User.new.enable!
end
expect(events.count).to eq(2)
expect(events[0]).to be_a_semantic_logger_event(
level: :info,
message: "User enabled"
)
expect(events[1]).to be_a_semantic_logger_event(
level: :debug,
message: "Completed"
)
end
end
Every argument is optional, so you assert only on what matters to the test. The available checks are
the same as the Minitest table above, plus message_includes,
payload_includes, and exception_includes for partial matches:
expect(events[0]).to be_a_semantic_logger_event(
level: :info,
message_includes: "enabled",
payload_includes: {first_name: "Jack"}
)
An expected value that is a Class matches by type, and :nil asserts the attribute is nil:
expect(events[0]).to be_a_semantic_logger_event(time: Time, exception: :nil)
Match against a list of events
a_semantic_logger_event is a composable alias, so it works inside include and other matchers:
expect(events).to include(
a_semantic_logger_event(message: "User enabled")
)
Assert directly on a block
log_semantic_logger_event captures the events for you and passes if any of them match. Pass on:
to capture only one class’s events:
expect { User.new.enable! }.to(
log_semantic_logger_event(level: :info, message: "User enabled")
)
expect { ApiClient.new.call }.to(
log_semantic_logger_event(on: ApiClient, metric: "ApiClient/call")
)
Other test frameworks
The same approach works anywhere: replace the logger with a
SemanticLogger::Test::CaptureLogEvents instance. It looks and behaves like a normal logger, but
keeps the raw log events in memory instead of writing them, so your assertions are unaffected by the
configured appenders or their formats.
logger = SemanticLogger::Test::CaptureLogEvents.new
Stub it onto a single class to capture just that class’s logging:
User.stub(:logger, logger) do
# Capture all logging calls to the User logger.
end
Or stub the global processor to capture logging from every class:
SemanticLogger::Logger.stub(:processor, logger) do
# Capture all log events during the block.
end
Either way, the captured events are available as logger.events.
If you add helper methods for another framework like the Minitest ones, a pull request would be welcome.