Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RSpec custom diffable matcher

Tags:

ruby

rspec

I have a custom matcher in RSpec, that ignores whitespaces / newlines, and just matches content:

RSpec::Matchers.define :be_matching_content do |expected|
  match do |actual|
    actual.gsub(/\s/,'').should == expected.gsub(/\s/,'')
  end

  diffable
end

I can use it like this:

    body = "   some data   \n more data"
    body.should be_matching_content("some data\nmore wrong data")

However, when a test fails (like the one above), the diff output looks not good:

   -some data
   -more wrong data
   +   some data   
   + more data

Is it possible to configure the diffable output? The first line some data is right, but the second more wrong data is wrong. It would be very useful, to only get the second line as the root cause of the failure.

like image 311
23tux Avatar asked Sep 09 '15 10:09

23tux


3 Answers

You can override the expected and actual methods that will then be used when generating the diff. In this example, we store the expected and actual values as instance variables and define methods that return the instance variables:

RSpec::Matchers.define :be_matching_content do |expected_raw|
  match do |actual_raw|
    @actual = actual_raw.gsub(/\s/,'')
    @expected = expected_raw.gsub(/\s/,'') 
    expect(expected).to eq(@actual)
  end

  diffable
  attr_reader :actual, :expected
end

Another example is to match for specific attributes in two different types of objects. (The expected object in this case is a Client model.)

RSpec::Matchers.define :have_attributes_of_v1_client do |expected_client|
  match do |actual_object|
    @expected = client_attributes(expected_client)
    @actual = actual_object.attributes
    expect(actual_object).to have_attributes(@expected)
  end

  diffable
  attr_reader :actual, :expected

  def failure_message
    "expected attributes of a V1 Client view row, but they do not match"
  end

  def client_attributes(client)
    {
      "id" => client.id,
      "client_type" => client.client_type.name,
      "username" => client.username,
      "active" => client.active?,
    }
  end
end

Example failure looks like this:

Failure/Error: is_expected.to have_attributes_of_v1_client(client_active_partner)
  expected attributes of a V1 Client view row, but they do not match
  Diff:
  @@ -1,6 +1,6 @@
   "active" => true,
  -"client_type" => #<ClientType id: 2, name: "ContentPartner">,
  +"client_type" => "ContentPartner",
   "id" => 11,
like image 154
Robin Daugherty Avatar answered Oct 17 '22 02:10

Robin Daugherty


I believe you should disable default diffable behaviour in RSpec and substitute your own implementation:

RSpec::Matchers.define :be_matching_content do |expected|
  match do |actual|
    @stripped_actual = actual.gsub(/\s/,'')
    @stripped_expected = expected.gsub(/\s/,'')
    expect(@stripped_actual).to eq @stripped_expected
  end

  failure_message do |actual|
    message = "expected that #{@stripped_actual} would match #{@stripped_expected}"
    message += "\nDiff:" + differ.diff_as_string(@stripped_actual, @stripped_expected)
    message
  end

  def differ
    RSpec::Support::Differ.new(
        :object_preparer => lambda { |object| RSpec::Matchers::Composable.surface_descriptions_in(object) },
        :color => RSpec::Matchers.configuration.color?
    )
  end
end

RSpec.describe 'something'do
  it 'should diff correctly' do
    body = "   some data   \n more data"
    expect(body).to be_matching_content("some data\nmore wrong data")
  end
end

produces the following:

Failures:

  1) something should diff correctly
     Failure/Error: expect(body).to be_matching_content("some data\nmore wrong data")
       expected that somedatamoredata would match somedatamorewrongdata
       Diff:
       @@ -1,2 +1,2 @@
       -somedatamorewrongdata
       +somedatamoredata

You can use custom differ if you want, even reimplement this whole matcher to a system call to diff command, something like this:

♥ diff -uw --label expected --label actual <(echo "   some data    \n more data") <(echo "some data\nmore wrong data")
--- expected
+++ actual
@@ -1,2 +1,2 @@
    some data    
- more data
+more wrong data

Cheers!

like image 25
Alexey Shein Avatar answered Oct 17 '22 04:10

Alexey Shein


There is a gem called diffy which can be used.

But it goes through a string line by line and compares them so instead of removing all whitespace you could replace any amount of whitespace with a newline and diff those entries.

This is an example of something you could do to improve your diffs a little bit. I am not 100% certain about where to insert this into your code.

def compare(str1, str2)
  str1 = break_string(str1)
  str2 = break_string(str2)
  return true if str1 == str2
  puts Diffy::Diff.new(str1, str2).to_s
  return false
end

def break_string(str)
  str.gsub(/\s+/,"\n")
end

The diffy gem can be set to produce color output suitable for the terminal.

Using this code would work like this

str1 = 'extra some                 content'
str2 = 'extra more content'
puts compare(str1, str2)

this would print

 extra
 -some   # red in terminal
 +more   # green in terminal
 content
 \ No newline at end of file
like image 1
Albin Avatar answered Oct 17 '22 02:10

Albin