Ruby on Rails RSpec 测试模型只允许数据库中有一条记录
Ruby on Rails RSpec test for model to only allow one record in database
我正在尝试为我的模型构建一个 RSpec 测试规范:徽标,以确保只能将单个记录保存到数据库中。当我第二次调用 .build
方法构建徽标时,我的测试失败了,因为 FactoryBot 能够构建徽标。
但是,如果我对 FactoryBot 中的第二个 Logo 条目使用 .create
方法,我会收到测试错误,因为我的模型会根据我的模型的 :only_one_row
方法。
如何使用 RSpec 和 FactoryBot 完成这项工作?
这是我试过的代码,没有成功:
# app/models/logo.rb
class Logo < ApplicationRecord
before_create :only_one_row
private
def only_one_row
raise "You can only have one logo file for this website application" if Logo.count > 0
end
end
# spec/factories/logos.rb
FactoryBot.define do
factory :logo do
image { File.open(File.join(Rails.root, 'spec', 'fixtures', 'example_image.jpg')) }
end
end
# spec/logo_spec.rb
require 'rails_helper'
RSpec.describe Logo, type: :model do
it 'can be created' do
example_logo = FactoryBot.create(:logo)
expect(example_logo).to be_valid
end
it 'can not have more than one record' do
# Ensure there are no logo records in the database before this test is run.
Logo.destroy_all
example_logo_one = FactoryBot.create(:logo)
# This is where the trouble lies...
# If I go with .create method I error with the raised error defined in my model file...
example_logo_two = FactoryBot.create(:logo)
# ... if I go with the .build method I receive an error as the .build method succeeds
# example_logo_two = FactoryBot.build(:logo)
expect(example_logo_two).to_not be_valid
end
end
您在这里的验证是作为一个钩子实现的,而不是验证,这就是 be_valid
调用永远不会失败的原因。我想指出,从逻辑的角度来看,这里没有真正的问题——一个硬异常,因为在这种情况下完整性检查似乎是可以接受的,因为它不应该是应用程序试图做的事情。你甚至可以 re-write 你的测试来明确地测试它:
it 'can not have more than one record' do
# Ensure there are no logo records in the database before this test is run.
Logo.destroy_all
example_logo_one = FactoryBot.create(:logo)
expect { FactoryBot.create(:logo) }.to raise_error(RuntimeError)
end
但是,如果应用可能会尝试它并且您想要更好的用户体验,则可以将其构建为验证。棘手的部分是,对于未保存的徽标(我们需要确保没有其他保存的徽标,句号)与现有徽标(我们只需要验证我们是唯一的),验证看起来不同。我们可以通过确保没有徽标不是这个来完成一次检查:
class Logo < ApplicationRecord
validate do |logo|
if Logo.first && Logo.first != logo
logo.errors.add(:base, "You can only have one logo file for this website application")
end
end
end
此验证将允许第一个徽标保存,但应该立即知道第二个徽标无效,通过了您的原始规范。
When I utilize the .build method for the second call to build a Logo, my test fails because FactoryBot is able to build out a Logo.
没错,build
不保存对象。
However, if I use the .create method for the second Logo entry in FactoryBot I receive an error for the test because my model raises an error, as instructed, based upon my model's method for the :only_one_row method.
使用 expect
块和 raise_error
matcher.
捕获异常
context 'with one Logo already saved' do
let!(:logo) { create(:logo) }
it 'will not allow another' do
expect {
create(:logo)
}.to raise_error("You can only have one logo file for this website application")
end
end
注意这必须将异常消息硬编码到测试中。如果消息发生变化,则测试将失败。您可以测试 RuntimeError,但 any RuntimeError 将通过测试。
为避免这种情况,请创建 RuntimeError 的子类,引发它,并测试该特定异常。
class Logo < ApplicationRecord
...
def only_one_row
raise OnlyOneError if Logo.count > 0
end
class OnlyOneError < RuntimeError
MESSAGE = "You can only have one logo file for this website application".freeze
def initialize(msg = MESSAGE)
super
end
end
end
然后您可以测试该异常。
expect {
create(:logo)
}.to raise_error(Logo::OnlyOneError)
请注意,如果您的测试和测试数据库设置正确,则 Logo.destroy_all
应该是不必要的。每个测试示例都应该从一个干净的空数据库开始。
这里有两件事:
如果您的整个应用程序只允许一个徽标(而不是,比如说,每个公司、每个用户或其他任何一个徽标),那么我认为没有理由将它放入数据库中.相反,只需将它放入文件系统并完成它。
如果有充分的理由将它保存在数据库中,尽管我之前有评论,并且您真的想确保只有一个徽标,我非常建议在数据库级别设置此约束。我想到的两种方法是撤销相关 table 的 INSERT
权限,或者如果 table 已经有记录,则定义一个触发器来阻止 INSERT
查询。
这种方法很关键,因为它很容易忘记 1) 可以有意或无意地规避验证(save(validate: false)
、update_column
等)和 2) 数据库可以被您的应用以外的客户端访问(例如另一个应用程序,数据库自己的控制台工具等)。如果你想确保数据的完整性,你必须在数据库级别上做这些基本的事情。
我正在尝试为我的模型构建一个 RSpec 测试规范:徽标,以确保只能将单个记录保存到数据库中。当我第二次调用 .build
方法构建徽标时,我的测试失败了,因为 FactoryBot 能够构建徽标。
但是,如果我对 FactoryBot 中的第二个 Logo 条目使用 .create
方法,我会收到测试错误,因为我的模型会根据我的模型的 :only_one_row
方法。
如何使用 RSpec 和 FactoryBot 完成这项工作?
这是我试过的代码,没有成功:
# app/models/logo.rb
class Logo < ApplicationRecord
before_create :only_one_row
private
def only_one_row
raise "You can only have one logo file for this website application" if Logo.count > 0
end
end
# spec/factories/logos.rb
FactoryBot.define do
factory :logo do
image { File.open(File.join(Rails.root, 'spec', 'fixtures', 'example_image.jpg')) }
end
end
# spec/logo_spec.rb
require 'rails_helper'
RSpec.describe Logo, type: :model do
it 'can be created' do
example_logo = FactoryBot.create(:logo)
expect(example_logo).to be_valid
end
it 'can not have more than one record' do
# Ensure there are no logo records in the database before this test is run.
Logo.destroy_all
example_logo_one = FactoryBot.create(:logo)
# This is where the trouble lies...
# If I go with .create method I error with the raised error defined in my model file...
example_logo_two = FactoryBot.create(:logo)
# ... if I go with the .build method I receive an error as the .build method succeeds
# example_logo_two = FactoryBot.build(:logo)
expect(example_logo_two).to_not be_valid
end
end
您在这里的验证是作为一个钩子实现的,而不是验证,这就是 be_valid
调用永远不会失败的原因。我想指出,从逻辑的角度来看,这里没有真正的问题——一个硬异常,因为在这种情况下完整性检查似乎是可以接受的,因为它不应该是应用程序试图做的事情。你甚至可以 re-write 你的测试来明确地测试它:
it 'can not have more than one record' do
# Ensure there are no logo records in the database before this test is run.
Logo.destroy_all
example_logo_one = FactoryBot.create(:logo)
expect { FactoryBot.create(:logo) }.to raise_error(RuntimeError)
end
但是,如果应用可能会尝试它并且您想要更好的用户体验,则可以将其构建为验证。棘手的部分是,对于未保存的徽标(我们需要确保没有其他保存的徽标,句号)与现有徽标(我们只需要验证我们是唯一的),验证看起来不同。我们可以通过确保没有徽标不是这个来完成一次检查:
class Logo < ApplicationRecord
validate do |logo|
if Logo.first && Logo.first != logo
logo.errors.add(:base, "You can only have one logo file for this website application")
end
end
end
此验证将允许第一个徽标保存,但应该立即知道第二个徽标无效,通过了您的原始规范。
When I utilize the .build method for the second call to build a Logo, my test fails because FactoryBot is able to build out a Logo.
没错,build
不保存对象。
However, if I use the .create method for the second Logo entry in FactoryBot I receive an error for the test because my model raises an error, as instructed, based upon my model's method for the :only_one_row method.
使用 expect
块和 raise_error
matcher.
context 'with one Logo already saved' do
let!(:logo) { create(:logo) }
it 'will not allow another' do
expect {
create(:logo)
}.to raise_error("You can only have one logo file for this website application")
end
end
注意这必须将异常消息硬编码到测试中。如果消息发生变化,则测试将失败。您可以测试 RuntimeError,但 any RuntimeError 将通过测试。
为避免这种情况,请创建 RuntimeError 的子类,引发它,并测试该特定异常。
class Logo < ApplicationRecord
...
def only_one_row
raise OnlyOneError if Logo.count > 0
end
class OnlyOneError < RuntimeError
MESSAGE = "You can only have one logo file for this website application".freeze
def initialize(msg = MESSAGE)
super
end
end
end
然后您可以测试该异常。
expect {
create(:logo)
}.to raise_error(Logo::OnlyOneError)
请注意,如果您的测试和测试数据库设置正确,则 Logo.destroy_all
应该是不必要的。每个测试示例都应该从一个干净的空数据库开始。
这里有两件事:
如果您的整个应用程序只允许一个徽标(而不是,比如说,每个公司、每个用户或其他任何一个徽标),那么我认为没有理由将它放入数据库中.相反,只需将它放入文件系统并完成它。
如果有充分的理由将它保存在数据库中,尽管我之前有评论,并且您真的想确保只有一个徽标,我非常建议在数据库级别设置此约束。我想到的两种方法是撤销相关 table 的 INSERT
权限,或者如果 table 已经有记录,则定义一个触发器来阻止 INSERT
查询。
这种方法很关键,因为它很容易忘记 1) 可以有意或无意地规避验证(save(validate: false)
、update_column
等)和 2) 数据库可以被您的应用以外的客户端访问(例如另一个应用程序,数据库自己的控制台工具等)。如果你想确保数据的完整性,你必须在数据库级别上做这些基本的事情。