FactoryGirl因模型中的外键而炸毁规格

Scr*_*cro 5 rspec ruby-on-rails factory-bot

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

Foo还has_many与模特Bar 有关系.两个模型规范都传递了有效工厂的规范,但出于某种原因,在运行构建Bar实例的功能规范时,由于与之相关的外键问题导致测试爆炸state_code

我得到了所有模型的模型规格,包括有效工厂的测试.但是,每当我尝试为'Bar'创建测试对象时,我都会遇到麻烦.在Foo中使用build爆炸外键错误state_code(尽管事实上Foo工厂明确指定了一个确认在状态中作为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
Run Code Online (Sandbox Code Playgroud)

下面的工厂为我的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
Run Code Online (Sandbox Code Playgroud)

......基本规格是:

# 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
Run Code Online (Sandbox Code Playgroud)

该规范通过,没有任何问题.但如果我使用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".
Run Code Online (Sandbox Code Playgroud)

代码'AZ'肯定在状态表中,所以我不清楚它失败的原因.

在一个功能规范中,我试图创建持久存储在数据库中的bar实例,因此我可以测试它们是否在#index,#show和#edit actions中正确显示.但是我似乎无法让它正常工作.功能规范失败:#spec/features/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
Run Code Online (Sandbox Code Playgroud)

此规范失败,并显示一条指示无匹配的消息.使用save_and_open_page不会在视图中显示预期的项目.(我有一个包含开发数据的工作页面,所以我知道逻辑实际上按预期工作).思想机构帖子build_stubbed表明它应该持久化对象:

它使对象看起来像是被持久存在,与build_stubbed策略创建关联(而构建仍然使用create),并且存储了一些与数据库交互的方法,如果你调用它们则会引发.

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

我真的被困在这里了.模型似乎有有效的工厂并通过所有规格.但是功能规格要么炸毁外键关系,要么似乎不会build_stubbed在视图之间保持对象.感觉就像一团糟,但我找不到正确的方法来解决它.我在实践中有实际的,工作的观点,做我期望的 - 但我希望测试覆盖率有效.

UPDATE

我回去并更新了所有的模型代码,以删除自然键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"
Run Code Online (Sandbox Code Playgroud)

模型规格通过,我的一些简单的功能规格通过.我现在意识到只有在创建了多个Foo对象时才会出现问题.发生这种情况时,由于列上的唯一性约束,第二个对象将失败:code

Failure/Error: let!(:foo_2) { create(:foo) }
     ActiveRecord::RecordInvalid:
       Validation failed: Code has already been taken
Run Code Online (Sandbox Code Playgroud)

我试图: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".
Run Code Online (Sandbox Code Playgroud)

显然state_id不是在州,因为它是id在国家,state_id在foos.另一种方法:

# in factory for foo:
state { 1 }    # alternately w/ same error ->  state 1

ActiveRecord::AssociationTypeMismatch:
   State(#70175500844280) expected, got Fixnum(#70175483679340)
Run Code Online (Sandbox Code Playgroud)

要么:

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

ActiveRecord::RecordInvalid:
   Validation failed: State can't be blank
Run Code Online (Sandbox Code Playgroud)

我真正想做的就是创建一个Foo对象的实例,让它包含与states表中某个状态的关系.我预计不会对states表进行很多更改- 它实际上只是一个参考.

不要需要创建一个新的状态.我只需要使用states表中列中state_id66个值之一填充Foo对象上的外键:id.从概念上讲,工厂:foo理想情况下只需选择1到66之间的整数值:state_id.它在控制台中工作:

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
Run Code Online (Sandbox Code Playgroud)

我现在唯一可以看到的方法是摆脱:code列中的唯一性限制states.或者 - 删除foos和之间的外键约束states,并让Rails强制执行该关系.

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

Scr*_*cro 1

这里发生了很多事情,但是关于 Foo 和 State 之间的外键关系爆发的 FactoryGirl 问题,我已经弄清楚了。

@Max 指出了使用自然键作为表上主键的问题states。它不遵循 Rails 约定,并导致一些问题的混合,例如可能必须验证 Foo 表上的外键(例如长度 2)。

但即使在修复该问题以链接 Rails 友好键上的表(:state_id作为外键foos:id主键)之后,我仍然找不到任何方法来使用工厂states创建 Foo 对象的多个实例:foo。当我尝试将整数值“插入”时它要么失败state_id,要么:state工厂在第二个实例上失败,说明代码已经存在。(有关尝试和相关失败的详细信息,请参阅我在问题中的更新)。

唯一的解决办法似乎是删除状态的唯一性验证,或者消除数据库层的外键关系(Postgres 9.4)。我决定不想做前者。在考虑后者时,我意识到我真的不需要数据库中的外键约束。该states表的目的只是提供一致的州代码列表作为参考。如果我出于某种原因删除了该表,那么我并不想销毁所有 Foo 记录。它们本质上是独立的,状态只是 Foo 的一个属性。我曾短暂考虑过将状态信息放入常量中,但是嗯。

删除数据库级外键约束为我解决了问题。

bin/rails g migration RemoveForeignKeyStatesFromFoos

class RemoveForeignKeyStatesFromFoos < ActiveRecord::Migration
  def change
    remove_foreign_key :foos, :states
  end
end
Run Code Online (Sandbox Code Playgroud)

这使:state_id我的表上的列完好无损,但从我的 schema.rb 中foos删除了该行add_foreign_key "foos", "states"

bin/rails g migration AddIndexToStateIdInFoos

class AddIndexToStateIdInFoos < ActiveRecord::Migration
  def change
    add_index :foos, :state_id
  end
end
Run Code Online (Sandbox Code Playgroud)

...将该行添加add_index "foos", ["state_id"], name: "index_foos_on_state_id", using: :btree到我的架构中。

迁移两者后,我最初犯了删除工厂的错误:state,认为我不需要创建新的状态。在测试中遇到一些麻烦之后,我意识到测试数据库通常不会播种rake db:seed- 因此我的测试由于Module::DelegationError. 我没有构建一个脚本来为测试 dB 提供状态种子,而是修改了工厂,并将关联保留在工厂上:foo

# spec/factories/foo_factory.rb
FactoryGirl.define do
  factory :foo do
    # columns omitted
    state
  end

  factory :state do
    code { Faker::Address.state_abbr }
    code { Faker::Address.state }
  end
end
Run Code Online (Sandbox Code Playgroud)

此时,Rails 仍然成功验证了模型中的has_manybelongs_to关系(未更改)。

我知道该add_foreign_key方法对于 Rails 来说相对较新,从 4.2 开始。我将这种关系的事实与在数据库层建立实际外键约束的需要混为一谈,从而放弃了这一点。

来自ActiveRecord 关联的 Rails 指南

您负责维护数据库架构以匹配您的关联。实际上,这意味着两件事,具体取决于您要创建哪种类型的关联。对于belongs_to 关联,您需要创建外键,对于has_and_belongs_to_many 关联,您需要创建适当的连接表。

在这种情况下,术语“外键”的使用对于 Rails 和 Postgres 来说似乎意味着不同的事情。只要表中有一列belongs_to符合惯例, Rails 似乎就非常高兴[parent_table_name]_id。这可以通过显式添加列或references在迁移中使用来实现:

使用 t.integer :supplier_id 使外键命名明显且明确。在当前版本的 Rails 中,您可以使用 t.references :supplier 来抽象出此实现细节

就我而言,这已经足够了——不需要实际的外键。