在RSpec 3请求中设置标头

时间:2014-09-12 19:46:36

标签: ruby-on-rails rspec ruby-on-rails-4.1 rspec3

我试图为需要身份验证的某些RSpec请求设置标头。标头为ACCESS_TOKEN。无论我如何设置标头,它都永远不会被设置。我知道应用程序有效,因为我可以手动测试它,我只是不能让rspec测试工作。查看完整源代码&此处测试此问题:https://github.com/lightswitch05/rspec-set-header-example

由于我的大多数请求规范都使用了身份验证,因此我创建了支持帮助程序模块来检索访问令牌并将其设置在标头中。以下是我尝试设置标题的摘要,查看我在full source

中尝试过的所有内容
# my_app/spec/support/session_helper.rb
module SessionHelper
  def retrieve_access_token
    post api_v1_session_path({email: 'test@example.com', password: 'poor_password'})

    expect(response.response_code).to eq 201
    expect(response.body).to match(/"access_token":".{20}"/)
    parsed = JSON(response.body)
    token = parsed['access_token']['access_token']

    @request.headers['HTTP_ACCESS_TOKEN'] = token
  end
end

使用此帮助程序的示例请求规范应该可以使用,但总是会失败,因为标头永远不会被设置:

# my_app/spec/requests/posts_spec.rb
# ...
context "create" do
  it "creates a post" do
    retrieve_access_token
    post = FactoryGirl.build(:post)

    post api_v1_posts_path(
      post: {
        title: post.title,
        content: post.content
      }
    )

    expect(response.body).to include('"id":')
    expect(response.body).to include('"title":"' + post.title + '"')
    expect(response.body).to include('"content":"' + post.content + '"')
    expect(response.response_code).to eq 201
  end
end

我知道我可以在单个getpost请求中手动设置标头 - 但这不是API范围授权的可维护解决方案。想象一下,如果标题名称略有改变,则必须更改每个测试。

5 个答案:

答案 0 :(得分:39)

注意:此答案基于您似乎正在调用api_v1_session_path,并postSessionsController请求您尝试在您的请求规范中运行的每个规范。

有两种方法可以解决我在这里遇到的问题。

解决方案#1 - 您可以在SessionHelper或其他一些名为support / requests_helper.rb的帮助程序文件中创建另一个帮助程序方法(但是您更喜欢)。我将在 support / requests_helper.rb 中创建另一个帮助器:

module RequestsHelper
  def get_with_token(path, params={}, headers={})
    headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
    get path, params, headers
  end

  def post_with_token(path, params={}, headers={})
    headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
    post path, params, headers
  end

  # similarly for xhr..
end

然后在rails_helper.rb中:

  # Include the sessions helper
  config.include SessionHelper, type: :request
  # Include the requests helper
  config.include RequestsHelper, type: :request

更改session_helper.rb:

# my_app/spec/support/session_helper.rb
module SessionHelper
  def retrieve_access_token
    post api_v1_session_path({email: 'test@example.com', password: 'poor_password'})

    expect(response.response_code).to eq 201
    expect(response.body).to match(/"access_token":".{20}"/)
    parsed = JSON(response.body)
    parsed['access_token']['access_token'] # return token here!!
  end
end

现在,您可以更改所有请求规范:

describe Api::V1::PostsController do

  context "index" do
    it "retrieves the posts" do
      get_with_token api_v1_posts_path

      expect(response.body).to include('"posts":[]')
      expect(response.response_code).to eq 200
    end

    it "requires a valid session key" do
      get api_v1_posts_path

      expect(response.body).to include('"error":"unauthenticated"')
      expect(response.response_code).to eq 401
    end
  end
end

解决方案#2 - 将规格/工厂/ access_token_factory.rb 更改为:

FactoryGirl.define do
  factory :access_token do
    active true
  end

  # can be used when you want to test against expired access tokens:
  factory :inactive_access_token do
    active false
  end
end

现在,更改您的所有请求规范以使用access_token

describe Api::V1::PostsController do

  context "index" do
    let(:access_token){ FactoryGirl.create(:access_token) }

    it "retrieves the posts" do
      # You will have to send HEADERS while making request like this:
      get api_v1_posts_path, nil, { 'HTTP_ACCESS_TOKEN' => access_token.access_token }

      expect(response.body).to include('"posts":[]')
      expect(response.response_code).to eq 200
    end

    it "requires a valid session key" do
      get api_v1_posts_path

      expect(response.body).to include('"error":"unauthenticated"')
      expect(response.response_code).to eq 401
    end
  end
end

我会选择“解决方案#1 ”,因为它会消除每次您想要提出此类请求时记住在标题中发送HTTP_ACCESS_TOKEN的负担。

答案 1 :(得分:14)

常见的误解是平等对待控制器和请求测试。

从阅读controller specsrequest specs开始,这将是一件好事。如您所见,控制器规范模拟http请求,而请求规范执行完整堆栈请求。

您可以找到一些很好的文章,说明为什么要编写控制器规范以及在那里测试的内容here。虽然写它们很好,但在我看来它们不应该触及数据库。

因此,虽然Voxdei answer部分有效(在将请求规范更改为控制器规范后,您的设置标头的方式仍然有效),但我认为这点不合适。

在请求规范中,您不能只使用请求/控制器方法,您必须将哈希的头文件作为请求方法的第三个参数传递,即即使如此。

post '/something', {}, {'MY-HEADER' => 'value'}

你可以做的是发布身份验证,如:

before do
  allow(AccessToken).to receive("authenticate").and_return(true)
end

然后,您可以在一个规范中测试您的身份验证,以确保它的工作原理,并在其他规范中使用之前使用此类身份验证。这也可能是更好的方法,因为每次运行需要身份验证的规范时执行额外的请求都是非常巨大的开销。

我还发现非常有趣的pull request in grape gem尝试添加默认标头行为,因此如果您真的想在请求规范中使用默认标头,也可以尝试使用此类方法。

答案 2 :(得分:4)

可能是因为现在Rspec如何处理spec文件。它no longer automatically infers spec type from a file location

尝试将此行为设置回您以前知道的内容

RSpec.configure do |config|
  config.infer_spec_type_from_file_location!
end

或为项目中的每个控制器规范文件在本地设置

describe MyController, type: :controller do
  # your specs accessing @request
end

答案 3 :(得分:2)

苏里亚的答案是最好的。但你可以再干一点:

def request_with_user_session(method, path, params={}, headers={})
    headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
    send(method, path, params, headers)
end

这里只有一个方法,并通过给定参数method调用请求方法。

答案 4 :(得分:0)

我将验证请求的函数存根,以返回true或函数返回的任何值。

ApplicationController.any_instance.stub(:authenticate_request) { true }