I'm new to RSpec. I'm using the following (correct) piece of spec code for making sure that the methods find
and same_director
are called in the tested controller:
require 'rails_helper'
describe MoviesController do
describe 'Searching movies for same director: ' do
before :each do
@movie1 = instance_double('Movie', id:1, title:'movie1')
@movie2 = instance_double('Movie', id:2, title:'movie2')
@any_possible_id = 1
end
it 'should call the model method that finds the movie corresponding to the passed id.' do
allow(@movie1).to receive(:same_directors).and_return [@movie1,@movie2]
expect(Movie).to receive(:find).with(@any_possible_id.to_s).and_return @movie1
get :directors, :id=>@any_possible_id
end
it 'should call the model method that finds the movies corresponding to the same director.' do
expect(@movie1).to receive(:same_directors).and_return [@movie1,@movie2]
allow(Movie).to receive(:find).with(@any_possible_id.to_s).and_return @movie1
get :directors, :id=>@any_possible_id
end
This is working fine, but as you can see, the two it
-clauses contain a repeated get :directors, :id=>@any_possible_id
line. Given the order of the statement in the controller (see the controller code at the end of the question), the get
has to appear at the end of the it
-clause. The challenge is to DRY it out by moving it to the before
-clause.
Of course, the best solution is to just move it to an after
-clause (I realized that while I was writing the question, I did it and it works.) But what I want now is to understand why my use of Spies here is not working.
My (failing) attempt with Spies is:
describe MoviesController do
describe 'Searching movies for same director: ' do
before :each do
@movie1 = instance_double('Movie', id:1, title:'movie1')
@movie2 = instance_double('Movie', id:2, title:'movie2')
@any_possible_id = 1
@spyMovie = class_spy(Movie)
@spymovie1 = instance_spy(Movie)
get :directors, :id=>@any_possible_id
end
it 'should call the model method that finds the movie corresponding to the passed id.' do
allow(@spymovie1).to receive(:same_directors)#.and_return [@movie1,@movie2]
expect(@spyMovie).to have_received(:find).with(@any_possible_id.to_s)#.and_return @movie1
end
it 'should call the model method that finds the movies corresponding to the same director.' do
expect(@spymovie1).to have_received(:same_directors)#.and_return [@movie1,@movie2]
allow(@spyMovie).to receive(:find).with(@any_possible_id.to_s)#.and_return @movie1
end
Now, there might be several problems with this second piece of code, but the error I am getting when running rspec
is:
1) MoviesController Searching movies for same director: should call the model method that finds the movie corresponding to the passed id.
Failure/Error: expect(@spyMovie).to have_received(:find).with(@any_possible_id.to_s)#.and_return @movie1
(ClassDouble(Movie) (anonymous)).find("1")
expected: 1 time with arguments: ("1")
received: 0 times
# ./spec/controllers/movies_controller_spec.rb:32:in `block (3 levels) in <top (required)>'
2) MoviesController Searching movies for same director: should call the model method that finds the movies corresponding to the same director.
Failure/Error: expect(@spymovie1).to have_received(:same_directors)#.and_return [@movie1,@movie2]
(InstanceDouble(Movie) (anonymous)).same_directors(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
# ./spec/controllers/movies_controller_spec.rb:35:in `block (3 levels) in <top (required)>'
So, you can see that the calls to :find
and to :same_directors
are not being detected when I use the spies.
I couldn't get the answer in rspec.info nor in relishapp. I would very much appreciate it if you can suggest something. Thanks!
In case you need it, this is the controller:
def directors
@movie = Movie.find(params[:id])
@movies = @movie.same_directors
if (@movies-[@movie]).empty?
flash[:notice] = "'#{@movie.title}' has no director info"
redirect_to movies_path
end
end
This is a relevant question: besides testing its behavior, one might want to make sure that certain controller methods are called. For example, if those methods are from legacy code, you want to make sure via a self-checking test that someone else is not going to come and substitute her own methods in the controller. (Thank you anyway max, I appreciate your time).
This is the code that answers the question:
before :each do
@movie1 = spy('Movie')
allow(Movie).to receive(:find).and_return @movie1
@any_possible_id = 1
get :directors, :id=>@any_possible_id
end
it 'should call the model method that finds the movie corresponding to the passed id.' do
expect(Movie).to have_received(:find).with(@any_possible_id.to_s)
end
it 'should call the model method that finds the movies corresponding to the same director.' do
expect(@movie1).to have_received(:same_directors)
end
Explanation
Line 2 - @movie1
is defined as a spy out of the class Movie
. Being a spy, we can use it later in expect(@movie1).to have_received(:anything)
. If @movie1
is defined as a double
then it becomes necessary to use an :allow
clause to allow it to receive the method anything
(or "message", as the parlance of the docs).
Line 3 - This line is the key for explaining the error thrown by RSpec. The .and_return @movie1
is fundamental for telling RSpec that @movie1
is playing the role of @movie
in the controller (see the controller code at the end of the question). RSpec establishes this match from seeing that Movie.find
in the controller returns @movie
, as specified in this allow
statement. The reason for the error in the question is not that Movie
or @movie
were not receiving the specified methods in the controller; the reason is that RSpec was not matching Movie
and @movie1
in the spec with the real Movie
and @movie
in the controller.
The rest is self-explanatory. By the way, I came with other ways of writing this test, but this one with the use of the spy is the most compact one.