The matchers this suite already uses

Free preview 4 min to read

This repository has 204 spec files: models, queries, requests, services, policies, helpers, jobs and mailers. Reading a few of them shows what the suite is already doing well, and one thing it is not doing at all.

What a matcher is

expect(user.online_time_in_hours).to eq(2.0)

eq(2.0) is a matcher: an object that knows how to answer does this value match? and, more importantly, how to explain itself when the answer is no.

That second responsibility is the one that decides whether a matcher is worth using. Any assertion can be written as expect(condition).to be true, and every one written that way produces the same useless failure: expected true, got false. A matcher exists to say something better.

From this repository's model spec

RSpec.describe Identity::User, type: :model do
  describe 'validations' do
    subject { build(:user) }

    it { is_expected.to validate_presence_of(:email) }
    it { is_expected.to validate_uniqueness_of(:email).case_insensitive.with_message('is not available') }
  end

  describe '#update_activity!' do
    it 'updates last_activity_at to the current time' do
      user = create(:user)
      now  = Time.current
      user.update_activity!
      expect(user.reload.last_activity_at).to be_within(1.second).of(now)
    end
  end
end

Four things in that file are worth naming, because each is a deliberate choice.

is_expected.to is expect(subject).to, which is what makes a one-line example readable. With a subject defined above, it { is_expected.to validate_presence_of(:email) } needs no description: RSpec generates one from the matcher.

validate_uniqueness_of(...).with_message('is not available') asserts the exact message, which the OWASP course established was a deliberate anti-enumeration choice. A test that checked only that it validates would pass if someone reverted the message to Rails' default. And the security property would be gone with no failing spec.

be_within(1.second).of(now) is the right matcher for a timestamp. eq(now) would compare to sub-microsecond precision and fail intermittently, which is the flaky-test category from the CI course arriving through a matcher choice.

subject { build(:user) } uses build rather than create, so the validation matchers do not touch the database until one of them needs to.

The three families

Family Source Example
Built-in rspec-expectations eq, include, change, raise_error
Rails-aware rspec-rails have_http_status, render_template, have_enqueued_job
Model-aware shoulda-matchers validate_presence_of, belong_to, have_many

This suite configures the third:

Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

Shoulda-matchers are worth understanding as a category: each one exercises the real validation rather than reading the class's configuration. validate_uniqueness_of saves a record and tries to save a conflicting one, which is why it is slower than it looks and why it catches a validation that is declared and does not work.

flowchart TB
  E["expect(value).to matcher"] --> M{"does it match?"}
  M -->|"yes"| P["pass, silently"]
  M -->|"no"| F["failure_message: the whole point"]
  F --> D["A good message names the difference"]
  F --> B["A bad message says: expected true, got false"]

What this suite does not have

grep -rl "RSpec::Matchers.define" spec/     # nothing
grep -rl "shared_examples" spec/            # nothing

No custom matchers, and no shared examples. With 204 spec files, 35 of them for query objects and 40 for services, that is a suite where the same shape is being asserted repeatedly in longhand.

It also has one thing that shows the instinct is there:

# spec/support/auth_helpers.rb
module AuthHelpers
  def sign_in(user)
    post auth_sign_in_path, params: { email: user.email, password: user.password }
  end
end

A shared helper, included for type: :request. That is the same impulse as a shared example, expressed with the tool the team already knew. And lesson 6 is about when each of the three tools is the right one.

What this course covers

  1. The matchers this suite already uses (this lesson).
  2. Reading a failure message.
  3. The matchers worth knowing.
  4. Custom matchers.
  5. Shared examples and shared contexts.
  6. Matchers, helpers and factories.
  7. Putting it together: what to extract from this suite.

Key takeaways

  • A matcher answers whether a value matches and, more importantly, explains itself when it does not.
  • is_expected.to gives one-line examples with generated descriptions; subject { build(...) } avoids touching the database.
  • Asserting the exact validation message is what protects a deliberate choice like the anti-enumeration wording.
  • be_within rather than eq for timestamps prevents a flaky spec caused by a matcher choice.
  • This suite has 204 spec files, no custom matchers and no shared examples, which is what the rest of the course is about.
Lesson 1 of 7 · free

Keep going with lesson 2

Reading a failure message

Compares four failure messages for the same bug and explains why the matcher should receive the value itself.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...