gpt4 book ai didi

ruby-on-rails - 由于模型中的外键,FactoryGirl 炸毁了规范

转载 作者:行者123 更新时间:2023-12-04 20:38:00 27 4
gpt4 key购买 nike

我有一个模型 Foo,它有 state_code作为外键。 States 表是一个(或多或少)静态表,用于保存 50 个州的代码和名称,以及其他美国邮政编码(例如波多黎各的“PR”)。我选择使用 state_code作为状态的主键和 Foo 的外键,而不是像 state_id 这样的东西.它更适合人类阅读,并简化了我想要调用状态代码的 View 逻辑。 ( 编辑 - 只是为了澄清:我不是说调用代码从 View 访问模型;我的意思是将状态显示为 @foo.state_code 似乎比 @foo.state.state_code 更简单。)

Foo 还有一个 has_many与模型 Bar 的关系。两个模型规范都通过了有效工厂的规范,但是由于某种原因,在运行构建 Bar 实例的功能规范时,由于与 state_code 相关的外键问题,测试失败了。

我的所有模型都通过了模型规范,包括对有效工厂的测试。但是,每当我尝试为“Bar”创建测试对象时,都会遇到麻烦。使用 buildstate_code 的外键错误而崩溃在 Foo 中(尽管 Foo 工厂明确指定了一个值,该值被确认为在 States 中作为 state_code 存在)。使用 build_stubbed因为 Bar 对象似乎没有持久化该对象。

型号:

# models/foo.rb
class Foo < ActiveRecord
belongs_to :state, foreign_key: 'state_code', primary_key: 'state_code'
has_many :bars
validates :state_code, presence: true, length: { is: 2 }

# other code omitted...
end

# models/state.rb
class State < ActiveRecord
self.primary_key = 'state_code'
has_many :foos, foreign_key: 'state_code'
validates :state_code, presence: true, uniqueness: true, length: { is: 2 }

# other code omitted...
end

# models/bar.rb
class Bar < ActiveRecord
belongs_to :foo

# other code omitted
end

下面的工厂为我的 Foo 和 Bar 模型通过了绿色,所以从模型的角度来看,工厂看起来不错:
# spec/factores/foo_bar_factory.rb
require 'faker'
require 'date'

FactoryGirl.define do
factory :foo do
name { Faker::Company.name }
city { Faker::Address.city }
website { Faker::Internet.url }
state_code { 'AZ' } # Set code for Arizona b/c doesn't matter which state
end

factory :bar do
name { Faker::Name.name }
website_url { Faker::Internet.url }
# other columns omitted
association :foo
end
end

...基本规范是:
# spec/models/foo_spec.rb
require 'rails_helper'

describe Foo, type: :model do
let(:foo) { build(:foo) }

it "has a valid factory" do
expect(foo).to be_valid
end

# code omitted...
end

# spec/models/bar_spec.rb
require 'rails_helper'

describe Bar, type: :model do
let(:bar) { build_stubbed(:bar) } # have to build_stubbed - build causes error

it "has a valid factory" do
expect(bar).to be_valid
end
end

这个规范通过了,没有问题。但是如果我使用 build(:bar)用于 Bar 而不是 build_stubbed ,我收到外键错误:
1) Bar has a valid factory
Failure/Error: let(:bar) { build(:bar) }
ActiveRecord::InvalidForeignKey:
PG::ForeignKeyViolation: ERROR: insert or update on table "bars" violates foreign key constraint "fk_rails_3dd3a7c4c3"
DETAIL: Key (state_code)=(AZ) is not present in table "states".

代码“AZ”肯定在状态表中,所以我不清楚它为什么会失败。

在功能规范中,我试图创建保留在数据库中的 bar 实例,以便我可以测试它们是否在 #index、#show 和 #edit 操作中正确显示。但是我似乎无法让它正常工作。功能规范失败:
# 规范/功能/bar_pages_spec.rb
需要'rails_helper'
feature "Bar pages" do
context "when signed in as admin" do
let!(:bar_1) { build_stubbed(:bar) }
let!(:bar_2) { build_stubbed(:bar) }
let!(:bar_3) { build_stubbed(:bar) }

# code omitted...

scenario "clicking manage bar link shows all bars" do
visit root_path
click_link "Manage bars"
save_and_open_page

expect(page).to have_css("tr td a", text: bar_1.name)
expect(page).to have_css("tr td a", text: bar_2.name)
expect(page).to have_css("tr td a", text: bar_3.name)
end
end

