How do I verify PostgreSQL enum constraints from Rails model test?

Viewed 24

I am using an enum in Rails and PostgreSQL. In my model tests I usually verify that my Rails validations are backed by database constraints where appropriate (e.g. presence: true in Model and null: false in DB). I do this by deliberately making the model invalid, attempting to save it without validations and making sure it raises a ActiveRecord::StatementInvalid error.

How do I test a PostgreSQL enum from MiniTest? My usual approach isn't working as everything I try to do to set my ActiveRecord model to an invalid enum value raises an ArgumentError, even using write_attribute() directly.

Is there a way to deliberately bypass the enum restrictions in Rails? Do I need to drop down out of ActiveRecord and send an AREL or SQL query direct to the database? Is there some other approach?

# Model
class SisRecord < ApplicationRecord
  enum record_type: {
    student: "student",
    staff: "staff",
    contact: "contact"
  }
  
  validates :record_type, presence: true
end
# Migration
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
# Test
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 Answers

I have an answer to my own question, but I'm still open to better answers.

I found a comment on a gist that showed how to fairly simply insert a record with Arel so for now I am using this approach:

# Just the test in question
   test "record_type is restricted to accepted values by the database too" do
    accepted_values = %w(student staff contact)
    table = Arel::Table.new(:sis_records)
    manager = Arel::InsertManager.new
    
    manager.insert [
      [table[:record_type], accepted_values.sample],
      [table[:created_at], Time.now],
      [table[:updated_at], Time.now],
    ]
    assert_nothing_raised {
      SisRecord.connection.insert(manager.to_sql)
    }
    
    manager.insert [
      [table[:record_type], "other type"],
      [table[:created_at], Time.now],
      [table[:updated_at], Time.now],
    ]
    assert_raises(ActiveRecord::StatementInvalid) {
      SisRecord.connection.insert(manager.to_sql)
    }
  end

created_at and updated_at are required fields so we have to add a value for those.

In my real case (not the simplified version I posted above), SisRecord belongs to Person so I had to provide a valid person ID (UUID) too. I did this by grabbing an ID from my people fixtures:

    manager.insert [
      [table[:record_type], "other type"],
      [table[:person_id], people(:valid_person).id], # <--------
      [table[:created_at], Time.now],
      [table[:updated_at], Time.now],
    ]
Related