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

如何在Rails模型测试中验证PostgreSQL枚举约束?

测试PostgreSQL枚举的数据库约束(Rails + MiniTest)

问题场景

我在Rails和PostgreSQL中使用枚举,常规做法是测试模型验证是否有对应的数据库约束支持(例如模型的presence: true对应数据库的null: false):构造无效模型,跳过验证保存,确认抛出ActiveRecord::StatementInvalid错误。但测试枚举的数据库约束时,哪怕用write_attribute()设置无效枚举值,都会先触发Rails的ArgumentError,无法验证数据库层面的限制。

相关代码

模型定义

class SisRecord < ApplicationRecord
  enum record_type: {
    student: "student",
    staff: "staff",
    contact: "contact"
  }
  
  validates :record_type, presence: true
end

迁移文件

class CreateSisRecords < ActiveRecord::Migration[7.0]
  def change
    create_enum :sis_record_type, %w(student staff contact)
    
    create_table :sis_records do |t|
      t.enum :record_type, enum_type: :sis_record_type, null: false

      t.timestamps
    end
  end
end

现有测试代码(问题出在最后一个测试用例)

require "test_helper"

class SisRecordTest < ActiveSupport::TestCase
  test "a record_type is required" do
    record = sis_records(:valid_sis_record)
  
    record.record_type = nil
    assert_not record.save, "Saved the SIS Record without a record type"
  end
  
  test "a record_type is required by the database too" do
    record = sis_records(:valid_sis_record)
    
    record.record_type = nil
    assert_raises(ActiveRecord::StatementInvalid) {
      record.save(validate: false)
    }
  end
  
  test "record_type is restricted to accepted values" do
    accepted_values = %w(student staff contact)
    record = sis_records(:valid_sis_record)
    
    assert_nothing_raised {
      record.record_type = accepted_values.sample
    }
  
    assert_raises(ArgumentError) {
      record.record_type = "something else"
    }
  end
  
  test "record_type is restricted to accepted values by the database too" do
    accepted_values = %w(student staff contact)
    record = sis_records(:valid_sis_record)
    
    record.record_type = accepted_values.sample
    assert record.save, "Record didn't save despite accepted type value '#{record.record_type}'"
    
    record.write_attribute(:record_type, "nonsense") ### <-- 这里触发ArgumentError
    assert_raises(ActiveRecord::StatementInvalid) {
      record.save(validate: false)
    }
  end
end

解决方案

方法1:直接执行SQL语句(推荐)

绕过ActiveRecord的枚举封装,直接用SQL操作数据库,触发数据库层面的枚举约束错误:

test "record_type is restricted to accepted values by the database too" do
  record = sis_records(:valid_sis_record)
  
  # 测试更新操作触发约束
  assert_raises(ActiveRecord::StatementInvalid) do
    ActiveRecord::Base.connection.execute(
      "UPDATE sis_records SET record_type = 'nonsense' WHERE id = #{record.id}"
    )
  end

  # 额外测试插入操作触发约束
  assert_raises(ActiveRecord::StatementInvalid) do
    ActiveRecord::Base.connection.execute(
      "INSERT INTO sis_records (record_type, created_at, updated_at) VALUES ('invalid', NOW(), NOW())"
    )
  end
end

方法2:临时绕过Rails枚举校验

通过临时修改模型的枚举定义,跳过Rails的枚举值检查,之后再恢复:

test "record_type is restricted to accepted values by the database too" do
  record = sis_records(:valid_sis_record)
  
  # 临时移除原枚举的赋值校验
  SisRecord.remove_instance_variable(:@record_type_enum) if SisRecord.instance_variable_defined?(:@record_type_enum)
  SisRecord.define_method(:record_type=) { |value| write_attribute(:record_type, value) }
  
  record.write_attribute(:record_type, "nonsense")
  assert_raises(ActiveRecord::StatementInvalid) do
    record.save(validate: false)
  end
ensure
  # 恢复原枚举定义,避免影响其他测试
  SisRecord.enum record_type: { student: "student", staff: "staff", contact: "contact" }
end

方法3:使用AREL构建更新语句

用AREL生成数据库查询,既保持Rails风格,又绕过枚举校验:

test "record_type is restricted to accepted values by the database too" do
  record = sis_records(:valid_sis_record)
  table = SisRecord.arel_table
  
  update_manager = Arel::UpdateManager.new
  update_manager.table(table)
  update_manager.set(table[:record_type] => "nonsense")
  update_manager.where(table[:id].eq(record.id))
  
  assert_raises(ActiveRecord::StatementInvalid) do
    ActiveRecord::Base.connection.execute(update_manager.to_sql)
  end
end

注意事项

  • 方法1是最可靠的方案,直接对接数据库,不会干扰Rails模型的原有逻辑,测试结果准确。
  • 方法2需注意测试隔离,务必在ensure块中恢复模型定义,防止影响其他测试用例。
  • 方法3适合偏好使用AREL而非原生SQL的场景,但写法相对繁琐。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 06:57:37