You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rails Rspec测试Post更新/删除时HTTP 302错误排查求助

Rails Rspec测试中Update和Destroy动作返回302而非预期状态码问题

在Rails项目中使用Rspec测试Posts控制器时,执行update动作返回HTTP 302状态码(而非预期的200),destroy动作也返回302(而非预期的204),但create动作测试正常。update动作使用独立的Posts::Update资源类,其逻辑与create类似。以下是相关代码及测试失败详情:

相关代码

posts_controller.rb

class PostsController < ApplicationController
  before_action :set_post, only: %i[show update destroy]
  before_action :authenticate_user!, except: %i[index show]

  def index
    posts = Posts::List.new(params).execute
    render json: posts, meta: pagination(posts), each_serializer: PostSerializer, status: :ok
  end

  def show
    authorize @post
    render json: @post, serializer: PostSerializer, list_comments: true, status: :ok
  end

  def create
    @post = authorize Posts::Create.new(post_params, current_user).execute

    render json: @post, serializer: PostSerializer, status: :created
  end

  def update
    @post = authorize Posts::Update.new(post_params, @post).execute
    render json: @post, serializer: PostSerializer, status: :ok
  end

  def destroy
    authorize Posts::Destroy.new(@post).execute
    head :ok
  end

  private

  # Use callbacks to share common setup or constraints between actions.
  def set_post
    @post = Post.find(params[:id])
    authorize @post
  end

  # Only allow a list of trusted parameters through.
  def post_params
    params.require(:post).permit(:title, :description, :category_id)
  end
end

Posts::Update资源类(update.rb)

class Posts::Update
  attr_accessor :params, :post

  def initialize(params, post)
    @params = params
    @post = post
  end

  def execute
    post.update!(mount_params)
  end

  private

  def mount_params
    {
      title: params[:title] || post.title,
      description: params[:description] || post.description,
      category_id: params[:category_id] || post.category_id
    }
  end
end

posts_controller_spec.rb

require 'rails_helper'
require './spec/helpers/authentication_helper'

RSpec.describe PostsController, :focus, type: :controller do
  include AuthenticationHelper
  include Devise::Test::ControllerHelpers
  attr_accessor :post_one, :post_two, :post_three, :user

  before(:all) do
    posts = FactoryBot.create_list(:post, 3, :with_comments)
    @user = FactoryBot.create(:user)
    @post_one = posts.first
    @post_two = posts.second
    @post_three = posts.third
  end
  let(:valid_headers) do
    user.create_new_auth_token
  end
  let(:root_keys) { %w[post] }
  let(:expected_post_keys) { %w[id title description category_id user_id] }
  let(:expected_meta_keys) { %w[current_page per_page total_pages total_count] }
  let(:error_root_keys) { %w[error] }
  let(:expected_error_keys) { %w[message] }
  let(:params) do
    {
      post: {
        title: Faker::Quote.yoda,
        description: Faker::Lorem.characters(number: 15),
        category_id: FactoryBot.create(:category).id
      }
    }
  end

  describe 'GET #index' do
    let(:root_keys) { %w[posts meta] }

    before do
      get :index
      @body = JSON.parse(response.body)
    end

    it 'return status code :ok' do
      expect(response).to have_http_status(:ok)
    end

    it 'match with root keys' do
      expect(@body.keys).to contain_exactly(*root_keys)
    end

    it 'match with posts keys' do
      @body['posts'].map do |post|
        expect(post.keys).to contain_exactly(*expected_post_keys)
      end
    end

    it 'match with meta keys' do
      expect(@body['meta'].keys).to contain_exactly(*expected_meta_keys)
    end
  end

  describe 'GET #show' do
    context 'when post does not exists' do
      let(:root_keys) { %w[error] }
      let(:expected_error_keys) { %w[message] }

      before do
        get :show, params: { id: Faker::Number.number }
        @body = JSON.parse(response.body)
      end

      it 'return status code :not_found' do
        expect(response).to have_http_status(:not_found)
      end

      it 'match with root keys' do
        expect(@body.keys).to contain_exactly(*error_root_keys)
      end

      it 'match with post keys' do
        expect(@body['error'].keys).to contain_exactly(*expected_error_keys)
      end
    end

    context 'when post exists' do
      let(:expected_post_keys) { %w[id title description category_id user_id comments] }
      let(:expected_comment_keys) { %w[id comment post_id created_at] }

      before do
        get :show, params: { id: post_one.id }
        @body = JSON.parse(response.body)
      end

      it 'return status code :ok' do
        expect(response).to have_http_status(:ok)
      end

      it 'match with root keys' do
        expect(@body.keys).to contain_exactly(*root_keys)
      end

      it 'match with post keys' do
        expect(@body['post'].keys).to contain_exactly(*expected_post_keys)
      end

      it 'match with comment keys' do
        @body['post']['comments'].map do |comment|
          expect(comment.keys).to contain_exactly(*expected_comment_keys)
        end
      end
    end
  end

  describe 'POST #create' do
    before do
      set_authentication_headers_for(user)
      post :create, params: params
      @body = JSON.parse(response.body)
    end

    it 'return status code :created' do
      expect(response).to have_http_status(:created)
    end

    it 'match with root keys' do
      expect(@body.keys).to contain_exactly(*root_keys)
    end

    it 'match with post keys' do
      expect(@body['post'].keys).to contain_exactly(*expected_post_keys)
    end
  end

  describe 'PUT #update' do
    context 'when post does not exists' do
      before do
        set_authentication_headers_for(user)
        put :update, params: params.merge({ id: Faker::Number.number })
        @body = JSON.parse(response.body)
      end

      it 'return status code :not_found' do
        expect(response).to have_http_status(:not_found)
      end

      it 'match with root keys' do
        expect(@body.keys).to contain_exactly(*error_root_keys)
      end

      it 'match with post keys' do
        expect(@body['error'].keys).to contain_exactly(*expected_error_keys)
      end
    end

    context 'when post exists' do
      before do
        set_authentication_headers_for(user)
        put :update, params: params.merge({ id: post_one.id })

        @body = JSON.parse([response.body].to_json).first
      end

      it 'return status code :ok' do
        expect(response).to have_http_status(:ok)
      end

      it 'match with root keys' do
        expect(@body.keys).to contain_exactly(*root_keys)
      end

      it 'match with post keys' do
        expect(@body['post'].keys).to contain_exactly(*expected_post_keys)
      end
    end
  end

  describe 'DESTROY #destroy' do
    context 'when post does not exists' do
      before do
        set_authentication_headers_for(user)
        delete :destroy, params: params.merge({ id: Faker::Number.number })
        @body = JSON.parse(response.body)
      end

      it 'return status code :not_found' do
        expect(response).to have_http_status(:not_found)
      end

      it 'match with root keys' do
        expect(@body.keys).to contain_exactly(*error_root_keys)
      end

      it 'match with post keys' do
        expect(@body['error'].keys).to contain_exactly(*expected_error_keys)
      end
    end

    context 'when post exists' do
      before do
        set_authentication_headers_for(user)
        delete :destroy, params: { id: post_one.id }
      end

      it 'return status code :no_content' do
        expect(response).to have_http_status(:no_content)
      end
    end
  end
