ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

factory_bot 序列(sequence)完全指南:全局序列与工厂级序列的定义、生成与操作

factory_bot 序列(sequence)完全指南:全局序列与工厂级序列的定义、生成与操作 测试开发工具【免费下载链接】factory_botA library for setting up Ruby objects as test data.项目地址https://gitcode.com/gh_mirrors/fa/factory_bot点击查看免费下载本篇指南聚焦 factory_bot 的两级序列体系——可在多个工厂间共享的全局序列global sequences与仅作用于单个工厂的工厂级序列factory sequences覆盖sequence方法的完整参数起始值、:aliases别名、块、generate生成、序列 URI 寻址、set_sequence/rewind_sequence操作等实战细节。读完本文你将能够为邮箱、ID、用户名等需要唯一值的字段设计可复用、可控制的序列并理解其底层递增机制源码位于 lib/factory_bot/sequence.rb。两级序列体系总览factory_bot 的序列在FactoryBot.define块内定义共分两级作用域全局序列Global sequences直接在FactoryBot.define顶层通过sequence定义可以被任意工厂引用、共享同一个计数器工厂级序列Factory sequences在某个factory块内定义其递增作用域被限定在该工厂定义内部不会产生全局序列。无论是哪一级序列的核心价值都是以固定格式批量生成唯一值典型场景是邮箱地址FactoryBot.define do sequence :email do |n| person#{n}example.com end end generate :email # person1example.com generate :email # person2example.com全局序列Global sequences在Factory.define块中使用sequence方法即可定义可被其他工厂共享的全局序列。sequence 方法的签名与参数sequence方法接收三个部分一个名字、可选参数、一个块。名字必须是 Symbol例如:email。可选参数包括两类均有默认值参数类型默认值说明起始值positional argument任意响应#next的对象1序列从该值开始递增必须支持#next方法:aliasesArray of Symbol[]为序列注册别名别名共享同一个计数器块block接收一个值即当前序列值n作为参数并返回一个结果即该轮生成的实际值FactoryBot.define do sequence(:email) { |n| person#{n}example.com } end序列值全局递增全局序列的递增是全局共享的只要在多个地方使用同一个:email_address序列每次使用都会让值递增一次不会重复FactoryBot.define do sequence(:email_address) { |n| person#{n}example.com } end FactoryBot.create(:user) # 使用 email_address 序列 FactoryBot.create(:author) # 再次使用值继续递增在工厂中引用全局序列在工厂内通过generate引用全局序列并将其赋值给某个属性FactoryBot.define do sequence(:user_factory_email) { |n| person#{n}example.com } factory :user do email { generate(:user_factory_email) } end endgenerate是FactoryBot::Syntax::Methods提供的核心方法见 lib/factory_bot/syntax/methods.rb它会根据序列 URI 查找到对应序列并取出下一个值。工厂级序列Factory sequences序列也可以被限定在factory块内部。语法与全局序列完全一致但递增值的生效范围被限制在该工厂定义内。FactoryBot.define do factory :user do sequence(:email) { |n| person#{n}example.com } end end隐式调用 add_attribute在factory块内使用sequence时factory_bot 会隐式地调用add_attribute把该序列值绑定为工厂的一个属性。也就是说sequence(:email) { ... }等价于先定义序列、再写email { generate(:email) }唯一区别是不会产生任何全局序列。这一点在源码中有直接体现lib/factory_bot/definition_proxy.rb 中DefinitionProxy#sequence的实现创建序列后会调用add_attribute(name) { increment_sequence(registered_sequence) }把序列值作为属性声明注册到工厂定义中。全局序列与工厂序列的等价对照下面两段代码效果相似区别在于第二段不会产生任何全局序列全局序列方式sequence(:user_factory_email) { |n| person#{n}example.com } factory :user do # 引用全局序列 email { generate(:user_factory_email) } end工厂级序列方式factory :user do sequence(:email) { |n| person#{n}example.com } end底层递增机制从源码看每个序列由 lib/factory_bot/sequence.rb 中的FactoryBot::Sequence类实现初始化时通过value args.first || 1解析起始值默认是1若起始值不响应#peek会被包装成EnumeratorAdapter同文件内部类每次调用next(scope)时执行块并在ensure中调用increment_value递增保证无论如何都会递增EnumeratorAdapter#next内部调用value.next这就是“起始值必须响应#next”这一要求的来源。工厂级序列的注册与查找工厂级序列会被注册为“内联序列”inline sequences保存在FactoryBot::Internal.inline_sequences见 lib/factory_bot/internal.rb。DefinitionProxy#sequence会先通过 URI 查找是否已被父工厂注册未注册才重新注册definition_proxy.rb这保证继承场景下序列可被复用。Sequence.find_by_uri会同时搜索全局序列与内联序列sequence.rb。序列别名aliases序列可以注册别名别名与原名共享同一个计数器factory :user do sequence(:email, 1000, aliases: [:sender, :receiver]) { |n| person#{n}example.com } end # 递增 :email 的计数器:sender 与 :receiver 共享该计数器 generate(:sender)不指定起始值、使用默认值1的写法factory :user do sequence(:email, aliases: [:sender, :receiver]) { |n| person#{n}example.com } end指定字符型起始值并配合别名factory :user do sequence(:email, a, aliases: [:sender, :receiver]) { |n| person#{n}example.com } end注意起始值必须支持#next方法这里从a递增为b、c……别名用法详见 docs/src/sequences/aliases.md。自定义起始值任意响应#next方法的值都可以作为起始值例如1、2、3、a、b、cfactory :user do sequence(:email, 1000) { |n| person#{n}example.com } end起始值还可以懒加载传入一个 Proc它会在第一次调用sequence.next时才被求值factory :user do sequence(:email, proc { Person.count 1 }) { |n| person#{n}example.com } end这在数据库记录数动态变化、需要“当前数量 1”作为起点的场景下非常实用详见 docs/src/sequences/initial-value.md。直接生成序列值generate 与 generate_list不必构建对象也可以直接取出序列的下一个值加快测试速度generate(:sequence)生成单个值generate_list(:sequence, count)生成一组连续值组成的数组。FactoryBot.define do sequence(:char, a) { |c| global_character_#{c} } factory :user do sequence(:name, %w[Jane Joe Josh Jayde John].to_enum) trait :with_age do sequence(:age, 21) end end end generate(:char) # global_character_a generate_list(:char, 2) # [global_character_b, global_character_c] generate(:char) # global_character_d generate(:user, :name) # Jane generate_list(:user, :name, 3) # [Joe, Josh, Jayde] generate(:user, :name) # John generate(:user, :with_age, :age) # 21 generate_list(:user, :with_age, :age, 5) # [22, 23, 24, 25, 26] generate(:user, :with_age, :age) # 27注意generate_list(:char, 2)返回的是后续两个连续值b、c而不是从起点重新开始因为计数器是持续递增的。源码实现见 lib/factory_bot/syntax/methods.rbgenerate/generate_list都会先通过Sequence.find_by_uri查找序列找不到则抛出KeyErrorgenerate_list内部用(1..count).map循环取count个值。作用域参数scope如果序列块内引用了其他属性如name直接generate会失败必须通过scope:传入一个“能够响应这些属性”的对象FactoryBot.define do factory :user do sequence(:email) { |n| #{name}-#{n}example.com } end end generate(:user, :email) # ArgumentError, Sequence user:email failed to return a value. Perhaps it needs a scope to operate? (scope: object) jester build(:user, name: Jester) jester.email # Jester-1example.com generate(:user, :email, scope: jester) # Jester-2example.com generate_list(:user, :email, 2, scope: jester) # [Jester-3example.com, Jester-4example.com]测试时 scope 可以是任意响应所引用属性的对象例如OpenStructrequire ostruct FactoryBot.define do factory :user do sequence(:info) { |n| #{name}-#{n}-#{age n} } end end test_scope OpenStruct.new(name: Jester, age: 23) generate_list(user/info, 3, scope: test_scope) # [Jester-1-24, Jester-2-25, Jester-3-26]当序列块引用作用域属性失败时increment_sequence会抛出带提示的ArgumentError见 lib/factory_bot/syntax/methods.rb。作用域完整用法详见 docs/src/sequences/generating.md。序列 URI精确定位任意序列无论是generate、generate_list、FactoryBot.set_sequence还是rewind_sequence都需要通过序列 URI来定位目标序列。每个 URI 由最多三个名字组成位置名字是否必填1工厂名factory name仅当序列定义在 Factory 或 Factory Trait 内时2特质名trait name仅当序列定义在 Trait 内时3序列名sequence name始终必填URI 可以写成多个 Symbol、多个 String或单个资源字符串三种形式generate(:my_factory_name, :my_trait_name, :my_sequence_name) generate(my_factory_name, my_trait_name, my_sequence_name) generate(my_factory_name/my_trait_name/my_sequence_name)完整的 URI 定位示例注释标注了每个序列对应的 URIFactoryBot.define do sequence(:sequence) { |n| global_sequence_#{n} } # generate(:sequence) trait :global_trait do sequence(:sequence) { |n| global_trait_sequence_#{n} } # generate(:global_trait, :sequence) end factory :user do sequence(:sequence) { |n| user_sequence_#{n} } # generate(:user, :sequence) trait :user_trait do sequence(:sequence) { |n| user_trait_sequence_#{n} } # generate(:user, :user_trait, :sequence) end factory :author do sequence(:sequence) { |n| author_sequence_#{n} } # generate(:author, :sequence) trait :author_trait do sequence(:sequence) { |n| author_trait_sequence_#{n} } # generate(:author, :author_trait, :sequence) end end end end多 URI 与别名扩展一个序列可以拥有多个 URI当工厂或特质带有别名时序列会为每个别名或别名组合额外生成一个 URI。下面同一个序列有四种引用方式factory :user, aliases: [:author] do trait :user_trait, aliases: [:author_trait] do sequence(:sequence) { |n| author_trait_sequence_#{n} } end end # generate(:user, :user_trait, :sequence) # generate(:user, :author_trait, :sequence) # generate(:author, :user_trait, :sequence) # generate(:author, :author_trait, :sequence)使用 URI 时有两条重要规则无论嵌套多深URI 中的工厂名永远是定义该序列的那个工厂而不是任何父工厂如果工厂继承了某个序列URI 必须指向定义它的工厂而不是使用它的工厂。完整规则与示例见 docs/src/sequences/sequence-uris.md。设置序列值FactoryBot.set_sequence在测试或控制台中把序列直接设置为某个具体值非常有用。使用FactoryBot.set_sequence并传入序列 URI 与新值即可FactoryBot.define do sequence(:char, a) { |c| global_character_#{c} } factory :user do sequence(:name, %w[Jane Joe Josh Jayde John].to_enum) trait :with_email do sequence(:email) { |n| user_#{n}example.com } end end end # char generate(:char) # global_character_a FactoryBot.set_sequence(:char, z) generate(:char) # global_character_z # user name集合型序列 generate(:user, :name) # Jane FactoryBot.set_sequence(:user, :name, Jayde) generate(:user, :name) # Jayde # user email整数型序列 generate(:user, :with_email, :email) # user_1example.com FactoryBot.set_sequence(:user, :with_email, :email, 1_234_567) generate(:user, :with_email, :email) # user_1234567example.com使用限制新值必须匹配序列类型不能给整数型序列传字符串整数型序列如记录 ID可接受任意正整数固定集合序列可接受集合内的任意值无限序列如字符型sequence(:unlimited, a)若在默认最长搜索时间3 秒内找不到目标值会超时超时时间可通过FactoryBot.sequence_setting_timeout 1.5配置。从源码看set_value有三条路径lib/factory_bot/sequence.rb整数序列直接set_value支持find_index的集合序列按索引定位其余序列则rewind后逐步seek_value查找超时或找不到时抛ArgumentError: Unable to find ... in the sequence。详见 docs/src/sequences/setting-the-value.md。倒带序列rewind_sequences 与 rewind_sequence倒带全部序列FactoryBot.rewind_sequences会同时倒带所有全局序列与工厂级序列FactoryBot.define do sequence(:email) { |n| person#{n}example.com } factory :user do sequence(:email) { |n| user#{n}example.com } end end generate(:email) # person1example.com generate(:email) # person2example.com generate(:email) # person3example.com generate(:user, :email) # user1example.com generate(:user, :email) # user2example.com generate(:user, :email) # user3example.com FactoryBot.rewind_sequences generate(:email) # person1example.com generate(:user, :email) # user1example.com倒带单个序列向FactoryBot.rewind_sequence传入序列 URI 即可单独倒带FactoryBot.define do sequence(:email) { |n| global_email_#{n}example.com } factory :user do sequence(:email) { |n| user_email_#{n}example.com } end end FactoryBot.rewind_sequence(:email) generate(:email) # global_email_1example.com FactoryBot.rewind_sequence(:user, :email) generate(:user, :email) # user_email_1example.com内部实现上rewind_sequences会遍历全局序列与内联序列并逐个rewindlib/factory_bot/internal.rbrewind_sequence则先按 URI 查找再rewindinternal.rb。详见 docs/src/sequences/rewinding.md。序列作为隐式属性序列也可以被“隐式”引用在工厂里直接写属性名而不带块等价于email { generate(:email) }FactoryBot.define do sequence :email do |n| person#{n}example.com end end factory :user do email # 等价于 email { generate(:email) } end但注意如果存在与序列同名的工厂这种隐式写法不会生效。从源码看DefinitionProxy#method_missing在无参无块时会声明一个Declaration::Implicit其解析顺序是关联 → 序列 → 特质lib/factory_bot/definition_proxy.rb。详见 docs/src/sequences/as-implicit-attributes.md。与唯一性约束配合的注意事项使用序列处理唯一性约束时小心不要传入会与生成值冲突的覆盖值。例如下面代码中两个用户的 email 相同若 email 字段有唯一性约束就会报错factory :user do sequence(:email) { |n| person#{n}example.com } end FactoryBot.create(:user, email: person1example.com) FactoryBot.create(:user)第二个create生成的序列值恰为person1example.com与第一个用户的覆盖值撞车详见 docs/src/sequences/uniqueness.md。补充相关引用与简写全局序列的定位与工厂级序列的 URI 规则可对照 docs/src/sequences/global-sequences.md 与 docs/src/sequences/sequence-uris.md若不想显式调用generate参见 method_missing 中关于隐式属性解析的说明序列的单元测试覆盖了别名、倒带、取值、URI 寻址等场景可在 spec/factory_bot/sequence_spec.rb 与 spec/factory_bot/uri_manager_spec.rb 中查看验证用例FactoryBot.define块的入口与作用域细节见 docs/src/ref/define.md 与 lib/factory_bot.rb。要点回顾全局序列跨工厂共享计数器工厂级序列作用域受限且隐式注册属性起始值默认1且必须支持#next可传 Proc 懒加载通过序列 URI工厂名/特质名/序列名三段配合generate、generate_list、FactoryBot.set_sequence、rewind_sequence可以精确控制任意序列的取值与状态。赞分享测试开发工具【免费下载链接】factory_botA library for setting up Ruby objects as test data.项目地址https://gitcode.com/gh_mirrors/fa/factory_bot点击查看免费下载相关推荐Kubespray 如何使用 kube-vip 为控制面构建虚拟 IP 高可用负载端点Kubespray 如何使用 kube vip 为控制面构建虚拟 IP 高可用负载端点 如果你的 Kubespray 集群有多个控制面节点但又不想引入外部负测试开发工具factory_bot 序列隐式属性As Implicit Attributes在工厂中直接引用全局序列的快捷写法factory_bot 序列隐式属性As Implicit Attributes在工厂中直接引用全局序列的快捷写法 在 factory_bot 中除了用测试开发工具Newtonsoft.Json 序列化与反序列化完全指南Newtonsoft.Json 序列化与反序列化完全指南 引言 在现代.NET开发中JSON数据格式已成为应用程序间数据交换的事实标准。Newtonsoft.序列化后端上一篇3分钟上手全网资源下载神器res-downloader使用指南下一篇解锁Windows隐藏功能ViVeTool GUI让系统控制变得如此简单创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表