如何在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
相关产品推荐
相关产品推荐

