使用间谍来检测方法调用

时间:2015-12-02 08:55:05

标签: ruby-on-rails ruby-on-rails-4 rspec rspec-rails

我是RSpec的新手。我正在使用以下(正确的)规范代码来确保在测试控制器中调用方法findsame_director

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

这很好用,但正如您所看到的,这两个it - 子句包含重复的get :directors, :id=>@any_possible_id行。给定控制器中语句的顺序(请参阅问题末尾的控制器代码),get必须出现在it - 子句的末尾。挑战是通过将其移至before - 条款来彻底解决它。 当然,最好的解决方案是将其移至after - 条款(我意识到,当我写这个问题时,我做了它并且它有效。)但我现在想要的是理解为什么我的使用间谍在这里不起作用。

我与间谍的失败尝试是:

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

现在,第二段代码可能存在一些问题,但运行rspec时出现的错误是:

  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)>'

因此,您可以看到我使用间谍时未检测到对:find:same_directors的调用。

我无法在rspec.inforelishapp中得到答案。如果你能提出一些建议,我将非常感激。谢谢!

如果您需要它,这是控制器:

  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

2 个答案:

答案 0 :(得分:1)

您的示例失败,因为您在设置期望之前在get :directors, :id=>@any_possible_id块中调用before

与一些模拟框架不同,RSpec不会捕获在设置期望之前所做的调用。

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

上面的两个例子都应该通过。

然而,RSpec确实以间谍为特色:

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

此处的差异是allow(Movie).to receive(:find).and_return([@movie1,@movie2])用间谍替换.find类上的Movie方法。

测试行为 - 不实施。

但是我认为你在这里对控制器内部进行了大量研究,而不是测试你的控制器如何完成它的工作,你应该测试它的行为。

这里相关的是你的控制器为给定的一组参数返回正确的结果集:

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

请注意,在控制器规范中,您通常会按describe "HTTPVERB #method"对规范进行分组。它可以快速查找正在测试的内容。

在测试控制器时,有一些有效的模拟和间谍用途,但我想说是在测试应用程序的边界时 - 比如当你的应用程序触及第三方API时。另一个有效的案例是伪造身份验证/授权。

答案 1 :(得分:0)

这是一个相关问题:除了测试其行为之外,还可能需要确保调用某些控制器方法。例如,如果这些方法来自遗留代码,那么您需要通过自检来确保其他人不会来自控制器中替换自己的方法。 (无论如何,谢谢你,我很感激你的时间。)

这是回答问题的代码:

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

<强>解释

第2行 - @movie1被定义为类Movie中的spy。作为间谍,我们可以稍后在expect(@movie1).to have_received(:anything)中使用它。如果将@movie1定义为double,则becomes necessary to use :allow条款允许其接收方法anything(或&#34;消息&#34 ;,作为文档的说法)。

第3行 - 此行是解释RSpec引发的错误的关键。 .and_return @movie1是告诉RSpec @movie1在控制器中扮演@movie角色的基础(请参阅问题末尾的控制器代码)。 RSpec通过查看控制器中的Movie.find返回@movie来建立此匹配,如此allow语句中所指定的那样。问题中出错的原因不是Movie@movie没有在控制器中接收指定的方法;原因是RSpec未将规范中的Movie@movie1与控制器中的真实Movie@movie匹配。

其余的是不言自明的。顺便说一句,我带来了其他编写这个测试的方法,但这个使用间谍的方法是最紧凑的。