此规范失败,并显示一条指示没有匹配项的消息。使用 save_and_open_page不在 View 中显示预期的项目。 (不过,我有一个包含开发数据的工作页面,所以我知道该逻辑实际上按预期工作)。 The thoughtbot post on build_stubbed 表示它应该持久化对象:

It makes objects look look like they’ve been persisted, creates associations with the build_stubbed strategy (whereas build still uses create), and stubs out a handful of methods that interact with the database and raises if you call them.



...但在我的规范中似乎没有这样做。正在尝试使用 build代替 build_stubbed在此规范中生成与上述相同的外键错误。

我真的被困在这里了。这些模型似乎具有有效的工厂并通过了所有规范。但是功能规范要么破坏了外键关系,要么似乎不存在 build_stubbed View 之间的对象。感觉一团糟,但我想不出正确的方法来解决它。我在实践中有实际的工作 View ,可以满足我的期望 - 但我希望测试覆盖率有效。

更新

我回去更新了所有模型代码以删除 state_code 的自然键。 .我遵循了@Max 的所有建议。 Foo 表现在使用 state_id作为 states 的外键;我复制了 app/models/concerns/belongs_to_state.rb 的代码如推荐等。

更新了 schema.rb:
create_table "foos", force: :cascade do |t|
# columns omitted
t.integer "state_id"
end

create_table "states", force: :cascade do |t|
t.string "code", null: false
t.string "name"
end

add_foreign_key "foos", "states"

模型规范通过了,我的一些更简单的功能规范也通过了。我现在意识到问题仅在于创建了多个 Foo 对象时。发生这种情况时,第二个对象会由于列 :code 上的唯一性约束而失败。
Failure/Error: let!(:foo_2) { create(:foo) }
ActiveRecord::RecordInvalid:
Validation failed: Code has already been taken

我试图设置 :state_id列直接在工厂中用于 :foo 以避免调用 :state 工厂。例如。
# in factory for foo:
state_id { 1 }

# generates following error on run:
Failure/Error: let!(:foo_1) { create(:foo) }
ActiveRecord::InvalidForeignKey:
PG::ForeignKeyViolation: ERROR: insert or update on table "foos" violates foreign key constraint "fk_rails_5f3d3f12c3"
DETAIL: Key (state_id)=(1) is not present in table "states".

显然 state_id不在州,因为它是 id状态,和 state_id在福斯。另一种方法:
# in factory for foo:
state { 1 } # alternately w/ same error -> state 1

