Testing

When testing applications that use dry-logger, you'll want to verify that your code logs correctly without cluttering test output.

Using StringIO

The simplest approach is to log to a StringIO object, which you can inspect in your tests:

require "stringio"

RSpec.describe MyClass do
  let(:log_output) { StringIO.new }
  let(:logger) { Dry.Logger(:test, stream: log_output) }

  it "logs the operation" do
    subject = MyClass.new(logger: logger)
    subject.perform

    expect(log_output.string).to include("Operation completed")
  end
end

Testing log content

String format

For human-readable assertions:

RSpec.describe UserService do
  let(:log_output) { StringIO.new }
  let(:logger) do
    Dry.Logger(:test,
      stream: log_output,
      formatter: :string,
      template: :details
    )
  end

  it "logs user creation" do
    service = UserService.new(logger: logger)
    service.create_user(email: "test@example.com")

    expect(log_output.string).to include("User created")
    expect(log_output.string).to include('email="test@example.com"')
  end
end

JSON format

For structured assertions:

RSpec.describe UserService do
  let(:log_output) { StringIO.new }
  let(:logger) do
    Dry.Logger(:test, stream: log_output, formatter: :json)
  end

  it "logs user creation with correct data" do
    service = UserService.new(logger: logger)
    service.create_user(email: "test@example.com")

    log_entry = JSON.parse(log_output.string)
    expect(log_entry["message"]).to eq("User created")
    expect(log_entry["severity"]).to eq("INFO")
    expect(log_entry["email"]).to eq("test@example.com")
  end
end

Testing multiple log entries

When your code logs multiple times:

RSpec.describe OrderProcessor do
  let(:log_output) { StringIO.new }
  let(:logger) { Dry.Logger(:test, stream: log_output, formatter: :json) }

  it "logs each step of order processing" do
    processor = OrderProcessor.new(logger: logger)
    processor.process(order_id: 123)

    logs = log_output.string.split("\n").map { |line| JSON.parse(line) }

    expect(logs[0]["message"]).to eq("Order received")
    expect(logs[1]["message"]).to eq("Payment processed")
    expect(logs[2]["message"]).to eq("Order completed")
  end
end

Testing log levels

Verify that your code logs at the correct severity:

RSpec.describe ErrorHandler do
  let(:log_output) { StringIO.new }
  let(:logger) { Dry.Logger(:test, stream: log_output, formatter: :json) }

  it "logs errors at ERROR level" do
    handler = ErrorHandler.new(logger: logger)
    handler.handle_error(StandardError.new("Something went wrong"))

    log_entry = JSON.parse(log_output.string)
    expect(log_entry["severity"]).to eq("ERROR")
    expect(log_entry["message"]).to eq("Something went wrong")
  end
end

Suppressing logs in tests

Null device

Send logs to the null device to discard them:

# spec/spec_helper.rb
RSpec.configure do |config|
  config.around(:each) do |example|
    # Suppress all logging during tests
    logger = Dry.Logger(:test, stream: File.open(File::NULL, "w"))

    # Make it available to your app
    allow(MyApp).to receive(:logger).and_return(logger)

    example.run
  end
end

High log level

Set the log level to :fatal or above to suppress most logs:

RSpec.configure do |config|
  config.before(:each) do
    @original_logger = MyApp.logger
    MyApp.logger = Dry.Logger(:test, level: :fatal)
  end

  config.after(:each) do
    MyApp.logger = @original_logger
  end
end

Per-test control

Use RSpec metadata to control logging per test:

RSpec.configure do |config|
  config.around(:each) do |example|
    if example.metadata[:show_logs]
      example.run
    else
      logger = Dry.Logger(:test, stream: File.open(File::NULL, "w"))
      allow(MyApp).to receive(:logger).and_return(logger)
      example.run
    end
  end
end

# Enable logging for specific tests
RSpec.describe MyClass do
  it "does something", show_logs: true do
    # Logs will be visible for this test
  end

  it "does something else" do
    # Logs suppressed (default)
  end
end

Testing with dependency injection

Make loggers injectable for easier testing:

class UserService
  def initialize(logger: Dry.Logger(:user_service))
    @logger = logger
  end

  def create_user(email:)
    @logger.info("Creating user", email: email)
    # ... create user
    @logger.info("User created", email: email)
  end
end

# In tests
RSpec.describe UserService do
  let(:log_output) { StringIO.new }
  let(:logger) { Dry.Logger(:test, stream: log_output) }
  let(:service) { UserService.new(logger: logger) }

  it "logs user creation" do
    service.create_user(email: "test@example.com")
    expect(log_output.string).to include("User created")
  end
end

Testing filters

Verify that sensitive data is properly filtered:

RSpec.describe PaymentProcessor do
  let(:log_output) { StringIO.new }
  let(:logger) do
    Dry.Logger(:test,
      stream: log_output,
      formatter: :json,
      filters: [:card_number, :cvv]
    )
  end

  it "filters sensitive payment data" do
    processor = PaymentProcessor.new(logger: logger)
    processor.process(card_number: "4111111111111111", cvv: "123", amount: 99.99)

    log_entry = JSON.parse(log_output.string)
    expect(log_entry["card_number"]).to eq("[FILTERED]")
    expect(log_entry["cvv"]).to eq("[FILTERED]")
    expect(log_entry["amount"]).to eq(99.99)
  end
end

Testing custom formatters

If you've created custom formatters, test them directly:

RSpec.describe MyCustomFormatter do
  let(:formatter) { MyCustomFormatter.new }
  let(:entry) do
    Dry::Logger::Entry.new(
      clock: Dry::Logger::Clock.new,
      progname: "test",
      severity: :info,
      message: "Test message",
      payload: {user_id: 42}
    )
  end

  it "formats entries correctly" do
    output = formatter.call(:info, Time.now, "test", entry)
    expect(output).to include("Test message")
    expect(output).to include("user_id=42")
  end
end

octocatEdit on GitHub