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

如何用RSpec测试Rails中的乐观锁实现?

如何用RSpec测试乐观锁的Update动作

首先咱们先梳理下你现有测试代码的问题:

  • 你的两次patch请求都没携带lock_version参数,Rails的乐观锁需要客户端提交当前的lock_version值才会触发版本校验;如果不传这个参数,Rails会直接忽略乐观锁检查,自然不会抛出StaleObjectError。
  • 另外你的控制器已经rescue了StaleObjectError并执行retry,这个异常不会向外抛出,所以测试里用expect(response).to raise_error(...)是捕获不到的——因为控制器已经把异常内部处理掉了。

接下来我给你写两种场景的正确测试用例,覆盖乐观锁的核心逻辑:

场景1:验证过期版本请求会触发重试并最终更新成功

这个测试模拟并发修改场景:先获取初始版本号,用旧版本发起更新触发乐观锁异常,再验证控制器会自动reload模型并重试,最终完成更新。

require 'rails_helper'

RSpec.describe OperatorsController, type: :controller do
  let(:site) { FactoryBot.create(:site) }
  let!(:operator1) { FactoryBot.create(:operator, site: site) }
  let(:valid_attributes) { 
    { 
      first_name: "test1", 
      last_name: "test2", 
      employee_number: "test12345", 
      badge: "test215235", 
      suspended: true, 
      site_id: site.id 
    } 
  }

  it "retries and succeeds when optimistic lock error is raised" do
    # 记录初始的lock_version
    initial_lock_version = operator1.lock_version

    # 第一次请求:用初始版本更新,让lock_version递增
    patch :update, params: { id: operator1.id, operator: valid_attributes.merge(lock_version: initial_lock_version) }
    expect(response).to have_http_status(:ok)
    operator1.reload
    expect(operator1.lock_version).to eq(initial_lock_version + 1)

    # 准备用旧版本号发起第二次请求
    stale_attributes = valid_attributes.merge(
      first_name: "test_fake",
      lock_version: initial_lock_version  # 携带过期的版本号
    )

    # 验证StaleObjectError确实被触发,且控制器会重试更新
    expect_any_instance_of(Operator).to receive(:update_attributes).and_raise(ActiveRecord::StaleObjectError).once
    expect_any_instance_of(Operator).to receive(:update_attributes).with(stale_attributes.except(:lock_version)).and_return(true).once

    patch :update, params: { id: operator1.id, operator: stale_attributes }
    
    # 验证最终记录被成功更新
    operator1.reload
    expect(operator1.first_name).to eq("test_fake")
    expect(response).to have_http_status(:ok)
  end
end

场景2:直接测试模型层的乐观锁异常触发逻辑

如果你想单独验证乐观锁的核心机制,绕开控制器的rescue逻辑,直接测试模型层会更直观:

RSpec.describe Operator, type: :model do
  let(:site) { FactoryBot.create(:site) }
  let!(:operator) { FactoryBot.create(:operator, site: site) }

  it "raises StaleObjectError when updating with stale lock_version" do
    # 创建两个指向同一条记录的实例
    operator1 = Operator.find(operator.id)
    operator2 = Operator.find(operator.id)

    # 先更新第一个实例,让lock_version递增
    operator1.update!(first_name: "updated")
    expect(operator1.lock_version).to eq(operator.lock_version + 1)

    # 用第二个实例(仍持有旧lock_version)更新,会触发乐观锁异常
    expect {
      operator2.update!(first_name: "stale_update")
    }.to raise_error(ActiveRecord::StaleObjectError)
  end
end

额外小提示

你的控制器代码里的retry没有次数限制,可能导致无限重试的风险,建议加上重试次数控制:

retry_count = 0
begin
  @update_operator = Operator.find(params[:id])
  authorize! :update, @update_operator
  if @update_operator.update_attributes(operator_params)
    render json: @update_operator, except: :badge
  else
    render json: @update_operator.errors, status: :unprocessable_entity
  end
rescue ActiveRecord::StaleObjectError
  retry_count += 1
  if retry_count < 3
    @update_operator.reload
    retry
  else
    render json: { error: "Conflict: record was updated by another process" }, status: :conflict
  end
end

内容的提问来源于stack exchange,提问作者Darren

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 09:07:10