I'm using Rails 7 and Ruby 3.1, and Shoulda Matchers for tests, but not Active Record, for I do not need a database. I want to validate numericality. However, validations do not work. It looks like input is transformed into integer, instead of being validated. I do not understand why that happens.
My model:
# app/models/grid.rb
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, inclusion: { in: 1..50 }, numericality: { only_integer: true }
# Some other code...
end
My test:
# spec/models/grid_spec.rb
RSpec.describe Grid, type: :model do
describe 'validations' do
shared_examples 'validates' do |field, range|
it { is_expected.to validate_numericality_of(field).only_integer }
it { is_expected.to validate_inclusion_of(field).in_range(range) }
end
include_examples 'validates', 'rows', 1..50
end
# Some other tests...
end
Nonetheless, my test fails:
Grid validations is expected to validate that :rows looks like an integer
Failure/Error: it { is_expected.to validate_numericality_of(field).only_integer }
Expected Grid to validate that :rows looks like an integer, but this
could not be proved.
After setting :rows to ‹"0.1"› -- which was read back as ‹0› -- the
matcher expected the Grid to be invalid and to produce the validation
error "must be an integer" on :rows. The record was indeed invalid,
but it produced these validation errors instead:
* rows: ["is not included in the list"]
As indicated in the message above, :rows seems to be changing certain
values as they are set, and this could have something to do with why
this test is failing. If you've overridden the writer method for this
attribute, then you may need to change it to make this test pass, or
do something else entirely.
Update
Worse than before, because tests are working but code is not actually working.
# app/models/grid.rb
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, presence: true, numericality: { only_integer: true, in: 1..50 }
# Some other code...
end
# spec/models/grid_spec.rb
RSpec.describe Grid, type: :model do
describe 'validations' do
shared_examples 'validates' do |field, type, range|
it { is_expected.to validate_presence_of(field) }
it do
validate = validate_numericality_of(field)
.is_greater_than_or_equal_to(range.min)
.is_less_than_or_equal_to(range.max)
.with_message("must be in #{range}")
is_expected.to type == :integer ? validate.only_integer : validate
end
end
include_examples 'validates', 'rows', :integer, 1..50
end
# Some other tests...
end
CodePudding user response:
The underlying problem (or maybe not a problem) is that you're typecasting rows
attribute to integer
.
>> g = Grid.new(rows: "num"); g.validate
>> g.errors.as_json
=> {:rows=>["is not included in the list"]}
# NOTE: only inclusion errors shows up, making numericality test fail.
To make it more obvious, let's remove inclusion
validation:
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, numericality: { only_integer: true }
end
Still, this does not fix numericality test:
# NOTE: still valid
>> Grid.new(rows: "I'm number, trust me").valid?
=> true
# NOTE: because `rows` is typecasted to integer, it will
# return `0` which is numerical.
>> Grid.new(rows: "I'm number, trust me").rows
=> 0
>> Grid.new(rows: 0.1).rows
=> 0
# NOTE: keep in mind, this is the current behavior, which
# might be unexpected.
In the test validate_numericality_of
, first of all, expects an invalid record with "0.1"
, but grid is still valid, which is why it fails.
Besides replacing the underlying validations, like you did, there are a few other options:
You could replace numericality test:
it { expect(Grid.new(rows: "number").valid?).to eq true }
it { expect(Grid.new(rows: "number").rows).to eq 0 }
# give it something not typecastable, like a class.
it { expect(Grid.new(rows: Grid).valid?).to eq false }
Or remove typecast:
attribute :rows
Update
Seems like you're trying to overdo it with validations and typecasting. From what I can see the only issue is just one test, everything else works fine. Anyway, I've came up with a few more workaraounds:
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, inclusion: 1..50, numericality: { only_integer: true }
def rows= arg
# NOTE: you might want to raise an error instead,
# because this validation will go away if you run
# validations again.
errors.add(:rows, "invalid") if (/\d / !~ arg.to_s)
super
end
end
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, inclusion: 1..50, numericality: { only_integer: true }
validate :validate_rows_before_type_cast
def validate_rows_before_type_cast
rows = @attributes.values_before_type_cast["rows"]
errors.add(:rows, :not_a_number) if rows.is_a?(String) && rows !~ /^\d $/
end
end
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveRecord::AttributeMethods::BeforeTypeCast
attribute :rows, :integer
validates :rows, inclusion: 1..50
# NOTE: this does show "Rows before type cast is not a number"
# maybe you'd want to customize the error message.
validates :rows_before_type_cast, numericality: { only_integer: true }
end
https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html