ActiveRecord::AssociationTypeMismatch:
State(#70175500844280) expected, got Fixnum(#70175483679340)

或者:
# in factory for foo:
state { State.first }

ActiveRecord::RecordInvalid:
Validation failed: State can't be blank

我真正想要做的就是创建一个 Foo 对象的实例,并让它包含与 states 中的状态之一的关系。 table 。我预计不会对 states 进行大量更改表 - 这真的只是一个引用。

不要需要创建一个新的状态。我只需要填充外键 state_id在具有 :id 中的 66 个值之一的 Foo 对象上状态表上的列。从概念上讲,工厂为 :foo理想情况下,为 :state_id 选择一个 1 到 66 之间的整数值.它在控制台中工作:
irb(main):001:0> s = Foo.new(name: "Test", state_id: 1)
=> #<Foo id: nil, name: "Test", city: nil, created_at: nil, updated_at: nil, zip_code: nil, state_id: 1>
irb(main):002:0> s.valid?
State Load (0.6ms) SELECT "states".* FROM "states" WHERE "states"."id" = $1 LIMIT 1 [["id", 1]]
State Exists (0.8ms) SELECT 1 AS one FROM "states" WHERE ("states"."code" = 'AL' AND "states"."id" != 1) LIMIT 1
=> true

我现在能看到的唯一出路是摆脱 :code 上的唯一性约束。栏目 states .或者 - 删除 foos 之间的外键约束和 states ,并让 Rails 强制执行这种关系。

对不起,大量的帖子......

最佳答案

我将在 *rse 中感到痛苦,并认为约定可能比开发人员的便利性和感知的可读性更重要。

Rails 的一大优点是强大的约定允许我们打开任何项目并很快弄清楚发生了什么(假设原作者不是完全黑客)。用 PHP 项目试试。

这些约定之一是外键后缀 _id .许多其他组件(例如 FactoryGirl)依赖于这些约定。

我还认为,如果您的应用在美国以外的地区使用,使用州代码作为主要 ID 会导致问题。当您需要跟踪加拿大各省或印度各州和领地时会发生什么?你将如何处理不可避免的冲突?即使您认为这可能不是今天的交易,请记住,需求会随着时间而变化。

我将其建模为:

create_table "countries", force: :cascade do |t|
t.string "code", null: false # ISO 3166-1 alpha-2 or alpha-3
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

add_index "countries", ["code"], name: "index_countries_on_code"

create_table "states", force: :cascade do |t|
t.integer "country_id"
t.string "code", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

add_index "states", ["code"], name: "index_states_on_code"
add_index "states", ["country_id", "code"], name: "index_states_on_country_id_and_code"
add_index "states", ["country_id"], name: "index_states_on_country_id"

"and simplifies view logic where I want to call the state code"



如果可以避免,我认为您根本不应该从您的角度进行数据库调用。从您的 Controller 预先查询并将数据传递给您的 View 。它使优化查询和避免 N+1 问题变得更加简单。

使用演示者或辅助方法来帮助管理复杂性。不得不做的轻微不便 State.find_by(code: 'AZ')而不是 State.find('AZ')很可能没有你想象的那么重要。

添加:

这就是在 FactoryGirl 中正确使用关联的方式。考虑这个解决方案的简单性,这是为什么您的自定义外键安排可能会导致更多的痛苦而不是方便的最后一个论点。

楷模:
class State < ActiveRecord::Base
# Only the State model should be validating its attributes.
# You have a major violation of concerns.
validates_uniqueness_of :state_code
validates_length_of :state_code, is: 2
end
# app/models/concerns/belongs_to_state.rb
module BelongsToState

extend ActiveSupport::Concern

included do
belongs_to :state
validates :state, presence: true
validates_associated :state # will not let you save a Foo or Bar if the state is invalid.
end

def state_code
state.state_code
end

def state_code= code
self.assign_attributes(state: State.find_by!(state_code: code))
end
end
class Foo < ActiveRecord::Base
include BelongsToState
end
class Bar < ActiveRecord::Base
include BelongsToState
end

工厂:
# spec/factories/foos.rb
require 'faker'
FactoryGirl.define do
factory :foo do
name { Faker::Company.name }
city { Faker::Address.city }
website { Faker::Internet.url }
state
end
end

# spec/factories/states.rb
FactoryGirl.define do
factory :state do
state_code "AZ"
name "Arizona"
end
end

这些规范使用 shoulda-matchers 对于极其简洁的验证示例:
require 'rails_helper'

RSpec.describe Foo, type: :model do

let(:foo) { build(:foo) }
it { should validate_presence_of :state }

it 'validates the associated state' do
foo.state.state_code = 'XYZ'
foo.valid?
expect(foo.errors).to have_key :state
end

describe '#state_code' do
it 'returns the state code' do
expect(foo.state_code).to eq 'AZ'
end
end

describe '#state_code=' do
let!(:vt) { State.create(state_code: 'VT') }
it 'allows you to set the state with a string' do
foo.state_code = 'VT'
expect(foo.state).to eq vt
end
end
end
# spec/models/state_spec.rb
require 'rails_helper'

RSpec.describe State, type: :model do
it { should validate_length_of(:state_code).is_equal_to(2) }
it { should validate_uniqueness_of(:state_code) }
end

https://github.com/maxcal/sandbox/tree/31773581

另外,在您需要使用的功能、 Controller 或集成规范中 FactoryGirl.create不是 build_stubbed . build_stubbed不会将模型持久化到数据库中,在这些情况下,您需要 Controller 能够从数据库加载记录。

你也应该避免使用 CSS如果可能,请在您的功能规范中使用选择器。功能规范应该从用户的 POV 描述您的应用程序。
feature "Bar management" do
context "as an Admin" do
let!(:bars){ 3.times.map { create(:bar) } }

background do
visit root_path
click_link "Manage bars"
end

scenario "I should see all the bars on the management page" do
# just testing a sampling is usually good enough
expect(page).to have_link bars.first.name
expect(page).to have_link bars.last.name
end

scenario "I should be able to edit a Bar" do
click_link bars.first.name
fill_in('Name', with: 'Moe´s tavern')
# ...
end
end
end

关于ruby-on-rails - 由于模型中的外键,FactoryGirl 炸毁了规范,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31773581/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com