RSpec Matchers should be simple

(Everyday Code – instead of keeping our knowledge in a README.md let’s share it with the internet)

Today on a review I saw an RSpec Matcher that was more complex than it should be. We’ve discussed it internally. I think this article could be useful to our team and to the community as a whole.

An RSpec Matcher should be simple. It should return just ‘true’ or ‘false’

IRL example of a complex RSpec Matcher

This is the real live example of an RSpec Matcher from todays review

  RSpec::Matchers.define :have_content_picture_type_set_to do |picture_ref, content_picture_type, content_object|
    match do |page|
      content_object_undersore = content_object.model_name.to_s.underscore
      content_object_undersore = "tutorial" if content_object_undersore == "episode"
      content_object_undersore = "courses/#{content_object.course.to_param}/section" if content_object_undersore == "course_section"

      picture_ref_row = page.find(:xpath, "//tr[.//img[@src='#{picture_ref.content_picture.image_url}']]")
      js_agnostic_click_table_action picture_ref.to_param, "/author/#{content_object_undersore}s/#{content_object.to_param}/pictures/#{picture_ref.to_param}/edit", "Edit Picture"

      page.select content_picture_type, from: "content_picture_ref[content_picture_type]"

      click_on_and_expect "Update Picture", "Picture successfully updated"
      expect(page).to have_picture_ref_with_content_picture_type picture_ref, content_picture_type
    end
    failure_message do |page|
      "expected page to have content_picture with the specified type, but was #{page.body}"
    end
  end

There is a lot to unpack here.

  1. We do some calculations of paths with the whole content_object_underscore logic
  2. Then we find something on the page with the page.find method
  3. Then we call ‘js_agnostic_click_table_action‘ which is a complex method
  4. Then we select something from an HTML select
  5. Then we click a button with the click_on_and_expect
  6. At the end we expect something

We should have only step 6. That’s the purpose of an RSpec Matcher. Return true or false if the page contains something or not.

Why has this happened?

Because RSpec.define :matcher is a function like any other ruby function. And as a ruby function you can do whatever you want with it. You can call anything you want in this function.

Why should an RSpec Matcher be simple?

That’s the convention for RSpec Matchers. Imagine the following spec:

    visit_pictures material
    expect(page).to have_picture_ref_with_content_picture_type material.content_picture_refs.first, "Thumbnail"

With this spec you would expect visit a page and check if there is something on the page. But now have_pictuer_ref_with_content_picture_type makes calls, changes the db, modifies the current page and there is no way for you to understand it from reading the spec. Let alone reuse the Matcher in another situation.

What feature is this about?

The feature above is about showing a picture on the animated 3D models and building instructions that we show at BuildIn3D. Authors could choose which picture is the Thumbnail – example is this FabBrix Hen :).

FabBRIX Farm Animals, Hen in 3D building instructions