Use of Spies for detecting method calls

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

Answers


Your examples fail because you are calling get :directors, :id=>@any_possible_id in the before block before the expectation is set.

Unlike some mocking frameworks RSpec does not catch calls which where made before the expectation is set.

describe "RSpec doubles" do

  let(:foo){ instance_double('Foo') }

  it "does not catch calls made before the expectation is set" do
     foo.bar
     expect(foo).to_not have_received(:bar)
  end

  it "catches calls made after the expectation is set" do
     expect(foo).to have_received(:bar)
     foo.bar
  end
end

Both of the examples above should pass.

However RSpec does feature spies:

before(:each) do
  # ...
  allow(Movie).to receive(:find).and_return([@movie1,@movie2])
  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("1")
end

The difference here is allow(Movie).to receive(:find).and_return([@movie1,@movie2]) replaces the .find method on the Movie class with a spy.

Test behavior - not implementation.

However I think you are poking a bit to much into the internals of your controller here, rather than testing how your controller does its job you should be testing its behaviour.

The relevant thing here is that your controller returns the correct result set for a given set of params:

describe MoviesController do
  describe 'GET #directors' do
    let(:director) { FactoryGirl.create(:director) }
    let!(:movies) do # not lazy loaded
      [Movie.create(director: director), Movie.create(director: director), Movie.create] 
    end

    before { get :directors, id: director.to_param }

    it "matches movies by the given director" do
      expect(assigns[:movies]).to include(movies.first)
      expect(assigns[:movies].size).to eq 2
    end

    it "does not match movies which are not by the given director" do
      expect(assigns[:movies]).to_not include(movies.last)
    end
  end
end

Note that in a controller spec you usually group your specs by describe "HTTPVERB #method". It makes it quick to lookup what is under test.

There are valid uses for mocking and spies in when testing controllers but I would say it is when you are testing the boundaries of your application - like for example when your application touches a third party API. Another valid case is to fake authentication/authorization.


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.


Need Your Help

jPlayer and IE10 - SCRIPT438: Object doesn't support property or method 'jPlayer'

javascript jquery internet-explorer internet-explorer-10 jplayer

I'm using the jPlayer in a very simplistic Setup, but I can't get it to work in a stable way, which is confusing, as I really only use the Basic parts of the jPlayer. Basically, it only consists of a

2 dimensional string array declaration, Exception in thread "main" java.lang.NullPointerException java

java arrays string nullpointerexception declaration

I'm new to java programming and I can't find a solution to my problem. I think it's a pretty easy problem but I can't figure a what I'm doing wrong so I hope one of you could help me. The problem i...