Skip to content

How RSpec works

In order to understand how the RSpec integration in SuperDiff works, it’s important to study the pieces in play within RSpec itself.

Context

Imagine a file such as the following:

# spec/some_spec.rb
describe "Some tests" do
  it "does something" do
    expect([1, 2, 3]).to eq([1, 6, 3])
  end
end

Then, imagine that the user runs:

rspec

Without SuperDiff activated, this will produce the following output:

Some tests
  does something (FAILED - 1)

Failures:

  1) Some tests does something
     Failure/Error: expect([1, 2, 3]).to eq([1, 6, 3])

       expected: [1, 6, 3]
            got: [1, 2, 3]

       (compared using ==)
     # ./spec/some_spec.rb:3:in `block (2 levels) in <top (required)>'

Finished in 0.01186 seconds (files took 0.07765 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/some_spec.rb:2 # Some tests does something

Now imagine that we want to modify this output to replace the “expected:”/”actual:” lines with a diff. How would we do this?

RSpec’s cast of characters

First, we will review several concepts in RSpec: 1

What RSpec does

Given the above, RSpec performs the following sequence of events:

  1. The developer adds an failing assertion to a test using the following forms (filling in <actual value>, <matcher>, <block>, and <args...> appropriately):
    • expect(<actual value>).to <matcher>(<args...>)
    • expect { <block> }.to <matcher>(<args...>)
    • expect(<actual value>).not_to <matcher>(<args...>)
    • expect { <block> }.not_to <matcher>(<args...>)
  2. The developer runs the test using the rspec executable.
  3. The rspec executable calls RSpec::Core::Runner.invoke.
  4. Skipping a few steps, RSpec::Core::Runner#run_specs is called, which runs all tests by surrounding them in a call to RSpec::Core::Reporter#report.
  5. Skipping a few more steps, RSpec::Core::Example#run is called to run the current example.
  6. From here one of two paths is followed depending on whether the assertion is positive (.to) or negative (.not_to).
  7. RSpec::Expectations::ExpectationHelper.handle_failure calls RSpec::Expectations.fail_with.
  8. RSpec::Expectations.fail_with creates a diff using RSpec::Matchers::MultiMatcherDiff, wraps it in an exception, and feeds the exception to RSpec::Support.notify_failure.
  9. RSpec::Support.notify_failure calls the currently set failure notifier, which by default raises the given exception.
  10. Returning to RSpec::Core::Example#run, this method rescues the exception and then calls finish, which calls example_failed on the reporter.
  11. RSpec::Core::Reporter#example_failed uses RSpec::Core::Notifications::ExampleNotification.for to construct a notification, which in this case is an RSpec::Core::Notifications::FailedExampleNotification. RSpec::Core::Notifications::FailedExampleNotification in turn constructs an RSpec::Core::Formatters::ExceptionPresenter.
  12. RSpec::Core::Reporter#example_failed then passes the notification object along with an event of :example_failed to the notify method. Because RSpec::Core::Formatters::ProgressFormatter is a listener on the reporter, its example_failed method gets called, which prints a message Failure: to the terminal.
  13. Returning to RSpec::Core::Reporter#report, it now calls finish after all tests are run.
  14. RSpec::Core::Reporter#finish notifies listeners of the :dump_failures event, this time using an instance of RSpec::Core::Notifications::ExamplesNotification. Again, because RSpec::Core::Formatters::ProgressFormatter is registered, its dump_failures method is called, which is actually defined in RSpec::Core::Formatters::BaseTextFormatter.
  15. RSpec::Core::Formatters::BaseTextFormatter#dump_failures calls RSpec::Core::Notifications::ExamplesNotification#fully_formatted_failed_examples.
  16. RSpec::Core::Notifications::ExamplesNotification#fully_formatted_failed_examples formats all of the failed examples by wrapping them in RSpec::Core::Notifications::FailedExampleNotifications and calling fully_formatted on them.
  17. RSpec::Core::Notifications::FailedExampleNotification#fully_formatted then calls fully_formatted on its RSpec::Core::Formatters::ExceptionPresenter.
  18. RSpec::Core::Formatters::ExceptionPresenter#fully_formatted then constructs various pieces of what will eventually be printed to the terminal, including the name of the test, the line that failed, the error and backtrace, and other pertinent details.

  1. Note that the analysis of the RSpec source code in this document is accurate as of RSpec v3.13.0, released February 4, 2024.