end

测试失败结果

PostsController
  GET #index
    return status code :ok
    match with root keys
    match with posts keys
    match with meta keys
  GET #show
    when post does not exists
      return status code :not_found
      match with root keys
      match with post keys
    when post exists
      return status code :ok
      match with root keys
      match with post keys
      match with comment keys
  POST #create
    return status code :created
    match with root keys
    match with post keys
  PUT #update
    when post does not exists
      return status code :not_found
      match with root keys
      match with post keys
    when post exists
      return status code :ok (FAILED - 1)
      match with root keys (FAILED - 2)
      match with post keys (FAILED - 3)
  DESTROY #destroy
    when post does not exists
      return status code :not_found
      match with root keys
      match with post keys
    when post exists
      return status code :no_content (FAILED - 4)

Failures:

  1) PostsController PUT #update when post exists return status code :ok
     Failure/Error: expect(response).to have_http_status(:ok)
       expected the response to have status code :ok (200) but it was :found (302)
     # ./spec/controllers/posts_controllers_spec.rb:163:in `block (4 levels) in <top (required)>'
     # ./spec/support/database_cleaner.rb:9:in `block (3 levels) in <main>'
     # ./spec/support/database_cleaner.rb:8:in `block (2 levels) in <main>'

  2) PostsController PUT #update when post exists match with root keys
     Failure/Error: expect(@body.keys).to contain_exactly(*root_keys)
     
     NoMethodError:
       undefined method `keys' for #<String:0x0000555e1e849aa0>
     # ./spec/controllers/posts_controllers_spec.rb:167:in `block (4 levels) in <top (required)>'
     # ./spec/support/database_cleaner.rb:9:in `block (3 levels) in <main>'
     # ./spec/support/database_cleaner.rb:8:in `block (2 levels) in <main>'

  3) PostsController PUT #update when post exists match with post keys
     Failure/Error: expect(@body['post'].keys).to contain_exactly(*expected_post_keys)
     
     NoMethodError:
       undefined method `keys' for nil:NilClass
     # ./spec/controllers/posts_controllers_spec.rb:171:in `block (4 levels) in <top (required)>'
     # ./spec/support/database_cleaner.rb:9:in `block (3 levels) in <main>'
     # ./spec/support/database_cleaner.rb:8:in `block (2 levels) in <main>'

  4) PostsController DESTROY #destroy when post exists return status code :no_content
     Failure/Error: expect(response).to have_http_status(:no_content)
       expected the response to have status code :no_content (204) but it was :found (302)
     # ./spec/controllers/posts_controllers_spec.rb:204:in `block (4 levels) in <top (required)>'
     # ./spec/support/database_cleaner.rb:9:in `block (3 levels) in <main>'
     # ./spec/support/database_cleaner.rb:8:in `block (2 levels) in <main>'

Finished in 1.64 seconds (files took 5.32 seconds to load)
24 examples, 4 failures

Failed examples:

rspec ./spec/controllers/posts_controllers_spec.rb:162 # PostsController PUT #update when post exists return status code :ok
rspec ./spec/controllers/posts_controllers_spec.rb:166 # PostsController PUT #update when post exists match with root keys
rspec ./spec/controllers/posts_controllers_spec.rb:170 # PostsController PUT #update when post exists match with post keys
rspec ./spec/controllers/posts_controllers_spec.rb:203 # PostsController DESTROY #destroy when post exists return status code :no_content

已搜索类似错误但未找到合适的解决方案。

内容的提问来源于stack exchange,提问作者Marcos Eduardo Santos Henke

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 14:46:59