rails+rspec 具有多态关联的控制器规范

rails+rspec controller spec with polymorphic association

我正在尝试测试控制器规范中的操作,但出于某种原因,我收到了“无路由匹配”错误。我应该怎么做才能使路线正常运行?

ActionController::UrlGenerationError:
   No route matches {:action=>"create", :comment=>{:body=>"Consectetur quo accusamus ea.", 
   :commentable=>"4"}, :controller=>"comments", :post_id=>"4"}

型号

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true, touch: true 

class Post < ActiveRecord::Base
  has_many :comments, as: :commentable, dependent: :destroy

路线

resources :posts do
  resources :comments, only: [:create, :update, :destroy], module: :posts
end

controller_spec

describe "POST create" do
  let!(:user) { create(:user) }
  let!(:profile) { create(:profile, user: @user) }
  let!(:commentable) { create(:post, user: @user) }

  context "with valid attributes" do
    subject(:create_action) { xhr :post, :create, post_id: commentable, comment: attributes_for(:comment, commentable: commentable, user: @user) }

    it "saves the new task in the db" do
      expect{ create_action }.to change{ Comment.count }.by(1)
    end
    ...

编辑

上面的 controller_spec 可以在 spec/controllers/comments_controller_spec.rb

中找到

controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_action :authenticate_user!

  def create
    @comment = @commentable.comments.new(comment_params)
    authorize @comment
    @comment.user = current_user
    if @comment.save
      @comment.send_comment_creation_notification(@commentable)
      respond_to :js
    end
  end

controllers/posts/comments_controller.rb

  class Posts::CommentsController < CommentsController
    before_action :set_commentable

    private

    def set_commentable
      @commentable = Post.find(params[:post_id])
    end

使用 module: :posts 将路由到 Posts::CommentsController#create

如果这不是您想要的,请删除模块选项。

否则您需要确保您的控制器和规范的 class 名称正确。

class Posts::CommentsController
  def create

  end
end

RSpec.describe Posts::CommentsController do
  # ...
end

另请注意,嵌套资源的 "individual actions" 通常没有意义。

相反,您可能希望像这样声明路由:

resources :comments, only: [:update, :destroy] # show, edit ...

resources :posts do
  resources :comments, only: [:create], module: :posts # new, index
end

这给你:

class CommentsController < ApplicationController

  before_action :set_posts

  # DELETE /comments/:id
  def destroy
     # ...
  end

  # PUT|PATCH /comments/:id
  def update
  end
end 

class Posts::CommentsController < ApplicationController
  # POST /posts/:post_id/comments
  def create
     # ...
  end
end 

有关原因的更深入解释,请参阅 Avoid Deeply Nested Routes in Rails


在这种情况下将控制器设置为使用继承是一个好主意 - 但是您无法通过控制器规范中的父 CommentsController class 测试 create 方法,因为RSpec 在尝试解析路由时将始终查看 described_class

相反,您可能想使用共享示例:

# /spec/support/shared_examples/comments.rb
RSpec.shared_examples "nested comments controller" do |parameter|
  describe "POST create" do
    let!(:user) { create(:user) }

    context "with valid attributes" do
      subject(:create_action) { xhr :post, :create, post_id: commentable, comment: attributes_for(:comment, commentable: commentable, user: @user) }

      it "saves the new task in the db" do
        expect{ create_action }.to change{ Comment.count }.by(1)
      end
    end
  end
end

require 'rails_helper'
require 'shared_examples/comments'
RSpec.describe Posts::CommentsController
  # ...
  include_examples "nested comments controller" do
    let(:commentable) { create(:post, ...) }
  end
end

require 'rails_helper'
require 'shared_examples/comments'
RSpec.describe Products::CommentsController
  # ...
  include_examples "nested comments controller" do
    let(:commentable) { create(:product, ...) }
  end
end

我更喜欢的另一种选择是改用请求规范:

require 'rails_helper'
RSpec.describe "Comments", type: :request do

  RSpec.shared_example "has nested comments" do
    let(:path) { polymorphic_path(commentable) + "/comments" } 
    let(:params) { attributes_for(:comment) }

    describe "POST create" do
      expect do
        xhr :post, path, params
      end.to change(commentable.comments, :count).by(1)
    end
  end


  context "Posts" do
     include_examples "has nested comments" do
       let(:commentable) { create(:post) }
     end
  end

  context "Products" do
     include_examples "has nested comments" do
       let(:commentable) { create(:product) }
     end
  end
end

由于您实际上是在发送 HTTP 请求而不是伪造它,因此它们涵盖了更多的应用程序堆栈。然而,就测试速度而言,这确实要付出很小的代价。 shared_context 和 shared_examples 都是让 RSpec 非常棒的两件事。