DEV Community

Augusts Bautra
Augusts Bautra

Posted on

More readable failure messages for RSpec #change

The fact that change failure messages for bulky objects like arrays of hashes or just large hashes has griped me for a while and today I decided to use my pre-vacation day when tying up loose ends to see if I can override RSpec's built-in failure message for change matcher to be more readable.

Built-in:

expected `object.data` to have changed to { a: 1 } but, is now { b: 2 }
Enter fullscreen mode Exit fullscreen mode

Desired (multi-line, values aligned):

expected `object.data` to have changed, but the value did not match expectation:
  expected: { a: 1 }
    actual: { b: 2 }
Enter fullscreen mode Exit fullscreen mode

This turned out to be a bit more involved than monkeypatching a single message method. When you use a compound change {}.and(change {}) structure, RSpec uses RSpec::Matchers::BuiltIn::Compound::And matcher, instead of RSpec::Matchers::BuiltIn::Change, so overriding in several places is necessary.

It can probably be achieved more cleanly by someone more familiar with RSpec's architecture and codebase, but I achieved my goal with this code:

# place in some /spec/support/respec_change_message_patch.rb

CHANGE_FAILURE_REGEX = %r'\Aexpected `(?<subject>[^`]+)` to have changed to (?<expected>.+), but is now (?<actual>.+)\z'm

CHANGE_FAILURE_MESSAGE_IMPROVER = ->(original_message) {
  match = original_message.to_s.match(CHANGE_FAILURE_REGEX)

  if match.present?
    text = <<~TEXT
      expected `#{match[:subject]}` to have changed, but the value did not match expectation:
        expected: #{match[:expected]}
          actual: #{match[:actual]}
    TEXT

    RSpec::Core::Formatters::ConsoleCodes.wrap(text, :failure)
  else
    original_message
  end
}

RSpec::Matchers::BuiltIn::Change.prepend(
  Module.new do
    def failure_message
      CHANGE_FAILURE_MESSAGE_IMPROVER.call(super)
    end

    def failure_message_when_negated
      CHANGE_FAILURE_MESSAGE_IMPROVER.call(super)
    end
  end
)

[
  RSpec::Matchers::BuiltIn::Compound,
  RSpec::Matchers::BuiltIn::Compound::And
].each do |klass|
  klass.prepend(
    Module.new do
      def failure_message
        CHANGE_FAILURE_MESSAGE_IMPROVER.call(super)
      end

      def compound_failure_message
        CHANGE_FAILURE_MESSAGE_IMPROVER.call(super)
      end
    end
  )
end
Enter fullscreen mode Exit fullscreen mode

Here's to hoping I get this merged into RSpec at some point. 🍻

Top comments (0)