我使用Rails 7和Ruby 3.1,以及Shoulda匹配器进行测试,但不是活动记录,因为我不需要数据库。我想验证数字的有效性。然而,验证不起作用。看起来输入被转换成整数,而不是被验证。我不明白为什么会这样。
我的模型:
# 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
我的测试:
# 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
然而,我的测试失败了:
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.
<标题>更新比以前更糟,因为测试工作,但代码实际上没有工作。
# 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
标题>根本问题(或也许不是问题)是你铸字rows
属性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.
为了更明显,让我们删除inclusion
验证:
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, numericality: { only_integer: true }
end
然而,这并不能解决数字测试:
# 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.
在测试validate_numericality_of
中,首先,期望"0.1"
的记录无效,但是grid仍然有效,这就是为什么它失败的原因。
除了像您那样替换底层验证之外,还有一些其他选项:
你可以替换数字测试:
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 }
或remove typeccast:
attribute :rows
似乎你试图过度使用验证和类型转换。在我看来,唯一的问题只是一个测试,其他一切都很好。无论如何,我想出了更多的变通方法:
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
我的解决方案是将验证和类型转换划分为模型。
# app/models/grid.rb
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
end
# app/models/grid_data.rb
class GridData
include ActiveModel::Model
include ActiveModel::Attributes
Grid.attribute_names.each { |name| attribute name }
validates(*attribute_names, presence: true)
validates :rows, numericality: { only_integer: true, in: 1..50 }
end
规格
# spec/models
RSpec.describe Grid, type: :model do
let(:grid) { described_class.new(**attributes) }
describe 'type cast' do
let(:attributes) { default(rows: '2') }
it 'parses string valid arguments to integer or float' do
expect(grid.rows).to eq 2
end
end
end
RSpec.describe GridData, type: :model do
it 'has same attributes as Grid model' do
expect(described_class.attribute_names).to eq Grid.attribute_names
end
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)
validate = validate.only_integer if type == :integer
expect(subject).to validate
end
it do
expect(subject).to validate_inclusion_of(field)
.in_range(range)
.with_message("must be in #{range}")
end
end
include_examples 'validates', 'rows', :integer, 1..50
end
end
控制器
# app/controller/grids_controller.rb
class GridsController < ApplicationController
def create
@grid_data = GridData.new(**grid_params)
if @grid_data.valid?
play
else
render :new, status: :unprocessable_entity
end
end
private
def grid_params
params.require(:grid_data).permit(*Grid.attribute_names)
end
def play
render :play, status: :created
Grid.new(**@grid_data.attributes).play
end
end