如何使用rspec测试路由约束

18

我正在开发一个主要作为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
在每个路由中,我的Constraint是一个新的ApiConstraints对象,该对象位于我的./lib文件夹中。该类如下所示:
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
现在,手动测试时一切都按预期工作。在我的API中,每个版本可能有5到10个控制器,并且不想为每个单独的控制器测试API约束,因为这毫无意义。我正在寻找一个规范文件来测试我的API约束,但我不确定该把这个规范文件放在哪里。
我尝试添加了一个spec/routing/api_spec.rb文件来测试,但它没有正常工作,因为它抱怨某些内容未被提供,如下所示:
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 recognized options <{"format"=>"json", "action"=>"index", "controller"=>"api/v1/posts"}>
did not match <{"controller"=>"api/v1/posts"}>,
difference: <{"format"=>"json", "action"=>"index"}>.

请注意,控制器已正确确定,但由于我不想在此测试中测试格式和操作,因此它会出现错误。我希望有3个"API规范":

  • 将未版本化的请求路由到最新版本
  • 如果没有指定格式,则应默认为JSON格式
  • 在请求时应返回指定的API版本

有没有人有编写此类路由规范的经验?我不想为API内的每个控制器添加规范,因为它们不负责此功能。

1个回答

5
Rspec的route_to匹配器委托给ActionDispatch::Assertions::RoutingAssertions#assert_recognizes。传递给route_to的参数作为expected_options哈希传递(经过一些预处理,使其也能理解类似items#index的速记样式参数)。
您期望与route_to匹配器匹配的哈希表(即{:get => "/api/posts", :format => "json"})实际上不是expect的格式良好的参数。如果您查看源代码,可以看到我们通过以下方式获取要与之匹配的路径: path,query = *verb_to_path_map.values.first.split('?')

#first是一个明确的信号,表明我们期望一个只有一个键值对的哈希表。因此,:format => "json"组件实际上被丢弃了,没有起到任何作用。

ActionDispatch断言期望您将完整的路径+动词与完整的控制器、操作和路径参数集进行匹配。因此,rspec匹配器只是传递了它委托的方法的限制。

听起来rspec内置的route_to匹配器不能满足您的需求。因此,下一个建议是假设ActionDispatch将按预期执行,并为您的ApiConstraints类编写规范。

为了做到这一点,我首先建议不要使用默认的spec_helper。Corey Haines有一个不错的代码片段如何制作一个更快的spec helper,而不需要启动整个rails应用程序。它可能并不完全适合你的情况,但我想指出,因为你只是实例化基本的ruby对象,不需要任何rails魔法。如果你不想像我这样存根请求对象,你也可以尝试要求ActionDispatch::Request和依赖项。

这看起来会是这样的:

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

...然后我会让你自己想办法设置上下文并编写其他测试的期望结果。


是的,这是正确的。理想情况下,我希望在我的API规范文件中有三个测试,一个测试默认格式是否有效,一个测试当未指定版本时它是否路由到有效的控制器,以及一个测试当指定版本时它是否路由到正确的版本。 - Mike Trpcic
1
使用 route_to 时,您需要提供更具体的期望,例如 expect(:get => "/api/posts.json"').to route_to(:controller => "api/v1/posts", :action => "index", :format => "json")。不幸的是,默认的 rspec-rails 匹配器无法避免这种情况。 - gregates
问题在于每个规范将测试其他每个规范的逻辑。 这本质上是将所有规范合并成一个测试,这并不理想。 - Mike Trpcic
是的,我理解你的感受,只是想说rspec的路由规范DSL相当有限。我感觉它并没有被广泛使用。这可能是一个不错的主题,可以提交给rspec-rails进行修补。 - gregates
另外,我怀疑有些人会建议你只测试ApiConstraint类上的#matches?方法,并信任ActionDispatch来完成它的工作。 - gregates
这不是一个坏主意,因为ActionDispatch经过了彻底的测试。我该如何为lib文件夹中的ApiConstraints类编写规范(specs)?是在spec/lib/api_constraints_spec.rb中吗?需要包括什么? - Mike Trpcic

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