The matchers this suite already uses
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
- The matchers this suite already uses (this lesson).
- Reading a failure message.
- The matchers worth knowing.
- Custom matchers.
- Shared examples and shared contexts.
- Matchers, helpers and factories.
- 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.togives 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_withinrather thaneqfor 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.
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.