加速rspec控制器测试:使用before all失败?

12

我有一个简单的控制器测试,其中包含以下代码:

context "POST :create" do
  before (:each) do
    post :create, :user_id => @user.id,
         :account => { .. some data ... }
  end
  it { response.status.should == 201 }
  it { response.location.should be_present }
end

现在我想到了一种非常简单的加速测试的方法,即使用before(:all)而不是before(:each)。在这种情况下,post请求只会被执行一次。

于是我写了这个:

context "POST :create" do
  before (:all) do
    post :create, :user_id => @user.id,
         :account => { .. some data ... }
  end
  it { response.status.should == 201 }
  it { response.location.should be_present }
end

但是我遇到了以下错误:

 RuntimeError:
   @routes is nil: make sure you set it in your test's setup method.

这是设计上的吗?有方法可以规避它吗?


1
你找到解决方案了吗?我也遇到了同样的问题。 - ktusznio
3个回答

12
我在rspec邮件列表上问了这个问题,得到了@dchelimsky本人的回复:
RSPEC-Rails包装了Rails的测试框架,而这个框架中没有before(:all)概念,因此每个示例运行之前都会重置所有数据。即使我们想要在rspec-rails中支持这个功能(我不想),也需要先对Rails进行更改。
因此,在before(:all)块中调用控制器是不可能的,它只能用于设置数据库或实例变量。

3

我不确定这是否是一个好主意,但是在before(:each)块中使用||=设置类变量似乎可以起作用:

describe PagesController do
  describe "GET 'index'" do
    before(:each) do
      @@response ||= begin
        get :index
        response
      end
    end
    it { @@response.should redirect_to(root_path) }
    it { @@response.status.should == 301 }
    it { @@response.location.should be_present }
  end
end

更新

另一种可能更简洁的方法是在单个规范中添加多个断言。添加:aggregate_failures标签(或将断言包装在aggregate_failures {...}块中)将单独打印每个失败,这提供了单独测试的细粒度:

describe PagesController do
  describe "GET 'index'" do
    it "redirects to homepage", :aggregate_failures do
       get :index
       expect(response).to redirect_to(root_path)
       expect(response.status).to eq(301)
       expect(response.location).to be_present
    end
  end
end

你试过这个吗?当我测试时,POST请求甚至都没有起作用,因为你还没有进入任何控制器上下文。 - nathanvda
哎呀,那应该是“还没有”尝试过这个。使用不同的技术更新了答案。 - Zubin
现在你又做了一个 before(:each),这正是我想避免的,而且有更漂亮/可读性更好的写法。如果你使用 before :each,你只需要写 get :index 并使用 response 即可。 - nathanvda
@nathanvda 注意类变量和 ||= 的使用 - 这应该只在第一次执行代码。 - Zubin
1
为什么这个答案没有被接受?有人能否评论一下这是否是一个好的做法。它似乎有效。 - Faraaz Khan

3

如果你想采用不太规范的全局变量方式,以此从速度上获得优势,你可以使用这个方法,但需要注意。这种混乱的逻辑虽然能解决问题,但会破坏用清晰易读的测试驱动开发的初衷。因此强烈建议将其重构为带有yield的helper。

describe PagesController do
  describe "GET 'index'" do
    before(:each) do
      GLOBAL ||= {}
      @response = GLOBAL[Time.now.to_f] || begin
        get :index
        response
      end
    end
    it { @response.should redirect_to(root_path) }
    it { @response.status.should == 301 }
    it { @response.location.should be_present }
  end
end

您可以将重构放入spec/support中您选择的文件中,具体如下:
RSPEC_GLOBAL = {}

def remember_through_each_test_of_current_scope(variable_name)
  self.instance_variable_set("@#{variable_name}", RSPEC_GLOBAL[variable_name] || begin
    yield
  end)
  RSPEC_GLOBAL[variable_name] ||= self.instance_variable_get("@#{variable_name}")
end

因此,测试文件中的代码变成了:
describe PagesController do
  describe "GET 'index'" do
    before(:each) do
      remember_through_each_test_of_current_scope('memoized_response') do
        get :index
        response
      end
    end
    it { @memoized_response.should redirect_to(root_path) }
    it { @memoized_response.status.should == 301 }
    it { @memoized_response.location.should be_present }
  end
end

希望这有所帮助,再次提醒您小心使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接