如何使用rspec测试路由约束 [英] How to test route constraints with rspec

查看:168
本文介绍了如何使用rspec测试路由约束的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在处理一个主要作为API(除了一些次要视图,例如会话/注册,这将是标准)的应用程序。我喜欢在 Railscast#350:版本化API 中完成的方法,因此遵循它。我的路线如下:

  namespace:api,:defaults => {:format => 'json'} do 
scope:module => :v1,:constraints => ApiConstraints.new(:version => 1,:default => false)do
resources:posts,:only => [:create,:show,:destroy,:index]
end

scope:module => :v2,:constraints => ApiConstraints.new(:version => 2,:default => true)do
resources:posts,:only => [:create,:show,:destroy,:index]
end
end

在每个路由中,我的约束是一个新的ApiConstraints对象,它位于我的 ./ lib 文件夹中。类如下所示:

  class ApiConstraints 
def initialize $ b @version = options [:version]
@default = options [:default]
end

def matches?(req)
@default || req.headers ['Accept']。include?(application/vnd.MYAPP.v#{@version})
end
end
pre>

现在,当手动测试时,一切工作正常。在我的API中,我可能每个版本有5到10个控制器,并且不想测试API约束对每个单独的控制器是否有效,因为这没有意义。我正在寻找一个spec文件来测试我的API约束,但我不确定在哪里放这个spec。



我试过添加一个 spec / routing / api_spec.rb 文件测试的东西,但它不能正常工作,因为它抱怨一些事情没有提供,像这样:

  it应该将未版本化的请求路由到最新版本do 
expect(:get =>/ api / posts,:format = >json)。to route_to(:controller =>api / v1 / posts)
end

即使控制器匹配正确,上面的代码也会抛出一个错误。它失败,并显示以下错误:

 可识别的选项< {format=>json =>index,controller=>api / v1 / posts}> 
不匹配< {controller=>api / v1 / posts}>,
difference:< {format=>json >index}> ;.

请注意,控制器已正确确定,但由于我不想测试格式,动作在这个测试中,它出错了。我想要有3个API规范:




  • 应将未版本化的请求路由到最新版本

  • 如果未指定,则应默认为JSON格式

  • 请求时应返回指定的API版本



有没有人有这些类型的路线写作规范的经验?我不想为API中的每个控制器添加规范,因为它们不负责此功能。

解决方案

Rspec的 route_to 匹配器委托给 ActionDispatch :: Assertions :: RoutingAssertions#assert_recognizes



route_to 作为 expected_options 哈希传递(在一些预处理之后,它允许它也理解简写参数,例如<$ c $



您希望匹配 route_to matcher(即 {:get =>/ api / posts,:format =>json} expect 的形成参数。如果您查看,您可以看到我们通过



路径获取匹配的路径,query = * verb_to_path_map.values.first.split('?')



#first 是一个确定的符号,我们希望只有一个键值对。所以:format =>



ActionDispatch 断言期望你将一个完整的路径+动词匹配到一套完整的控制器,动作,路径参数。所以rspec匹配器只是传递了它委托的方法的限制。



这听起来像rspec的内置 route_to matcher不会做你想要的。所以下一个建议是假设 ActionDispatch 会做它应该做的,而只是写你的 ApiConstraints 不使用默认的 spec_helper 。 Corey Haines有一个好主意,关于如何使一个更快的规格助手,不会启动整个rails应用程序。它可能不是完美的你的情况,因为,但我只是想我会指出,因为你只是实例化基本的ruby对象在这里,并不真正需要任何铁路魔术。您也可以尝试要求 ActionDispatch :: Request &



看起来像



spec / lib / api_constraint.rb

  require'active_record_spec_helper '
require_relative'../../lib/api_constraint'

describe ApiConstraint do

描述#matches? do

let(:req){Object.new}

contextdefault versiondo

before:each do
req .stub(:headers).and_return {}
@opts = {:version => nil,:default => true}
end

it无论版本号如何都返回truedo
ApiConstraint.new(@opts).should匹配req
end

end

end

end

... aaand我会让你明确地确定如何设置上下文/写上你的其他测试的期望。


I'm working on an application that will be primarily served as an API (other than a few minor views, such as session/registration, which will be "standard"). I like the approach that was finalized in Railscast #350: Versioning an API, and so followed it. My routes look like:

namespace :api, :defaults => {:format => 'json'} do
  scope :module => :v1, :constraints => ApiConstraints.new(:version => 1, :default => false) do
    resources :posts, :only => [:create, :show, :destroy, :index]
  end

  scope :module => :v2, :constraints => ApiConstraints.new(:version => 2, :default => true) do
    resources :posts, :only => [:create, :show, :destroy, :index]
  end
end

In each route, my Constraint is a new ApiConstraints object, which is located in my ./lib folder. The class looks like this:

class ApiConstraints
  def initialize(options)
    @version = options[:version]
    @default = options[:default]
  end

  def matches?(req)
    @default || req.headers['Accept'].include?("application/vnd.MYAPP.v#{@version}")
  end
end

Now, when testing manually, everything works as expected. In my API, I may have between 5 and 10 controllers per version, and don't want to test that the API constraints works for each individual controller, as that makes no sense. I'm looking for one spec file that tests my API constraints, but I'm unsure of where to put that spec.

I've tried adding a spec/routing/api_spec.rb file to test things, but it's not working properly, as it complains that some things aren't provided, like so:

it "should route an unversioned request to the latest version" do
  expect(:get => "/api/posts", :format => "json").to route_to(:controller => "api/v1/posts")
end

The above throws an error even though the controller matches properly. It fails with the following error:

The recognized options <{"format"=>"json", "action"=>"index", "controller"=>"api/v1/posts"}>
did not match <{"controller"=>"api/v1/posts"}>,
difference: <{"format"=>"json", "action"=>"index"}>.

Notice that the controller was properly determined, but since I don't want to test for the format and action in this test, it errors out. I would like there to be 3 "API specs":

  • It should route an unversioned request to the latest version
  • It should default to the JSON format if none is specified
  • It should return a specified API version when requested

Does anyone have experience with writing specs for these kinds of routes? I don't want to add specs for every controller inside the API, as they're not responsible for this functionality.

解决方案

Rspec's route_to matcher delegates to ActionDispatch::Assertions::RoutingAssertions#assert_recognizes

The the argument to route_to is passed in as the expected_options hash (after some pre-processing that allows it to also understand shorthand-style arguments like items#index).

The the hash that you're expecting to match the route_to matcher (i.e., {:get => "/api/posts", :format => "json"}) is not actually a well-formed argument to expect. If you look at the source, you can see that we get the path to match against via

path, query = *verb_to_path_map.values.first.split('?')

The #first is a sure sign that we're expecting a hash with just one key-value pair. So the :format => "json" component is actually just being discarded, and isn't doing anything.

The ActionDispatch assertion expects you to be matching a complete path + verb to a complete set of controller, action, & path parameters. So the rspec matcher is just passing along the limitations of the method it delegates to.

It sounds like rspec's built-in route_to matcher won't do what you want it to. So the next suggestion would be to assume ActionDispatch will do what it is supposed to do, and instead just write specs for your ApiConstraints class.

To do that, I'd first recommend not using the default spec_helper. Corey Haines has a nice gist about how to make a faster spec helper that doesn't spin up the whole rails app. It may not be perfect for your case as-is, but I just thought I'd point it out since you're just instantiating basic ruby objects here and don't really need any rails magic. You could also try requiring ActionDispatch::Request & dependencies if you don't want to stub out the request object like I do here.

That would look something like

spec/lib/api_constraint.rb

require 'active_record_spec_helper'
require_relative '../../lib/api_constraint'

describe ApiConstraint do

  describe "#matches?" do

    let(:req) { Object.new }

     context "default version" do

       before :each do
         req.stub(:headers).and_return {}
         @opts = { :version => nil, :default => true }
       end

       it "returns true regardless of version number" do
         ApiConstraint.new(@opts).should match req
       end

     end

  end

end

...aaand I'll let you figure out exactly how to set up the context/write the expectations for your other tests.

这篇关于如何使用rspec测试路由约束的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