Matchers
Built-in matchers
Get an overview of all the matchers that are built into RSpec Show archive.org snapshot .
Play with some of these matchers in your MovieDB tests.
The benefits of using better matchers
Which of the following two lines is better? Why?
expect(array).to include(5)
expect(array.include?(5)).to eq(true)
Custom matchers
Write a
custom matcher
Show archive.org snapshot
called have_same_attributes_as
. It should compare the attributes of two ActiveRecord instances:
movie1 = create(:movie, title: 'Foo', year: 2007, description: 'Lorem ipsum')
movie2 = create(:movie, title: 'Foo', year: 2007, description: 'Lorem ipsum')
movie3 = create(:movie, title: 'Bar', year: 2008, description: 'Lorem ipsum')
expect(movie1).to have_same_attributes_as(movie2)
# passes
expect(movie1).to have_same_attributes_as(movie3)
# Fails with 'Expected movie #112 to have same attributes as movie #113, but the attributes #title and #year differed'
Now write a method Movie#copy
in your MovieDB. It saves a duplicate of the movie, copying all the attributes. Test this method using your new have_same_attributes_as
matcher.
Tip
ActiveSupport gives your arrays a
#to_sentence
method that may help you build the error message:['foo', 'bar', 'baz'].to_sentence => "foo, bar and baz"
Also see Where to put custom matchers and other support code.
DRY specs
Goals
You should know the following tools:
- Shared example groups
- Nested example groups
before(:each)
after(:each)
let
subject
-
RSpec.configure
,config.before
,config.after
Resources
-
Everyday Rails Testing with RSpec (in our
library
Show archive.org snapshot
), chapter 8 (Keeping Specs DRY)
- Note: Please refer to the Rails 5 version of this book for now, Chapter 8 is currently work in progress for the Rails 7 version
- Documentation for rspec-core Show archive.org snapshot
- Shared examples and contexts in RSpec Show archive.org snapshot
- Testing shared traits or modules without repeating yourself
- ActiveRecord callbacks Show archive.org snapshot
- Composing a custom matcher from existing matchers
Exercise: Shared examples
Make the following change to your MovieDB:
-
A new tab "Changes" shows a log of recent changes made to movies and actors.
-
E.g. when a movie was created there is a log entry saying
Movie "Sunshine" was created
-
E.g. when a movie was updated there is a log entry saying
Movie "Sunshine" was updated
-
E.g. when an actor was destroyed there is a log entry saying
Actor "Shohreh Aghdashloo" was destroyed
-
Use callbacks Show archive.org snapshot to automatically write changelog entries to the database as a model record gets created, updated or destroyed.
-
Extract the logic into a
module
so it can be re-used by bothActor
andMovie
models.Tip
The method producing the logged identifier may differ between models, e.g.
Actor#full_name
vs.Movie#title
. You can either use the same method name here (like#to_s
or#name_for_log
), or build a parametrized module using Modularity Show archive.org snapshot . -
In your RSpec tests, use a shared example group to share tests between
actor_spec.rb
andmovie_spec.rb
.
DRY vs. coupling
Sharing test setup can lead to DRY, but tightly coupled test code. Read Prefer self-contained examples Show archive.org snapshot for an argument for isolating tests instead, even if it means some duplication. In general it is more important for a test to be simple than to be DRY.
A sweet spot is often to prefer isolated tests where possible, but share test setup when it becomes excessively complicated or expensive. If we share setup, it is best to do within a shared context
only. This way so we limit the setup's scope.
describe Klass do
describe '#foo' do
it 'does basic thing 1' do
# isolated test without shared setup here
end
it 'does basic thing 2' do
# isolated test without shared setup here
end
context 'on the night of DST change in Australia' do
before :each do
# complicated, shared setup here
end
it 'handles special case 3' do
# test using the shared setup
end
it 'handles special case 4' do
# test using the shared setup
end
end
end
end
Mocking
Through Mocking you can take control of the program under test. This lets you test unmocked code easier.
Resources
- Mock objects (or “doubles”) Show archive.org snapshot
- Setting constraints Show archive.org snapshot and Configuring responses Show archive.org snapshot
- Any instance Show archive.org snapshot
- Message chains Show archive.org snapshot
- Verifying doubles (card)
- Expect one of multiple matchers to match
- Expecting non-primitive objects as method invocation arguments
- Mocking the current time in tests
Exercise
Earlier in this card you implemented a change log for MovieDB. Change your RSpec tests so they no longer write log entries to the database. Instead use mocks to test that log entries would have been written with the correct attributes.
Discussion
Talk to your mentor about the pros and cons of mocking:
- Can you imagine a reason why the mocking test could pass, but the code is broken?
- Can you imagine a reason why the mocking test could fail, but the code is correct?
Spec types
Until now we have only written "model specs". These are classic "unit tests" where you instantiate an object, call a method and observe its return value or side effects.
Your MovieDB already uses the gem rspec-rails Show archive.org snapshot which has many different types of specs. These help you take a close look at Rails components that are not easily instantiated, such as routes, views or controller actions.
Go through the rspec-rails Show archive.org snapshot docs and get an overview what types of specs are supported.
Request spec
When your MoviesController#show
cannot find a movie, it currently crashes with ActiveRecord::RecordNotFound
. Change that so instead of crashing, it sets a flash "Movie not found" and redirects to the movie index.
Write a
request spec
Show archive.org snapshot
that takes a close look at MoviesController#show
:
- If the given ID was found, the view
movies/show
is rendered - If the given ID was not found, a redirect to
/movies
is returned. The HTTP status 307 is used for the redirect. The movies index shows a flash message.
Tip
There is a
render_template()
matcher that helps with test above. To get this matcher, add a gemrails-controller-testing
.
Tip
If you place your spec file in
spec/requests
you don't need thetype: :request
option Show archive.org snapshot .
Helper spec
In the validations card we added a helper to display an error message.
Test that helper with a helper spec Show archive.org snapshot .