ruby-on-rails – 由于模型中的外键,FactoryGirl炸毁了规格
我有一个模型Foo,它将state_code作为外键. States表是(或多或少)静态表,用于保存50个州的代码和名称,以及其他美国邮政编码(例如波多黎各的“PR”).我选择使用state_code作为状态的主键和Foo上的外键,而不是像state_id那样.它对人类更好,并简化了我想调用状态代码的视图逻辑. (编辑 – 只是为了澄清:我并不是说从视图中调用代码来访问模型;我的意思是将状态显示为@ foo.state_code似乎比@ foo.state.state_code更简单.)
Foo与模特Bar也有很多关系.两个模型规范都传递了有效工厂的规范,但出于某种原因,当运行构建Bar实例的功能规范时,由于与state_code相关的外键问题,测试会爆炸 我得到了所有模型的模型规格,包括有效工厂的测试.但是,每当我尝试为’Bar’创建测试对象时,我都会遇到麻烦.使用构建会炸毁Foo中state_code的外键错误(尽管事实上Foo工厂明确指定了一个确认在状态中作为state_code存在的值).对Bar对象使用build_stubbed似乎不会持久保存该对象. 型号: # 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,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)代替build而不是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 actions中正确显示.但是我似乎无法让它正常工作.功能规范失败: 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不会在视图中显示预期的项目. (我有一个包含开发数据的工作页面,所以我知道逻辑实际上按预期工作). The thoughtbot post on
…但在我的规范中它似乎没有这样做.尝试在此规范中使用build代替build_stubbed会产生上述相同的外键错误. 我真的被困在这里了.模型似乎有有效的工厂并通过所有规格.但是功能规范要么炸毁外键关系,要么似乎不会在视图之间保留build_stubbed对象.感觉就像一团糟,但我找不到正确的方法来解决它.我在实践中有实际的,工作的观点,做我期望的 – 但我希望测试覆盖率有效. UPDATE 我回去并更新了所有的模型代码,以删除state_code的自然键.我遵循了@Max的所有建议. Foo表现在使用state_id作为状态的外键;我按照推荐等方式复制了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对象时才会出现问题.发生这种情况时,由于列上的唯一性约束,第二个对象失败:代码 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,在foos中是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表中某个状态的关系.我不期望对状态表做很多改变 – 它实际上只是一个参考. 我不需要创建一个新状态.我只需要使用states表中的:id列中的66个值之一填充Foo对象上的外键state_id.从概念上讲,工厂: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 我现在唯一可以看到的方法是摆脱状态中的代码列的唯一性约束.或者 – 删除foos和状态之间的外键约束,并让Rails强制执行该关系. 对不起,大量的帖子…… 解决方法
我将成为一个痛苦的人,并认为约定可能比开发人员的便利性和感知可读性更重要.
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 end add_index "states",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"
我认为如果可以避免的话,你根本不应该从你的视图中进行数据库调用.从您的控制器预先查询并将数据传递到您的视图.它使优化查询和避免N 1问题变得更加简单. 使用演示者或帮助程序方法来帮助管理复杂性.不得不做State.find_by(代码:’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 这些规范使用 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 此外,在您的功能,控制器或集成规范中,您需要使用FactoryGirl.create而不是build_stubbed. build_stubbed不会将模型持久保存到数据库中,在这些情况下,您需要控制器能够从数据库加载记录. 此外,如果可能,您应该避免在功能规范中使用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 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |