

2 ways to test Rails log messages with RSpec
source link: https://everydayrails.com/2020/08/10/rails-log-message-testing-rspec.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

2 ways to test Rails log messages with RSpec
One thing about writing about Rails for as long as I have is, sometimes I search online to see how to do something, and a post I wrote about it shows up in the first few results! Yet another reason to blog what you learn–you might forget it someday.
Testing log messages is one of those things I don’t do on a regular basis, and always have to root around to find the answer. Which codebase was it again? Which test? And on top of that, there are two (that I know of) ways to do this!
In this article, I’d like to share two approaches to this, along with a little exploration on how the two solutions differ. For the sake of simplicity, I’ll use an RSpec system spec–though in reality, I seldom test log messages unless there’s no other interface to test. In practice, this may be more useful for testing code used in a background job, for example, than in a web interface or API response.
Let’s say I’ve got a controller that, among other things, outputs to the Rails application log:
class HomeController < ApplicationController
def index
# Does some stuff ...
logger.info "Someone visited the site!"
# Does some more stuff ...
end
end
Naively, let’s just add a line indicating that the application logger
(Rails.logger
) should expect
to receive
the intended log message,
and see what happens.
it "logs a message" do
visit root_path
expect(page).to have_content "Welcome to my site!"
expect(Rails.logger).to receive(:info).with("Someone visited the site!")
end
Hmm, that doesn’t work. The test fails with:
Failures:
1) HomeLogs logs a message
Failure/Error: expect(Rails.logger).to receive(:info).with("Someone visited the site!")
(#<ActiveSupport::Logger:0x00007fd777f90078 @level=0, @progname=nil, @default_formatter=#<Logger::Formatter:0x00007fd777f92968 @datetime_format=nil>, @formatter=#<ActiveSupport::Logger::SimpleFormatter:0x00007fd777f90028 @datetime_format=nil, @thread_key="activesupport_tagged_logging_tags:14360">, @logdev=#<Logger::LogDevice:0x00007fd777f92918 @shift_period_suffix="%Y%m%d", @shift_size=1048576, @shift_age=0, @filename="/Users/asumner/code/examples/logging_test_demo/log/test.log", @dev=#<File:/Users/asumner/code/examples/logging_test_demo/log/test.log>, @binmode=false, @mon_data=#<Monitor:0x00007fd777f928c8>, @mon_data_owner_object_id=10440>>).info("Someone visited the site!")
expected: 1 time with arguments: ("Someone visited the site!")
received: 0 times
# ./spec/system/home_logs_spec.rb:12:in `block (2 levels) in <top (required)>'
We need to stub out the Rails logger’s info
method, using RSpec’s
allow
method. This lets us then use expect
to watch for specific
messages being passed to info
:
it "logs a message" do
allow(Rails.logger).to receive(:info)
visit root_path
expect(page).to have_content "Welcome to my site!"
expect(Rails.logger).to receive(:info).with("Someone visited the site!")
end
Wait, that still doesn’t work:
Failures:
1) HomeLogs logs a message
Failure/Error: expect(Rails.logger).to receive(:info).with("Someone visited the site!")
(#<ActiveSupport::Logger:0x00007fed5e70d110 @level=0, @progname=nil, @default_formatter=#<Logger::Formatter:0x00007fed5e70fa00 @datetime_format=nil>, @formatter=#<ActiveSupport::Logger::SimpleFormatter:0x00007fed5e70d0c0 @datetime_format=nil, @thread_key="activesupport_tagged_logging_tags:14360">, @logdev=#<Logger::LogDevice:0x00007fed5e70f9b0 @shift_period_suffix="%Y%m%d", @shift_size=1048576, @shift_age=0, @filename="/Users/asumner/code/examples/logging_test_demo/log/test.log", @dev=#<File:/Users/asumner/code/examples/logging_test_demo/log/test.log>, @binmode=false, @mon_data=#<Monitor:0x00007fed5e70f960>, @mon_data_owner_object_id=10440>>).info("Someone visited the site!")
expected: 1 time with arguments: ("Someone visited the site!")
received: 0 times
Option 1: Expect, then act
I’ll admit it, this behavior stumped me for a long time. I stumbled upon a solution one day, while still relatively new to RSpec and desperately trying anything. Totally by accident, I put the expectation before triggering the case under test:
it "logs a message" do
allow(Rails.logger).to receive(:info)
expect(Rails.logger).to receive(:info).with("Someone visited the site!")
visit root_path
expect(page).to have_content "Welcome to my site!"
end
Wait, why does that work? It’s by design, as shown in the passing examples in RSpec’s documentation on expecting messages. From an English grammar standpoint, this makes sense: You expect something before it happens, not after. But it runs counter to the Arrange, Act, Assert pattern by expecting the specific log message before actually performing an action to test. (You may also know this pattern as Given, When, Then.) In a complex test, this nuance might be easy to miss.
Option 2: Spy on the log
We’re writing Ruby, though, and in Ruby, we like to have more than one
way to do things. Let’s test the same behavior using a spy. A spy
tests the same behavior, but with a slightly different syntax that lets
us expect
, or assert, after acting.
Here’s what it’d look like:
it "logs a message" do
allow(Rails.logger).to receive(:info)
visit root_path
expect(page).to have_content "Welcome to my site!"
expect(Rails.logger).to have_received(:info).with("Someone visited the site!")
end
This passes just fine, and follows the Assemble, Act, Assert the pattern. It lets us arrange the example in such a way that assembly, action, and assertion are distinct steps within the test.
Which way is the right way?
Like I said already, this is Ruby, so we can have more than one right way.
Consider the first approach when the test is closer to a traditional isolated unit test:
- The code being exercised lacks a lot of external dependencies (including Rails)
- The test can be structured so that all assertions are kept together–or better yet, the log assertion is the only thing being checked
Consider a spy when the scenario is more complex:
- The code relies heavily on Rails, Active Record, or anything else that’s not easily abstracted away
- The test asserts outcomes in addition to the log message
I typically prefer using a spy for both cases, since my brain thinks in Given, When, Then, anyway. But you may prefer something else, and that’s totally cool. Just be aware that you may see either approach (or both!) when reading source code someday.
Recommend
-
7
so much funPublished January 23, 2015 #rails #happy_seed
-
12
Work around flaky test failures in Rails with rspec-retry By Aaron Sumner, June 08, 2020. Most of the time, a failing test is a good thing. It points to...
-
16
Setting Up Rails with Rspec From the Start Feb 26, 2017 So this morning I was working on my side project and I realized that one aspect of it alone is now 26 plus PORO (plain old ruby objects) models and growi...
-
15
Ruby 社区一直都喜欢测试驱动开发,Rails 框架内置就有测试模块,本文将介绍如何利用 RSpec 来做单元测试,当然 RSpec 也可以用来做集成测试。 Rails 和 RSpec Rails 项...
-
15
The following is an excerpt from my book, Rails Testing for Beginners. What we’re going to do One of the biggest challenges in getting started with testin...
-
8
Migrate from Rspec to Minitest with Rails New Rspec has released, with lots of DSL changes. Every rspecer is painfull. ^_^ Minitest is so lightweight, cute and simple. Why was I so stuip to choose Rspec? This article w...
-
6
Spec tasks rake spec:controllers rake spec:helpers rake spec:lib rake spec:mailers rake spec:models rake spec:requests rake spec:routing rake spec:views Models # spec/models/*.rb describe MyM...
-
13
I have an article already on File Uploading in GraphQL API in Rails with ActiveStorage. After that article, codelion asked how to unit...
-
11
Is there a better way to test this Rails controller with RSpec? advertisements I have an Account model that validates that...
-
10
Back to Rails Test - Putting Rspec in the Rear View - Test Examples Jun 5, 2022 I find myself building a new application, Cartazzi, and since the "template app" I'm...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK