Skip to content

Commit 0abe26c

Browse files
Factory Sequence: bug-fix + new methods for setting, generating and rewinding (#1742)
* Sequences refactored for generating, rewinding, setting & bugfix. :generate and :generate_list expanded to work with factory sequences: - generate(:sequence_name) - generate(:factory_name, :sequence_name) - generate_list(:sequence_name, 3) - generate_list(:factory_name, :sequence_name, 3) :rewind_seqence added to rewind individual sequences - rewind_sequence(:factory_name, :trait_name, :sequence_name) :set_sequence added to set the sequence to a new value: - set_sequence(:sequence_name, new_value) - set_sequence(:factory_name, :sequence_name, new_value) Test coverage at 100% & docs updated. * Conflict with Truffleruby and RSpec When calling :peek on the Enumerator class, internally it calls :next. There was an issue when stubbing the :next method to raise an exception, then triggering it when calling :peek. This was only an issue in Truffleruby. Co-authored-by: Neil Carvalho <me@neil.pro>
1 parent e35980d commit 0abe26c

24 files changed

+1314
-234
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ coverage
99
tmp
1010
bin
1111
.rubocop-https*
12+
.zed
1213

1314
gemfiles/*.lock

docs/src/SUMMARY.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,14 @@
6161
- [Global sequences](sequences/global-sequences.md)
6262
- [With dynamic attributes](sequences/with-dynamic-attributes.md)
6363
- [As implicit attributes](sequences/as-implicit-attributes.md)
64-
- [Inline sequences](sequences/inline-sequences.md)
64+
- [Factory sequences](sequences/factory-sequences.md)
6565
- [Initial value](sequences/initial-value.md)
6666
- [Without a block](sequences/without-a-block.md)
6767
- [Aliases](sequences/aliases.md)
68+
- [Sequence URIs](sequences/sequence-uris.md)
6869
- [Rewinding](sequences/rewinding.md)
70+
- [Setting the value](sequences/setting-the-value.md)
71+
- [Generating a sequence](sequences/generating.md)
6972
- [Uniqueness](sequences/uniqueness.md)
7073
- [Traits](traits/summary.md)
7174
- [As implicit attributes](traits/as-implicit-attributes.md)

docs/src/sequences/inline-sequences.md renamed to docs/src/sequences/factory-sequences.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Inline sequences
1+
# Factory sequences
22

3-
And it's also possible to define an in-line sequence that is only used in
3+
And it's also possible to define a sequence that is only used in
44
a particular factory:
55

66
```ruby

docs/src/sequences/generating.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Generating a Sequence
2+
3+
Being able to diectly generate a sequence, without having to build the object can really speed up testing. This can be achieved by passing the [sequence URI](sequence-uris.md) to `:generate` for a single value or `:generate_list` for an Array of sequential values.
4+
5+
```ruby
6+
FactoryBot.define do
7+
sequence(:char, 'a') {|c| "global_character_#{c}" }
8+
9+
factory :user do
10+
sequence(:name, %w[Jane Joe Josh Jayde John].to_enum)
11+
12+
trait :with_age do
13+
sequence(:age, 21)
14+
end
15+
end
16+
end
17+
18+
##
19+
# char
20+
generate(:char) # "global_character_a"
21+
generate_list(:char, 2) # ["global_character_b", "global_character_c"]
22+
generate(:char) # "global_character_d"
23+
24+
##
25+
# user name
26+
generate(:user, :name) # "Jane"
27+
generate_list(:user, :name, 3) # ['Joe', 'Josh', 'Jayde']
28+
generate(:user, :name) # "John"
29+
30+
##
31+
# user age
32+
generate(:user, :with_age, :age) # 21
33+
generate_list(:user, :with_age, :age, 5) # [22, 23, 24, 25, 26]
34+
generate(:user, :with_age, :age) # 27
35+
```
36+
37+
## Scope
38+
39+
On occasion a sequence block may refer to a scoped attribute. In this case, the scope must be provided, or else an exception will be raised:
40+
41+
```ruby
42+
FactoryBot.define do
43+
factory :user do
44+
sequence(:email) { |n| "#{name}-#{n}@example.com" }
45+
end
46+
end
47+
48+
generate(:user, :email)
49+
# ArgumentError, Sequence user:email failed to return a value. Perhaps it needs a scope to operate? (scope: <object>)
50+
51+
jester = build(:user, name: "Jester")
52+
jester.email # "Jester-1@example.com"
53+
54+
generate(:user, :email, scope: jester)
55+
# "Jester-2@example.com"
56+
57+
generate_list(:user, :email, 2, scope: jester)
58+
# ["Jester-3@example.com", "Jester-4@example.com"]
59+
```
60+
61+
When testing, the scope can be any object that responds to the referrenced attributes:
62+
63+
```ruby
64+
require 'ostruct'
65+
66+
FactoryBot.define
67+
factory :user do
68+
sequence(:info) { |n| "#{name}-#{n}-#{age + n}" }
69+
end
70+
end
71+
72+
test_scope = OpenStruct.new(name: "Jester", age: 23)
73+
74+
generate_list('user/info', 3, scope: test_scope)
75+
# ["Jester-1-24", "Jester-2-25", "Jester-3-26"]
76+
```

docs/src/sequences/rewinding.md

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,52 @@
11
# Rewinding
22

3-
Sequences can also be rewound with `FactoryBot.rewind_sequences`:
3+
Sequences can also be rewound to their starting value:
4+
5+
## All sequences
6+
7+
Rewind all global and factory sequences with `FactoryBot.rewind_sequences`:
48

59
```ruby
6-
sequence(:email) {|n| "person#{n}@example.com" }
10+
FactoryBot.define do
11+
sequence(:email) {|n| "person#{n}@example.com" }
12+
13+
factory :user do
14+
sequence(:email) {|n| "user#{n}@example.com" }
15+
end
16+
end
717

818
generate(:email) # "person1@example.com"
919
generate(:email) # "person2@example.com"
1020
generate(:email) # "person3@example.com"
1121

22+
generate(:user, :email) # "user1@example.com"
23+
generate(:user, :email) # "user2@example.com"
24+
generate(:user, :email) # "user3@example.com"
25+
1226
FactoryBot.rewind_sequences
1327

14-
generate(:email) # "person1@example.com"
28+
generate(:email) # "person1@example.com"
29+
generate(:user, :email) # "user1@example.com"
1530
```
1631

17-
This rewinds all registered sequences.
32+
## Individual sequences
33+
34+
An individual sequence can be rewound by passing the [sequence URI](sequence-uris.md) to `FactoryBot.rewind_sequence`:
35+
36+
```ruby
37+
FactoryBot.define do
38+
sequence(:email) {|n| "global_email_#{n}@example.com" }
39+
40+
factory :user do
41+
sequence(:email) {|n| "user_email_#{n}@example.com" }
42+
end
43+
end
44+
45+
FactoryBot.rewind_sequence(:email)
46+
generate(:email)
47+
#=> "global_email_1@example.com"
48+
49+
factoryBot.rewind_sequence(:user, :email)
50+
generate(:user, :email)
51+
#=> "user_email_1@example.com"
52+
```

docs/src/sequences/sequence-uris.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Sequence URIs
2+
3+
There are many reasons to manipulate a specific sequence:
4+
5+
- generating a single value with: `generate`
6+
- generating multiple values with: `generate_list`
7+
- setting it to a new value with `FactoryBot.set_sequence`
8+
- rewinding it with: `rewind_sequence`
9+
10+
To accomplish this we need to be able to reference the desired sequence. This is achieved with its unique URI.
11+
12+
## URI Composition
13+
14+
Each URI is composed of up to three names:
15+
16+
| position | name | required |
17+
| :------: | -------------- | -------------------------------------------------------------------- |
18+
| 1. | factory name: | **if** - the sequence is defined within a Factory or a Factory Trait |
19+
| 2. | trait name: | **if** - the sequence is defined within a Trait |
20+
| 3. | sequence name: | **always required** |
21+
22+
The URI can be entered as individual symbols:
23+
24+
```ruby
25+
generate(:my_factory_name, :my_trait_name, :my_sequence_name)
26+
```
27+
28+
**or** as individual strings:
29+
30+
```ruby
31+
generate('my_factory_name', 'my_trait_name', 'my_sequence_name')
32+
```
33+
34+
**or** as a single resource string:
35+
36+
```ruby
37+
generate("my_factory_name/my_trait_name/my_sequence_name")
38+
```
39+
40+
## Full URI example
41+
42+
This example details all the possible scenarios, with the comments showing the URI used to generate a value for each specific sequence:
43+
44+
```ruby
45+
FactoryBot.define do
46+
sequence(:sequence) {|n| "global_sequence_#{n}"}
47+
# generate(:sequence)
48+
49+
trait :global_trait do
50+
sequence(:sequence) {|n| "global_trait_sequence_#{n}"}
51+
# generate(:global_trait, :sequence)
52+
end
53+
54+
factory :user do
55+
sequence(:sequence) {|n| "user_sequence_#{n}"}
56+
# generate(:user, :sequence)
57+
58+
trait :user_trait do
59+
sequence(:sequence) {|n| "user_trait_sequence_#{n}"}
60+
# generate(:user, :user_trait, :sequence)
61+
end
62+
63+
factory :author do
64+
sequence(:sequence) {|n| "author_sequence_#{n}"}
65+
# generate(:author, :sequence)
66+
67+
trait :author_trait do
68+
sequence(:sequence) {|n| "author_trait_sequence_#{n}"}
69+
# generate(:author, :author_trait, :sequence)
70+
end
71+
end
72+
end
73+
end
74+
```
75+
76+
## Multi URIs
77+
78+
It is possible for a single sequence to have multiple URIs.
79+
80+
If the factory or trait has aliases, the sequence will have an additional URI for each alias, or combination of aliases.
81+
82+
In this example, the same sequence can referenced in four different ways:
83+
84+
```ruby
85+
factory :user, aliases: [:author] do
86+
trait :user_trait, aliases: [:author_trait] do
87+
sequence(:sequence) {|n| "author_trait_sequence_#{n}"}
88+
end
89+
end
90+
91+
# generate(:user, :user_trait, :sequence)
92+
# generate(:user, :author_trait, :sequence)
93+
# generate(:author, :user_trait, :sequence)
94+
# generate(:author, :author_trait, :sequence)
95+
```
96+
97+
<div class='warning'>
98+
99+
## Important
100+
101+
- No matter how deeply nested, the factory name component of the URI is always the factory where the sequence is defined, not any parent factories.
102+
103+
- If a factory inherits a sequence, the URI must reference the factory where it was defined, not the one in which it is used.
104+
105+
</div>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Setting the value
2+
3+
When testing or working in the console, being able to set the sequence to a specific value, is incredibly helpful. This can be achieved by passing the [sequence URI](sequence-uris.md) and the new value to `FactoryBot.set_sequence`:
4+
5+
## Global Sequences
6+
7+
Global sequences are set with the sequence name and the new value:
8+
9+
```ruby
10+
FactoryBot.define do
11+
sequence(:char, 'a') {|c| "global_character_#{c}" }
12+
13+
factory :user do
14+
sequence(:name, %w[Jane Joe Josh Jayde John].to_enum)
15+
16+
trait :with_email do
17+
sequence(:email) {|n| "user_#{n}@example.com" }
18+
end
19+
end
20+
end
21+
22+
##
23+
# char
24+
generate(:char) # "global_character_a"
25+
FactoryBot.set_sequence(:char, 'z')
26+
generate(:char) # "global_character_z"
27+
28+
##
29+
# user name
30+
generate(:user, :name) # "Jane"
31+
FactoryBot.set_sequence(:user, :name, 'Jayde')
32+
generate(:user, :name) # "Jayde"
33+
34+
##
35+
# user email
36+
generate(:user, :with_email, :email) # "user_1@example.com"
37+
FactoryBot.set_sequence(:user, :with_email, :email, 1_234_567)
38+
generate(:user, :with_email, :email) # "user_1234567@example.com"
39+
```
40+
41+
<div class='warning'>
42+
43+
## Note
44+
45+
- The new value must match the sequence collection: You cannot pass a String to an Integer based sequence!
46+
47+
- An integer based sequence, such as a record ID, can accept any positive integer as the value.
48+
49+
- A fixed collection sequence can accept any value within the collection.
50+
51+
- An unlimited sequence, such as a character `sequence(:unlimited,'a')` will timeout if not found within the default maximum search time of three seconds.
52+
53+
- The timeout can be configured with: `FactoryBot.sequence_setting_timeout = 1.5`
54+
55+
</div>

lib/factory_bot.rb

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
require "factory_bot/decorator/disallows_duplicates_registry"
4747
require "factory_bot/decorator/invocation_tracker"
4848
require "factory_bot/decorator/new_constructor"
49+
require "factory_bot/uri_manager"
4950
require "factory_bot/linter"
5051
require "factory_bot/version"
5152

@@ -58,6 +59,9 @@ module FactoryBot
5859
mattr_accessor :automatically_define_enum_traits, instance_accessor: false
5960
self.automatically_define_enum_traits = true
6061

62+
mattr_accessor :sequence_setting_timeout, instance_accessor: false
63+
self.sequence_setting_timeout = 3
64+
6165
# Look for errors in factories and (optionally) their traits.
6266
# Parameters:
6367
# factories - which factories to lint; omit for all factories
@@ -73,17 +77,43 @@ def self.lint(*args)
7377

7478
# Set the starting value for ids when using the build_stubbed strategy
7579
#
76-
# Arguments:
77-
# * starting_id +Integer+
78-
# The new starting id value.
80+
# @param [Integer] starting_id The new starting id value.
7981
def self.build_stubbed_starting_id=(starting_id)
8082
Strategy::Stub.next_id = starting_id - 1
8183
end
8284

8385
class << self
86+
# @!method rewind_sequence(*uri_parts)
87+
# Rewind an individual global or inline sequence.
88+
#
89+
# @param [Array<Symbol>, String] uri_parts The components of the sequence URI.
90+
#
91+
# @example Rewinding a sequence by its URI parts
92+
# rewind_sequence(:factory_name, :trait_name, :sequence_name)
93+
#
94+
# @example Rewinding a sequence by its URI string
95+
# rewind_sequence("factory_name/trait_name/sequence_name")
96+
#
97+
# @!method set_sequence(*uri_parts, value)
98+
# Set the sequence to a specific value, providing the new value is within
99+
# the sequence set.
100+
#
101+
# @param [Array<Symbol>, String] uri_parts The components of the sequence URI.
102+
# @param [Object] value The new value for the sequence. This must be a value that is
103+
# within the sequence definition. For example, you cannot set
104+
# a String sequence to an Integer value.
105+
#
106+
# @example
107+
# set_sequence(:factory_name, :trait_name, :sequence_name, 450)
108+
# @example
109+
# set_sequence([:factory_name, :trait_name, :sequence_name], 450)
110+
# @example
111+
# set_sequence("factory_name/trait_name/sequence_name", 450)
84112
delegate :factories,
85113
:register_strategy,
86114
:rewind_sequences,
115+
:rewind_sequence,
116+
:set_sequence,
87117
:strategy_by_name,
88118
to: Internal
89119
end

0 commit comments

Comments
 (0)