![最终产品图片](https://i-blog.csdnimg.cn/blog_migrate/ebe9ba55b5ed6866c4c4c005c3d8b8c7.png)
关于该流行和有用的Ruby gem的第二篇文章讨论了一些更细微的话题,初学者在开始时就不必马上关注自己。 再次,我竭尽全力使它可以被新手访问,并解释了测试驱动开发(TDD)的每一个行话专家可能会遇到的麻烦。
主题
- 相关属性
- 瞬态属性
- 惰性属性
- 改造工厂
- 回呼
- 社团协会
- 别名
- 特质
相依属性
如果您需要使用属性值动态地组合其他工厂属性,则可以使用Factory Girl。 您只需要将属性值包装在一个块中并插入所需的属性即可。 这些块可以访问评估器 (由评估器提供) ,而后者又可以访问其他属性,甚至是瞬时属性。
FactoryGirl.define do
factory :supervillain do
name 'Karl Stromberg'
passion 'marine biology'
ambition 'human extinction'
motivation 'save the oceans'
profile { "#{name} has a passion for #{passion} and aims to #{motivation} through #{ambition}."}
end
end
villain = create(:supervillain)
villain.profile
# => "Karl Stromberg has a passion for marine biology and aims to save the oceans through human extinction."
瞬态属性
我认为称它们为假属性是公平的。 这些虚拟属性还允许您在构造工厂实例时传递附加选项-当然是通过哈希。 实例本身不会受到它们的影响,因为不会在工厂对象上设置这些属性。 另一方面,Factory Girl将瞬态属性视为真实属性。
如果使用attributes_for
,它们将不会显示。 从属属性和回调能够访问工厂内部的这些伪造属性。 总体而言,它们是使工厂保持干燥的另一种策略。
FactoryGirl.define do
factory :supervillain do
transient do
megalomaniac false
cat_owner false
end
name 'Karl Stromberg'
passion 'marine biology'
ambition 'human extinction'
motivation { "Building an underwater civilization#{" and saving the world" if megalomaniac}" }
profile { "Insane business tycoon#{" – friends with Blofeld" if cat_owner}" }
end
end
villain = create(:supervillain)
villain.profile
# => "Insane business tycoon"
villain.motivation
# => "Building an underwater civilization"
cat_friendly_villain = create(:supervillain, cat_owner: true)
cat_friendly_villain.profile
# => "Insane business tycoon – friends with Blofeld"
narcissistic_villain = create(:supervillain, megalomaniac: true)
narcissistic_villain.motivation
# => "Building an underwater civilization and saving the world"
上面的示例变得更加干燥,因为无需为想要拯救世界或分别与布洛费尔德成为朋友的超级反派建立单独的工厂。 瞬态属性使您可以灵活地进行各种调整,并避免创建许多如此相似的工厂。
惰性属性
定义工厂时,将评估“工厂女孩”中的“普通”属性。 通常,您为与属性同名的方法提供静态值作为参数。 如果要将评估延迟到实例化实例的最后一个可能时刻,则需要通过代码块来提供属性的值。 从诸如DateTime
对象之类的对象创建的关联和动态创建的值将是您最懒惰的客户。
FactoryGirl.define do
factory :exploding_device do
transient do
countdown_seconds 10*60
time_of_explosion { Time.now + countdown_seconds }
end
time_of_explosion { "Exploding in #{countdown_seconds} seconds #{time_of_explosion.strftime("at %I:%M %p")}" }
end
end
ticking_device = create(:exploding_device)
ticking_device.time_of_explosion
# => "Exploding in 600 seconds at 11:53 PM"
修改工厂
这可能不是每天都会遇到的用例,但有时您会从其他开发人员那里继承工厂,并且想要更改它们(例如,如果您使用的是TDD宝石)。 如果您需要调整这些旧式工厂以更好地适合您的特定测试方案,则可以在不创建新工厂或使用继承的情况下对其进行修改。
您可以通过FactoryGirl.modify
执行此FactoryGirl.modify
,它必须位于要更改的特定FactoryGirl.define
块之外。 您不能做的是修改sequence
或trait
-但是您可以覆盖通过trait
定义的属性。 “原始”工厂的回调也不会被覆盖。 Factory.modify
块中的回调将仅在下一行中运行。
FactoryGirl.define do
factory :spy do
name 'Marty McSpy'
skills 'Espionage and infiltration'
deployment_status 'Preparing mission'
end
end
FactoryGirl.modify do
sequence :mission_deployment do |number|
"Mission #{number} at #{DateTime.now.to_formatted_s(:short)}"
end
factory :spy do
name 'James Bond'
skills 'CQC and poker'
favorite_weapon 'Walther PPK'
body_count 'Classified'
favorite_car 'Aston Martin DB9'
deployment { generate(:mission_deployment) }
end
end
在上面的示例中,我们需要使间谍更加“复杂”,并使用更好的机制来处理部署。 我看到了一些示例,这些示例中gem的作者不得不以不同的方式处理时间,并且通过简单地覆盖需要调整的内容来方便地修改工厂对象。
回呼
回调使您可以在对象生命周期的不同时刻注入一些代码,例如save
, after_save
, before_validation
等。 例如,Rails提供了很多功能,使新手很容易滥用此功能。
请记住,与对象持久性无关的回调是一种已知的反模式,因此最好不要越过这一线。 例如,在实例化诸如用户发送电子邮件或处理某些订单之类的东西之后,使用回调似乎很方便,但是这类事情会引起bug并创建不必要地难以重构的联系。 也许这就是为什么“只有女孩”为您提供五个回调选项供您使用的原因之一:
-
before(:create)
保存工厂实例before(:create)
执行一个代码块。 使用create(:some_object)
时激活。 - 保存工厂实例
after(:create)
执行代码块。 使用create(:some_object)
时激活。 -
after(:build)
工厂对象已建立在内存中after(:build)
执行代码块。 在同时使用build(:some_object)
和create(:some_object)
时激活。 - 工厂创建存根对象
after(:stub)
执行代码块。 使用build_stubbed(:some_object)
时激活。 -
custom(:your_custom_callback)
执行自定义回调,而无需在before
或after
添加前缀。
FactoryGirl.define do
factory :mission do
objective 'Stopping the bad dude'
provided_gadgets 'Mini submarine and shark gun'
after(:build) { assign_support_analyst }
end
end
注意!
请注意,对于所有回调选项,在回调块内,您都可以通过block参数访问工厂实例。 每隔一段时间,这将派上用场,尤其是在关联方面。
FactoryGirl.define do
factory :double_agent do
after(:stub) { |double_agent| assign_new_identity(double_agent) }
end
end
在下方,忍者拥有一堆令人讨厌的投掷星星(手里剑)。 由于回调中有ninja
对象,因此您可以轻松地将投掷星号指定为属于忍者。 如果该示例给您留下了几个问号,请看一下有关关联的部分。
FactoryGirl.define do
factory :ninja do
name "Ra’s al Ghul"
factory :ninja_with_shuriken do
transient do
number_of_shuriken 10
end
after(:create) do |ninja, evaluator|
create_list(:shuriken, evaluator.number_of_shuriken, ninja: ninja)
end
end
end
factory :shuriken do
name 'Hira-shuriken'
number_of_spikes 'Four'
ninja
end
end
ninja = create(:ninja)
ninja.shurikens.length # => 0
ninja = create(:ninja_with_shuriken)
ninja.shurikens.length # => 10
ninja = create(:ninja_with_shuriken, number_of_shuriken: 20)
ninja.shurikens.length # => 20
另外,通过评估程序对象,您也可以访问临时属性。 这使您可以选择在“创建”工厂对象时需要传递其他信息,并且需要即时对其进行调整。 这为您提供了玩关联和编写表达性测试数据所需的所有灵活性。
如果发现工厂需要多个回调,则Factory Girl不会妨碍您-甚至是多个类型的回调。 当然,执行顺序是从上到下。
FactoryGirl.define do
factory :henchman do
name 'Mr. Hinx'
after(:create) { |henchman| henchman.send_on_kill_mission }
after(:create) { send_cleaner }
end
end
FactoryGirl.define do
factory :bond_girl do
name 'Lucia Sciarra'
after(:build) { |bond_girl| bond_girl.hide_secret_documents }
after(:create) { close_hidden_safe_compartment }
end
end
当然,细节在于魔鬼。 如果使用create(:some_object)
, create(:some_object)
同时执行after(:build)
和after(:create)
回调。
可以捆绑多个构建策略来执行相同的回调。
FactoryGirl.define do
factory :spy do
name 'Marty McFly'
after(:stub, :build) { |spy| spy.assign_new_mission }
end
end
最后但并非最不重要的一点是,您甚至可以设置“全局”回调之类的内容来覆盖所有工厂的回调,至少在该特定文件中(如果您已将它们分成多个工厂文件)。
factory / gun.rb
FactoryGirl.define do
before(:stub, :build, :create) { |object| object.assign_serial_number }
factory :spy_gun do
name 'Walther PPK'
ammunition '7.65mm Browning'
association :owner
factory :golden_gun do
name 'Custom Lazar'
ammunition '24-carat gold bullet'
after(:create) { |golden_gun| golden_gun.erase_serial_number }
end
end
end
注意!
如果使用继承来构成子工厂,则父级上的回调也将被继承。
最后一英里
在以下有关关联和特征的部分中 ,我们将它们放在一起-是的,我也潜入了别名,因为它是最好的地方,而不会四处走动。 如果您已经关注并记住了第一篇文章中的内容,那么现在所有内容都应该整齐地放入。
社团协会
关联对于每个具有一点点复杂性的自重Web应用程序都是必不可少的。 属于用户的帖子,具有很多评分的列表等等都是面包开发人员在一周中的任何一天吃早餐的地方。 从这个角度来看,很明显,对于更复杂的场景,工厂需要防弹并易于处理-至少是为了不弄乱您的TDD mojo。
我想说,通过Factory Girl模拟模型关联相对简单。 在我看来,这本身就是相当了不起的。 实现构建复杂数据集的高度便捷性使TDD的实践变得轻而易举,并且效率更高。
新版Q具有黑客技能,需要拥有一台像样的计算机,对吗? 在这种情况下,您有一个Computer
类,并且其实例属于Quartermaster
类的实例。 容易吧?
FactoryGirl.define do
factory :quartermaster do
name 'Q'
skills 'Inventing stuff'
end
factory :computer do
model 'Custom Lenovo ThinkPad W Series'
quartermaster
end
end
那又有什么涉及呢? 假设我们的间谍使用的gun
有多个cartridges
(子弹)。
class Cartridge < ActiveRecord::Base
belongs_to :gun
end
class Gun < ActiveRecord::Base
has_many :cartridges
end
FactoryGirl.define do
factory :cartridge do
caliber '7.65'
gun
end
factory :gun do
name 'Walther PPK'
ammunition '7.65mm Browning'
caliber '7.65'
factory :gun_with_ammo do
transient do
magazine_size 10
end
after(:create) do |gun, evaluator|
create_list(:cartridge, evaluator.magazine_size, gun: gun)
end
end
end
end
回调与关联非常方便,是吗? 现在,您可以制造带有或不带有弹药的枪支。 通过哈希gun: gun
您提供了必要的信息,以便cartridge
工厂通过foreign_key
创建关联。
spy_gun = create(:gun)
spy_gun.cartridges.length # => 0
spy_gun_with_ammo = create(:gun_with_ammo)
spy_gun_with_ammo.cartridges.length # => 10
如果您需要其他杂志尺寸,则可以通过您的瞬时属性将其传递。
big_magazine_gun = create(:gun_with_ammo, magazine_size: 20)
big_magazine_gun.cartridges.length # => 20
那么不同的构建策略呢? 那里没有鱼吗? 好了,这是您需要记住的内容:如果对关联对象使用create
,则将同时保存它们。 因此, create(:quartermaster)
将构建并保存Q和他的ThinkPad。
我最好使用build
,然后,如果我想避免访问数据库,对吗? 好主意,但是在我们的示例中, build
仅适用于quartermaster
-关联的computer
仍将得到保存。 我知道有些棘手。 如果需要避免保存关联的对象,可以执行以下操作-为关联指定所需的构建策略。
FactoryGirl.define do
factory :quartermaster do
name 'Q'
skills 'Inventing stuff'
end
factory :computer do
model 'Custom Lenovo ThinkPad W Series'
association :quartermaster, strategy: :build
end
end
您可以命名关联的工厂对象,并使用构建策略传递哈希值。 您需要使用显式关联调用才能起作用。 下面的示例不起作用。
factory :computer do
model 'Custom Lenovo ThinkPad W Series'
quartermaster, strategy: :build
end
现在,两个对象都使用build
而没有任何内容保存到数据库。 我们可以使用new_record?
检查该假设new_record?
,如果实例未持久保存,则返回true
。
thinkpad = build(:computer)
thinkpad.new_record? # => true
thinkpad.quartermaster.new_record? # => true
在进行此操作时,通过显式关联调用,您还可以引用不同的工厂名称并即时更改属性。
FactoryGirl.define do
factory :quartermaster do
name 'Q'
end
factory :computer do
model 'Custom Lenovo ThinkPad W Series'
association :hacker, factory: :quartermaster, skills: 'Hacking'
end
end
让我们以多态的示例结束本章。
class Spy < ActiveRecord::Base
belongs_to :spyable, polymorpic: true
end
class MIFive < ActiveRecord::Base
has_many :spies, as: :spyable
end
class MISix < ActiveRecord::Base
has_many :spies, as: :spyable
end
FactoryGirl.define do
factory :mifive do
name 'Military Intelligence, Section 5'
principal_activity 'Domestic counter-intelligence'
end
factory :misix do
name 'Military Intelligence, Section 6'
principal_activity 'Foreign counter-intelligence'
end
factory :mifive_spy, class: Spy do
name '005'
association :spyable, factory: :mifive
end
factory :misix_spy, class: Spy do
name '006'
association :spyable, factory: :misix
end
end
# MI5 agents
mifive = create(:mifive)
mifive_spy = create(:mifive_spy)
mifive.spies << mifive_spy
mifive.name # => "Military Intelligence, Section 5"
mifive_spy.name # => '005'
mifive.spies.length # => 1
mifive.spies.first.name # => '005'
# MI6 agents
misix = create(:misix)
misix_spy_01 = create(:misix_spy, name: '007')
misix_spy_02 = create(:misix_spy)
misix.spies << misix_spy_01
misix.spies << misix_spy_02
misix.name # => "Military Intelligence, Section 6"
misix.spies.length # => 2
misix_spy_01.name # => '007'
misix_spy_02.name # => '006'
misix.spies.first.name # => '007'
如果这需要更多的时间,不要感到难过。如果您不确定这里发生了什么,我建议赶上多态关联 。
别名
工厂的别名使您可以更充分地表达使用工厂对象所处的上下文。您只需要提供替代名称的哈希即可更好地描述关联对象之间的关系。
假设您有一个:agent
工厂和一个:law_enforcement_vehicle
工厂。 在这些汽车的上下文中将代理人称为:owner
会很好吗? 在下面的示例中,我将其与没有别名的示例进行了对比。
FactoryGirl.define do
factory :agent, aliases: [:owner] do
name 'Fox Mulder'
job 'Chasing bad dudes'
special_skills 'Investigation and intelligence'
factory :double_O_seven do
name 'James Bond'
end
end
factory :law_enforcement_vehicle do
name 'Oldsmobile Achieva'
kind 'Compact car'
:owner
end
factory :spy_car do
name 'Aston Martin DB9'
kind 'Sports car'
double_O_seven
end
end
注意!
在工厂中将它们用于关联时,请不要忘记在别名工厂( :owner
)前面添加冒号。 在这种情况下,文档和许多博客文章都使用冒号而不加冒号。 您得到的可能只是一个NoMethodError
因为您现在缺少该别名的setter方法。 (我最好打开一个请求请求。)第一次遇到这个问题时,我感到困惑,花了我一些时间才能通过。 请记住,有时会选择性地不信任文档和博客文章。 当然,您也是如此。
我想您会同意,使用别名不仅可以更好地阅读,而且可以为您或在您之后的人提供有关所讨论对象的更多上下文。 是的,如果您只有一个:aliases
也需要使用复数:aliases
。
您也可以写出一些不同的方式-更详细些。
factory :agent, aliases: [:mulder] do
name 'Fox Mulder'
job 'Chasing bad dudes'
special_skills 'Investigation and intelligence'
end
factory :law_enforcement_vehicle do
name 'Oldsmobile Achieva'
kind 'Compact car'
association :owner, factory: :agent
end
好吧,不是那么整洁吧?
当然,您也可以使用这些别名立即“构建”工厂对象。
fbi_agent = create(:mulder)
fbi_agent.name # => 'Fox Mulder'
在注释的上下文中, :user
可以称为:commenter
,在:crime
的情况下, :user
可以别名为:suspect
,依此类推。 并不是真正的火箭科学,更像是方便的语法糖,它可以减少重复的诱惑。
特质
这是我对《工厂女郎》最喜欢的事情之一。 简而言之,特质就像是乐高玩具般的砌块,可以用来建造工厂并融入行为。 它们是您要添加到特定工厂的符号特征/属性的逗号分隔列表,并且它们也在工厂的文件中定义。
在我看来, trait
是保持工厂数据DRY的同时保持表现力的最强大,最方便的功能。 它使您可以将属性组捆绑在一起,为它们分配单独的名称,然后随时随地重复使用它们。 还记得我敦促您定义准工厂对象吗? 特质将帮助您在不牺牲任何便利的情况下准确实现这一目标。
FactoryGirl.define do
factory :spy_car do
model 'Aston Martin DB9'
top_speed '295 km/h'
build_date '2015'
ejection_seat true
trait :submarine do
ejection_seat false
water_resistant '100 m'
submarine_capabilities true
air_independent_propulsion true
end
trait :weaponized do
rockets true
number_of_rockets '12'
machine_gun true
rate_of_fire '1,500 RPM'
tank_armour true
end
trait :cloaked do
active_camouflage true
radar_signature 'reduced'
engine 'silenced'
end
trait :night_vision do
infrared_sensors true
heads_up_display true
end
end
end
如您所见,如果要更改分布在多个对象上的某些属性,则可以在一个中央位置进行。 无需shot弹枪手术。 通过特征管理状态再方便不过了。
通过这种设置,您可以通过混合各种属性捆绑包来构建精美的间谍车,而无需通过创建各种新工厂来复制任何东西,这些工厂可以满足您所需的所有选项。
invisible_spy_car = create(:spy_car, :cloaked, :night_vision)
diving_spy_car = create(:spy_car, :submarine, :cloaked)
tank_spy_car = create(:spy_car, :weaponized, :night_vision)
您可以将traits与create
, build
, build_stubbed
和attributes_for
。 如果Q变得更聪明,您还可以通过传入散列来同时覆盖各个属性。
build(:spy_car, :submarine, ejection_seat: true)
对于在特定工厂频繁发生的特征组合,您还可以创建子工厂,其名称最能代表各种数据集组合。 这样一来,与创建测试数据时的所有时间相反,您只需捆绑一次它们的特征。
FactoryGir.define do
factory :spy_car do
model 'Aston Martin DB9'
top_speed '295 km/h'
build_date '2015'
ejection_seat true
trait :submarine do
...
end
trait :weaponized do
...
end
trait :cloaked do
...
end
trait :night_vision do
...
end
end
factory :invisible_spy_car, traits: [:cloaked, :night_vision]
factory :diving_spy_car, traits: [:submarine, :cloaked]
factory :tank_spy_car, traits: [:weaponized, :night_vision]
factory :ultimate_spy_car, traits: [:cloaked, :night_vision, :submarine, :weaponized]
end
这使您可以更简洁地创建这些对象,并且也更具可读性。
build_stubbed(:invisible_spy_car)
create(:ultimate_spy_car)
代替:
build_stubbed(:spy_car, :cloaked, :night_vision)
create(:spy_car, :cloaked, :night_vision, :submarine, :weaponized)
阅读好多了,不是吗? 特别是在不涉及变量名的情况下。
您甚至可以将特征重新用作其他特征和工厂的属性。 当然,如果为多个特征定义相同的属性,则最后定义的优先。
FactoryGirl.define do
factory :spy_car do
model 'Aston Martin DB9'
top_speed '295 km/h'
build_date '2015'
ejection_seat true
trait :submarine do
...
end
trait :weaponized do
...
end
trait :cloaked do
...
end
trait :night_vision do
...
end
trait :mobile_surveillance do
cloaked
night_vision
signal_detector true
signal_analyzer true
wifi_war_driver true
license_plate_reader true
mini_drone true
end
end
factory :ultimate_spy_car, parent: :spy_car do
car_plane true
submarine
weaponized
mobile_surveillance
end
end
讲究mobile_surveillance
特质,这重用了cloaked
和night_vision
特征,基本上为属性。 另外, ultimate_spy_car
工厂,我分离出的的spy_car
厂家定义为乐趣此时,重用所有特质加上一个额外的属性,使得它飞得。 纯电影魔术,或者也许我应该说是《工厂女郎》魔术。
create_list
和build_list
也可以使用特征。 第二个参数必须是所需的工厂实例数。
create_list(:spy_car, 3, :night_vision)
build_list(:spy_car, 4, :submarine, :cloaked)
使用具有特征的关联会很酷吗? 当然,您可以将回调和关联整齐地打包到特征中。 h!
FactoryGirl.define do
factory :cartridge do
kind 'Small calliber pistol ammunition'
caliber '7.65'
projectile 'Lead'
gun
factory :golden_cartridge do
projectile 'Gold'
association :gun, :golden
end
end
factory :gun do
name 'Walther PPK'
ammunition '7.65mm Browning'
caliber '7.65'
transient do
magazine_size 10
end
trait :golden do
name 'Custom Lazar'
ammunition '23-carat gold bullet'
end
trait :with_ammo do
after(:create) do |gun, evaluator|
create_list(:cartridge, evaluator.magazine_size, gun: gun)
end
end
trait :with_golden_ammo do
after(:create) do |golden_gun, evaluator|
create_list(:golden_cartridge, evaluator.magazine_size, gun: golden_gun)
end
end
end
end
现在如何使用它们应该很无聊。
cartridge = create(:cartridge)
cartridge.projectile # => 'Lead'
cartridge.gun.name # => 'Walther PPK'
cartridge.gun.ammunition # => '7.65mm Browning'
cartridge.gun.caliber # => '7.65'
golden_cartridge = create(:golden_cartridge)
golden_cartridge.projectile # => 'Gold'
golden_cartridge.gun.name # => 'Custom Lazar'
golden_cartridge.gun.ammunition # => '23-carat gold bullet'
golden_cartridge.gun.caliber # => '7.65'
gun_with_ammo = create(:gun, :with_ammo)
gun_with_ammo.name # => 'Walther PPK'
gun_with_ammo.ammunition # => '7.65mm Browning'
gun_with_ammo.cartridges.length # => 10
gun_with_ammo.cartridges.first.projectile # => 'Lead'
gun_with_ammo.cartridges.first.caliber # => '7.65'
golden_gun_with_golden_ammo = create(:gun, :golden, :with_golden_ammo)
golden_gun_with_golden_ammo.name # => 'Custom Lazar'
golden_gun_with_golden_ammo.ammunition # => '24-carat gold bullet'
golden_gun_with_golden_ammo.cartridges.length # => 10
golden_gun_with_golden_ammo.cartridges.first.projectile # => 'Gold'
golden_gun_with_golden_ammo.cartridges.first.caliber # => '7.65'
最后的想法
智慧的最后一句话:更改是您始终不变的伴侣-更改属性或数据类型的需求始终存在。 像这样的设计决策会不断演变。 特质将减轻痛苦并帮助您管理数据集。
想象一下,如果您使用选项哈希进行实例化,并且该要求已完全更改。 您的测试中有多少个潜在位置可能会损坏,现在需要注意? 直截了当, trait
是消除测试套件中重复项的非常有效的工具。 但是,借助所有这些方便,不要懒惰,并且忘记在由特征表示的列上进行单元测试! 这样,您就可以为它们提供与有效对象所需的准系统属性相同的照顾。
在Factory Girl中还有更多发现,我相信您现在有足够的能力在需要时将它们组合在一起。 玩这个宝石玩得开心。 我希望您的TDD习惯将从中受益。
翻译自: https://code.tutsplus.com/articles/factory-girl-201--cms-25171